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
11 changes: 5 additions & 6 deletions pm-v2-oo-reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ liveness must be non-zero and inside the registered range. Each initialized requ
Automatic re-requests are enabled by default and can be disabled or re-enabled by the owner with
`setAutomaticRerequestsEnabled(...)`. The current setting is evaluated when a dispute or P4 settlement callback arrives,
not when the request was initialized or last re-requested. When enabled, the first dispute callback for a request
automatically creates one replacement request without consuming manual re-request budget. Later dispute callbacks open
the manual re-request gate and emit `RequestRerequestAllowed`.
attempts one replacement without consuming manual re-request budget. Failed attempts and later dispute callbacks open
the manual re-request gate without reverting the dispute.

P4 settlements intentionally reset the active request's manual budget to the current default. When automatic re-requests
are enabled, P4 settlements also create a replacement request without consuming manual budget. When automatic
re-requests are disabled, P4 settlements open the manual re-request gate instead so UMA-controlled oracle initializers can
continue recovery.
are enabled, P4 settlements also attempt a replacement without consuming manual budget. Disabled or failed attempts open
the manual re-request gate without reverting the settlement.

An enabled oracle initializer can call `rerequest(requestId, reward, proposalBond, liveness)` while the manual gate is
open and manual budget remains. Manual re-requests can update the active reward, proposal bond, and liveness for the
Expand All @@ -88,7 +87,7 @@ re-request-gate, rules-update, and resolution logs easy to correlate after a req
## Trusted Resolver Dependency

`OOReporter` stores final outcomes only after Managed OO settles the active request and calls `priceSettled(...)` with a
non-P4 price. A P4 settlement re-requests instead of finalizing.
non-P4 price. A P4 settlement attempts a re-request or opens the manual gate instead of finalizing.
Managed OO settlement is intentionally performed by trusted UMA resolver bots rather than permissionless callers. This
lets UMA use short liveness for routine proposals while resolver infrastructure can escalate uncertain requests to
human review before settlement.
Expand Down
49 changes: 30 additions & 19 deletions pm-v2-oo-reporter/src/OOReporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
emit RequestRerequestBudgetSet(requestId, newManualRerequestsRemaining);
}

/// @notice Managed OO dispute callback. Auto re-requests once, then opens the oracle-initializer re-request gate.
/// @notice Managed OO dispute callback. Attempts one auto re-request, otherwise opens the manual gate.
/// @inheritdoc IOptimisticRequester
function priceDisputed(bytes32 identifier, uint256 timestamp, bytes memory requestRules, uint256)
external
Expand All @@ -331,15 +331,19 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
_loadCallbackRequest(identifier, timestamp, requestRules);
if (shouldIgnore || request.resolved) return;

if (!request.automaticDisputeRerequestUsed && _canExecuteAutomaticRerequest(request)) {
request.automaticDisputeRerequestUsed = true;
_executeAutomaticRerequest(requestId, request, RerequestType.AutomaticDispute);
} else {
_allowRerequest(requestId, timestamp, request, RerequestTrigger.Dispute);
if (!request.automaticDisputeRerequestUsed && _shouldAttemptAutomaticRerequest(request)) {
try this.executeAutomaticRerequest(requestId, RerequestType.AutomaticDispute) {
request.automaticDisputeRerequestUsed = true;
return;
} catch {
emit AutomaticRerequestFailed(requestId, timestamp, RerequestType.AutomaticDispute);
}
}

_allowRerequest(requestId, timestamp, request, RerequestTrigger.Dispute);
}

/// @notice Managed OO settlement callback. Stores final prices; refreshes recovery budget and re-requests on P4.
/// @notice Managed OO settlement callback. Stores final prices; on P4, attempts an auto re-request or opens the gate.
/// @inheritdoc IOptimisticRequester
function priceSettled(bytes32 identifier, uint256 timestamp, bytes memory requestRules, int256 price)
external
Expand All @@ -353,17 +357,20 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
if (price == P4_PRICE) {
// Reporter requests are event-based, so Managed OO rejects proposed P4; P4 here is DVM-resolved.
// Refill the manual budget so an enabled UMA-controlled oracle initializer can continue recovery without
// owner intervention if automation is disabled.
// owner intervention if automation is unavailable.
uint256 budget = defaultRerequestBudget();
if (request.manualRerequestsRemaining != budget) {
request.manualRerequestsRemaining = budget;
emit RequestRerequestBudgetSet(requestId, budget);
}
if (_canExecuteAutomaticRerequest(request)) {
_executeAutomaticRerequest(requestId, request, RerequestType.AutomaticInvalidSettlement);
} else {
_allowRerequest(requestId, timestamp, request, RerequestTrigger.InvalidSettlement);
if (_shouldAttemptAutomaticRerequest(request)) {
try this.executeAutomaticRerequest(requestId, RerequestType.AutomaticInvalidSettlement) {
return;
} catch {
emit AutomaticRerequestFailed(requestId, timestamp, RerequestType.AutomaticInvalidSettlement);
}
}
_allowRerequest(requestId, timestamp, request, RerequestTrigger.InvalidSettlement);
} else {
request.resolved = true;
request.outcome = price;
Expand Down Expand Up @@ -400,6 +407,14 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
if (requestId == bytes32(0)) revert RequestNotRegistered();
}

/// @dev Keeps all re-request failure points inside the call frame caught by the callbacks.
function executeAutomaticRerequest(bytes32 requestId, RerequestType rerequestType) external {
if (msg.sender != address(this)) revert CallerNotSelf();

RequestData storage request = _requireRegistered(requestId);
_executeAutomaticRerequest(requestId, request, rerequestType);
}

/// @inheritdoc IOOReporter
function claimDeferredPayout(address repaymentAddress) external onlyOwner {
if (repaymentAddress == address(0)) revert AddressCannotBeZero();
Expand Down Expand Up @@ -464,12 +479,10 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
_emitRequestRerequested(requestId, request, previousRequestTimestamp, address(this), rerequestType);
}

/// @dev Handles expected local blockers only. Managed OO config failures still revert; reporter deprecation should
/// disable automation before de-whitelisting.
function _canExecuteAutomaticRerequest(RequestData storage request) private view returns (bool) {
/// @dev External checks must remain inside the catchable self-call.
function _shouldAttemptAutomaticRerequest(RequestData storage request) private view returns (bool) {
if (!automaticRerequestsEnabled()) return false;
if (block.timestamp <= request.requestTimestamp) return false;
return request.reward == 0 || rewardCurrency().balanceOf(address(this)) >= request.reward;
return block.timestamp > request.requestTimestamp;
}

/// @dev Emits the post-state for a replacement Managed OO request.
Expand Down Expand Up @@ -520,8 +533,6 @@ contract OOReporter is OwnableUpgradeable, UUPSUpgradeable, MulticallUpgradeable
oracle.setBond(priceIdentifier, requestTimestamp, requestRules, proposalBond);
}

// Unexpected Managed OO config drift should surface instead of silently opening the manual gate.
// For example, this can fail if oracle liveness config changed since the bounds were registered.
oracle.setCustomLiveness(priceIdentifier, requestTimestamp, requestRules, liveness);
}

Expand Down
6 changes: 6 additions & 0 deletions pm-v2-oo-reporter/src/interfaces/IOOReporter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ interface IOOReporter {
error CallerNotRequestRegistrar();
/// @notice Thrown when the caller is not the configured Managed Optimistic Oracle.
error CallerNotOptimisticOracle();
/// @notice Thrown when an external self-call helper is called by another address.
error CallerNotSelf();
/// @notice Thrown when the caller is not an enabled UMA oracle initializer.
error CallerNotOracleInitializer();
/// @notice Thrown when a requester allowlist update would not change state.
Expand Down Expand Up @@ -173,6 +175,10 @@ interface IOOReporter {
event RequestRerequestAllowed(
bytes32 indexed requestId, uint256 indexed requestTimestamp, RerequestTrigger indexed trigger
);
/// @notice Emitted when an automatic re-request fails and the callback falls back to the manual gate.
event AutomaticRerequestFailed(
bytes32 indexed requestId, uint256 indexed requestTimestamp, RerequestType indexed rerequestType
);
/// @notice Emitted when the reporter creates a replacement Managed OO request.
/// @dev proposalBond and liveness are reporter-requested parameters. Effective proposal-time values can differ
/// if Managed OO request-manager preconfigs apply.
Expand Down
69 changes: 69 additions & 0 deletions pm-v2-oo-reporter/test/OOReporter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ contract OOReporterTest {
event RequestRerequestAllowed(
bytes32 indexed requestId, uint256 indexed requestTimestamp, RerequestTrigger indexed trigger
);
event AutomaticRerequestFailed(
bytes32 indexed requestId, uint256 indexed requestTimestamp, RerequestType indexed rerequestType
);
event RequestRerequested(
bytes32 indexed requestId,
uint256 indexed requestTimestamp,
Expand Down Expand Up @@ -481,6 +484,40 @@ contract OOReporterTest {
assertEq(afterAuto.manualRerequestsRemaining, DEFAULT_REREQUEST_BUDGET, "dispute should not consume budget");
}

function test_executeAutomaticRerequestRejectsNonSelfCaller() external {
vm.expectRevert(IOOReporter.CallerNotSelf.selector);
reporter.executeAutomaticRerequest(REQUEST_ID, RerequestType.AutomaticDispute);
}

function test_priceDisputedOpensManualGateWhenAutomaticRerequestReverts() external {
bytes memory requestRules = _requestRules("primary");
_registerRequest(REQUEST_ID, BINARY_IDENTIFIER, requestRules);

vm.prank(oracleInitializer);
reporter.initializeRequest(REQUEST_ID, 0, PROPOSAL_BOND, LIVENESS);

RequestData memory request = reporter.getRequest(REQUEST_ID);
vm.warp(block.timestamp + 1);
optimisticOracle.setMinimumDisputeWindow(uint256(LIVENESS) + 1);

vm.expectEmit(address(reporter));
emit AutomaticRerequestFailed(REQUEST_ID, request.requestTimestamp, RerequestType.AutomaticDispute);

optimisticOracle.disputePrice(address(reporter), BINARY_IDENTIFIER, request.requestTimestamp, requestRules);

RequestData memory allowedRequest = reporter.getRequest(REQUEST_ID);
assertTrue(allowedRequest.rerequestAllowed, "failed automatic re-request should open manual gate");
assertFalse(
allowedRequest.automaticDisputeRerequestUsed,
"failed automatic re-request should not consume automatic slot"
);

bytes32 replacementKey =
optimisticOracle.requestKey(address(reporter), BINARY_IDENTIFIER, block.timestamp, requestRules);
MockOptimisticOracleV2.MockRequest memory replacementRequest = optimisticOracle.getMockRequest(replacementKey);
assertFalse(replacementRequest.requested, "failed automatic re-request should roll back replacement request");
}

function test_priceDisputedOpensManualGateAfterAutomaticDisputeUsed() external {
bytes memory requestRules = _requestRules("primary");
_registerRequest(REQUEST_ID, BINARY_IDENTIFIER, requestRules);
Expand Down Expand Up @@ -690,6 +727,38 @@ contract OOReporterTest {
reporter.getRequestResolution(REQUEST_ID);
}

function test_priceSettledP4OpensManualGateWhenAutomaticRerequestReverts() external {
bytes memory requestRules = _requestRules("primary");
_registerRequest(REQUEST_ID, BINARY_IDENTIFIER, requestRules);

vm.prank(oracleInitializer);
reporter.initializeRequest(REQUEST_ID, 0, PROPOSAL_BOND, LIVENESS);

RequestData memory request = reporter.getRequest(REQUEST_ID);
vm.warp(block.timestamp + 1);
optimisticOracle.setMinimumDisputeWindow(uint256(LIVENESS) + 1);

vm.expectEmit(address(reporter));
emit AutomaticRerequestFailed(REQUEST_ID, request.requestTimestamp, RerequestType.AutomaticInvalidSettlement);

optimisticOracle.settle(
address(reporter), BINARY_IDENTIFIER, request.requestTimestamp, requestRules, reporter.P4_PRICE()
);

RequestData memory afterP4 = reporter.getRequest(REQUEST_ID);
assertTrue(afterP4.rerequestAllowed, "failed automatic P4 re-request should open manual gate");

bytes32 activeRequestKey =
optimisticOracle.requestKey(address(reporter), BINARY_IDENTIFIER, request.requestTimestamp, requestRules);
MockOptimisticOracleV2.MockRequest memory activeRequest = optimisticOracle.getMockRequest(activeRequestKey);
assertTrue(activeRequest.settled, "P4 settlement should persist when automatic re-request fails");

bytes32 replacementKey =
optimisticOracle.requestKey(address(reporter), BINARY_IDENTIFIER, block.timestamp, requestRules);
MockOptimisticOracleV2.MockRequest memory replacementRequest = optimisticOracle.getMockRequest(replacementKey);
assertFalse(replacementRequest.requested, "failed automatic P4 re-request should roll back replacement request");
}

function test_priceSettledP4OpensManualGateWhenAutomaticRerequestsDisabled() external {
bytes memory requestRules = _requestRules("primary");
_registerRequest(REQUEST_ID, BINARY_IDENTIFIER, requestRules);
Expand Down
Loading