Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ ast = true
build_info = true
extra_output = ["storageLayout"]
optimizer = true
optimizer_runs = 25
via_ir = true

# Fork testing configuration
[rpc_endpoints]
Expand Down
24 changes: 12 additions & 12 deletions pm-v2-oo-reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ domain-separate request rules so their UMA request identities differ.
- UMA oracle initializer allowlisting;
- Managed OO request creation;
- reward, bond, request-specific liveness bounds, automatic re-request controls, and manual re-request budget controls;
- request rules update history for active requests;
- forwarding request rules updates to the Managed OO for active requests;
- raw UMA settlement storage.

The prediction market integration owns:
Expand Down Expand Up @@ -99,20 +99,20 @@ timeout or administrative recovery path in the market-side module that translate

## Rules Updates

Only the requester that registered a `requestId` can update rules for that request. Rules updates are append-only history keyed by `requestId`; they do not replace the original registered rules or create a new `(priceIdentifier, updatedRules)` lookup alias. Consumers should use `requestId` as the stable identity when reading update history.
Only the requester that registered a `requestId` can update rules for that request. The reporter does not store update
history itself; it forwards the update to the Managed OO via
`updateRequestRules(priceIdentifier, requestRules, updatedRules)`, which records append-only history keyed by its managed
request id and emits its own `RequestRulesUpdated` event.

Rules updates are stored as:
Rules updates do not replace the original registered rules or create a new `(priceIdentifier, updatedRules)` reporter
lookup alias. Consumers should use `requestId` as the stable reporter identity and read canonical update history from the
Managed OO using the reporter address plus the original `(priceIdentifier, requestRules)` tuple.

```solidity
struct RequestRulesUpdate {
uint256 timestamp;
bytes updatedRules;
}
```

The rules update event includes the updater address for self-contained logs. Stored attribution is derivable from `getRequest(requestId).requester`.
The reporter additionally emits a `RequestRulesUpdated` event carrying the requester-facing `requestId` and updater
address for self-contained logs.

Rules updates are rejected after the request has resolved.
For a registered request, updates can be forwarded before or after Managed OO initialization. The reporter rejects rules
updates after the request has resolved.

## Foundry Dependency Import

Expand Down
47 changes: 2 additions & 45 deletions pm-v2-oo-reporter/src/OOReporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import {
IOOReporter,
RequestData,
RequestRulesUpdate,
RerequestTrigger,
RerequestType
} from "./interfaces/IOOReporter.sol";
import {IOOReporter, RequestData, RerequestTrigger, RerequestType} from "./interfaces/IOOReporter.sol";
import {IOptimisticOracleV2} from "./interfaces/IOptimisticOracleV2.sol";
import {IOptimisticRequester} from "./interfaces/IOptimisticRequester.sol";

Expand Down Expand Up @@ -56,8 +50,6 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
mapping(bytes32 requestId => RequestData request) requests;
/// @notice Mapping of `(priceIdentifier, requestRules)` key to requester-defined request ID.
mapping(bytes32 reporterRequestKey => bytes32 requestId) requestIdsByReporterKey;
/// @notice Mapping of requester-defined request ID to request rules update history.
mapping(bytes32 requestId => RequestRulesUpdate[] updates) requestRulesUpdates;
/// @notice Default re-request budget seeded onto each request at initialization.
uint256 defaultRerequestBudget;
/// @notice Whether first-dispute and P4 automatic re-requests are enabled.
Comment on lines 53 to 55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the removed storage slot

When upgrading an existing OOReporter proxy, removing requestRulesUpdates from the ERC-7201 storage struct shifts every following member down one slot, so defaultRerequestBudget will read the old mapping slot and automaticRerequestsEnabled will read the old budget slot. This corrupts live configuration after upgrade and can break initialization/re-request behavior; keep a deprecated mapping/placeholder slot (or otherwise preserve layout) even if the reporter no longer uses it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OO reporter proxy has never been deployed, so this is not a concern.

Expand Down Expand Up @@ -250,9 +242,7 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
if (request.resolved) revert RequestAlreadyResolved();
if (msg.sender != request.requester) revert CallerNotRequestRegistrar();

RequestRulesUpdate memory requestRulesUpdate =
RequestRulesUpdate({timestamp: block.timestamp, updatedRules: updatedRules});
_getStorage().requestRulesUpdates[requestId].push(requestRulesUpdate);
optimisticOracle().updateRequestRules(request.priceIdentifier, request.requestRules, updatedRules);

emit RequestRulesUpdated(requestId, block.timestamp, msg.sender, updatedRules);
}
Expand Down Expand Up @@ -410,39 +400,6 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
if (requestId == bytes32(0)) revert RequestNotRegistered();
}

/// @inheritdoc IOOReporter
function getRequestRulesUpdates(bytes32 requestId) public view returns (RequestRulesUpdate[] memory) {
_requireRegistered(requestId);
return _getStorage().requestRulesUpdates[requestId];
}

/// @inheritdoc IOOReporter
function getLatestRequestRulesUpdate(bytes32 requestId) public view returns (RequestRulesUpdate memory) {
_requireRegistered(requestId);
RequestRulesUpdate[] storage updates = _getStorage().requestRulesUpdates[requestId];
uint256 updateCount = updates.length;
if (updateCount == 0) revert RequestRulesUpdateUnavailable();
return updates[updateCount - 1];
}

/// @inheritdoc IOOReporter
function getRequestRulesUpdates(bytes32 priceIdentifier, bytes calldata requestRules)
external
view
returns (RequestRulesUpdate[] memory)
{
return getRequestRulesUpdates(getRequestId(priceIdentifier, requestRules));
}

/// @inheritdoc IOOReporter
function getLatestRequestRulesUpdate(bytes32 priceIdentifier, bytes calldata requestRules)
external
view
returns (RequestRulesUpdate memory)
{
return getLatestRequestRulesUpdate(getRequestId(priceIdentifier, requestRules));
}

/// @inheritdoc IOOReporter
function claimDeferredPayout(address repaymentAddress) external onlyOwner {
if (repaymentAddress == address(0)) revert AddressCannotBeZero();
Expand Down
49 changes: 4 additions & 45 deletions pm-v2-oo-reporter/src/interfaces/IOOReporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ struct RequestData {
uint64 maximumLiveness;
}

struct RequestRulesUpdate {
/// @notice Block timestamp when the rules update was posted.
uint256 timestamp;
/// @notice Updated prediction market request rules.
bytes updatedRules;
}

enum RerequestTrigger {
/// @dev Managed OO dispute callback opened the re-request gate.
Dispute,
Expand Down Expand Up @@ -129,8 +122,6 @@ interface IOOReporter {
error RequestLivenessOutOfRange(uint64 liveness, uint64 minimumLiveness, uint64 maximumLiveness);
/// @notice Thrown when a final reporter outcome is requested before one is available.
error RequestResolutionUnavailable();
/// @notice Thrown when the latest rules update is requested before any update is posted.
error RequestRulesUpdateUnavailable();
/// @notice Thrown when Managed OO has no deferred reward payout for the reporter.
error DeferredPayoutUnavailable(address rewardCurrency);
/// @notice Thrown when the reporter cannot fund a requested reward amount.
Expand Down Expand Up @@ -280,10 +271,10 @@ interface IOOReporter {
uint64 maximumLiveness
) external;

/// @notice Posts updated request rules for offchain consumers without changing the active OO tuple.
/// @dev Updates are append-only history keyed by requestId. They do not replace the original request rules
/// registered for the request, and they do not create or reserve a `(priceIdentifier, updatedRules)` lookup alias.
/// Consumers should treat requestId as the stable identity for reading update history.
/// @notice Forwards updated request rules to the Managed OO, which records them against the active OO request.
/// @dev Does not change the active OO request tuple or reporter lookup key. The reporter forwards the original
/// request rules to Managed OO, so updates do not create or reserve a `(priceIdentifier, updatedRules)` alias.
/// Consumers should treat requestId as the stable reporter identity and read canonical update history from Managed OO.
/// @param requestId Registered request ID.
/// @param updatedRules Updated prediction market request rules.
function updateRequestRules(bytes32 requestId, bytes calldata updatedRules) external;
Expand Down Expand Up @@ -336,38 +327,6 @@ interface IOOReporter {
/// @return requestId Registered request ID.
function getRequestId(bytes32 priceIdentifier, bytes calldata requestRules) external view returns (bytes32);

/// @notice Returns all request-rules updates posted for requestId.
/// @param requestId Registered request ID.
/// @return Request-rules update history.
function getRequestRulesUpdates(bytes32 requestId) external view returns (RequestRulesUpdate[] memory);

/// @notice Returns the latest request-rules update posted for requestId.
/// @param requestId Registered request ID.
/// @return Latest request-rules update.
function getLatestRequestRulesUpdate(bytes32 requestId) external view returns (RequestRulesUpdate memory);

/// @notice Returns all request-rules updates posted for a UMA request identity.
/// @dev `requestRules` must be the original rules supplied to registerRequest, not a value previously posted
/// through updateRequestRules. Prefer the requestId overload when the stable external request ID is already known.
/// @param priceIdentifier UMA price identifier.
/// @param requestRules Original raw UMA request rules registered for the request.
/// @return Request-rules update history.
function getRequestRulesUpdates(bytes32 priceIdentifier, bytes calldata requestRules)
external
view
returns (RequestRulesUpdate[] memory);

/// @notice Returns the latest request-rules update posted for a UMA request identity.
/// @dev `requestRules` must be the original rules supplied to registerRequest, not a value previously posted
/// through updateRequestRules. Prefer the requestId overload when the stable external request ID is already known.
/// @param priceIdentifier UMA price identifier.
/// @param requestRules Original raw UMA request rules registered for the request.
/// @return Latest request-rules update.
function getLatestRequestRulesUpdate(bytes32 priceIdentifier, bytes calldata requestRules)
external
view
returns (RequestRulesUpdate memory);

/// @notice Claims a Managed OO deferred reward payout owed to this reporter.
/// @param repaymentAddress Address to receive the claimed payout.
function claimDeferredPayout(address repaymentAddress) external;
Expand Down
7 changes: 7 additions & 0 deletions pm-v2-oo-reporter/src/interfaces/IOptimisticOracleV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ interface IOptimisticOracleV2 {
bool callbackOnPriceSettled
) external;

/// @notice Records a request rules update against the request identified by the caller, identifier, and rules.
/// @dev Keyed without a timestamp, so it applies before initialization and across re-requests.
/// @param identifier Price identifier to identify the existing request.
/// @param requestRules Request rules of the price being requested.
/// @param updatedRules Updated request rules to record for the request.
function updateRequestRules(bytes32 identifier, bytes memory requestRules, bytes memory updatedRules) external;

/// @notice Gets current request data for an OO request tuple.
/// @param requester Sender of the initial price request.
/// @param identifier Price identifier to identify the existing request.
Expand Down
37 changes: 10 additions & 27 deletions pm-v2-oo-reporter/test/OOReporter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
pragma solidity 0.8.34;

import {OOReporter} from "src/OOReporter.sol";
import {
IOOReporter,
RequestData,
RequestRulesUpdate,
RerequestTrigger,
RerequestType
} from "src/interfaces/IOOReporter.sol";
import {IOOReporter, RequestData, RerequestTrigger, RerequestType} from "src/interfaces/IOOReporter.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
import {MockOptimisticOracleV2} from "test/mocks/MockOptimisticOracleV2.sol";

Expand Down Expand Up @@ -348,7 +342,7 @@ contract OOReporterTest {
reporter.initializeRequest(REQUEST_ID, 0, 0, STRICT_MINIMUM_LIVENESS);
}

function test_updateRequestRulesStoresAndReadsByRequestIdAndTuple() external {
function test_updateRequestRulesForwardsToOptimisticOracle() external {
bytes memory requestRules = _requestRules("primary");
bytes memory firstUpdatedRules = bytes("first rules update");
bytes memory secondUpdatedRules = bytes("second rules update");
Expand All @@ -367,22 +361,14 @@ contract OOReporterTest {
RequestData memory request = reporter.getRequest(REQUEST_ID);
assertEq(request.requester, requester, "stored requester mismatch");

RequestRulesUpdate[] memory updates = reporter.getRequestRulesUpdates(REQUEST_ID);
assertEq(updates.length, 2, "request rules update count mismatch");
assertEq(updates[0].updatedRules, firstUpdatedRules, "first rules update mismatch");
assertEq(updates[1].updatedRules, secondUpdatedRules, "second rules update mismatch");

RequestRulesUpdate memory latest = reporter.getLatestRequestRulesUpdate(REQUEST_ID);
assertEq(latest.timestamp, block.timestamp, "latest timestamp mismatch");
assertEq(latest.updatedRules, secondUpdatedRules, "latest rules update mismatch");

RequestRulesUpdate[] memory tupleRulesUpdates = reporter.getRequestRulesUpdates(BINARY_IDENTIFIER, requestRules);
assertEq(tupleRulesUpdates.length, 2, "tuple update count mismatch");
assertEq(tupleRulesUpdates[1].updatedRules, secondUpdatedRules, "tuple latest list mismatch");

RequestRulesUpdate memory tupleRulesLatest =
reporter.getLatestRequestRulesUpdate(BINARY_IDENTIFIER, requestRules);
assertEq(tupleRulesLatest.updatedRules, secondUpdatedRules, "tuple latest mismatch");
// The reporter does not store rules updates; it forwards them to the Managed OO keyed by its own address as
// the requester, the price identifier, and the original request rules.
MockOptimisticOracleV2.ForwardedRulesUpdate[] memory forwarded =
optimisticOracle.getForwardedRulesUpdates(address(reporter), BINARY_IDENTIFIER, requestRules);
assertEq(forwarded.length, 2, "forwarded update count mismatch");
assertEq(forwarded[0].updatedRules, firstUpdatedRules, "first forwarded rules mismatch");
assertEq(forwarded[1].updatedRules, secondUpdatedRules, "second forwarded rules mismatch");
assertEq(forwarded[1].timestamp, block.timestamp, "latest forwarded timestamp mismatch");
}

function test_updateRequestRulesRejectsUnknownAndWrongRequester() external {
Expand Down Expand Up @@ -438,9 +424,6 @@ contract OOReporterTest {

vm.expectRevert(IOOReporter.RequestResolutionUnavailable.selector);
reporter.getRequestResolution(REQUEST_ID);

vm.expectRevert(IOOReporter.RequestRulesUpdateUnavailable.selector);
reporter.getLatestRequestRulesUpdate(REQUEST_ID);
}

function test_priceSettledStoresRawBinaryPrices() external {
Expand Down
23 changes: 23 additions & 0 deletions pm-v2-oo-reporter/test/mocks/MockOptimisticOracleV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ contract MockOptimisticOracleV2 is IOptimisticOracleV2 {
int256 price;
}

struct ForwardedRulesUpdate {
uint256 timestamp;
bytes updatedRules;
}

mapping(bytes32 requestKey => MockRequest request) public requests;
mapping(bytes32 rulesKey => ForwardedRulesUpdate[] updates) public forwardedRulesUpdates;
mapping(IERC20 currency => mapping(address deferredRecipient => uint256 amount)) public deferredPayouts;
uint256 public minimumDisputeWindow = 5 minutes;
bool public deferNextDisputeRefund;
Expand Down Expand Up @@ -127,6 +133,23 @@ contract MockOptimisticOracleV2 is IOptimisticOracleV2 {
requests[requestKey(msg.sender, identifier, timestamp, requestRules)].customLiveness = customLiveness;
}

function updateRequestRules(bytes32 identifier, bytes memory requestRules, bytes memory updatedRules) external {
bytes32 key = rulesKey(msg.sender, identifier, requestRules);
forwardedRulesUpdates[key].push(ForwardedRulesUpdate({timestamp: block.timestamp, updatedRules: updatedRules}));
}

function rulesKey(address requester, bytes32 identifier, bytes memory requestRules) public pure returns (bytes32) {
return keccak256(abi.encode(requester, identifier, requestRules));
}

function getForwardedRulesUpdates(address requester, bytes32 identifier, bytes memory requestRules)
external
view
returns (ForwardedRulesUpdate[] memory)
{
return forwardedRulesUpdates[rulesKey(requester, identifier, requestRules)];
}

function disputePrice(address requester, bytes32 identifier, uint256 timestamp, bytes memory requestRules)
external
{
Expand Down
Loading
Loading