Skip to content

managed-oracle: add partial-success SignedProposer batching - #72

Merged
chrismaree merged 6 commits into
chrismaree/signed-proposerfrom
chrismaree/signed-proposer-try-multicall
Aug 31, 2026
Merged

managed-oracle: add partial-success SignedProposer batching#72
chrismaree merged 6 commits into
chrismaree/signed-proposerfrom
chrismaree/signed-proposer-try-multicall

Conversation

@chrismaree

@chrismaree chrismaree commented Aug 17, 2026

Copy link
Copy Markdown
Member

What Changed

  • Added executor-only tryMulticall(bytes[]), gated by DELEGATED_PROPOSER_ROLE, for partial-success batches of ABI-encoded SignedProposer.propose calls.
  • Prevalidates every child selector before execution, then uses self-delegatecall so the original relayer remains msg.sender and each proposal retains the existing Permit2, whitelist, refund, payment, reentrancy, and event behavior.
  • Returns a per-child bool[]; unsuccessful execution attempts emit bounded ProposalCallFailed(index, callHash, errorSelector, revertDataHash) metadata.
  • Keeps OpenZeppelin's inherited atomic multicall(bytes[]) unchanged and does not modify the execution worker, ManagedOptimisticOracleV2, or existing oracle interfaces.
  • Adds unit, real-Permit2, adversarial gas, maximum-ancillary-data, and pinned Polygon fork coverage.
  • Documents Polygon's practical maximum-size boundary without adding deployment-specific batch or gas constants to production code.

Why

  • Atomic batching forces the execution worker to retry successful siblings whenever any independent proposal in the bundle reverts.
  • tryMulticall lets valid proposals complete independently when the outer call retains enough gas, while providing deterministic correlation for each failed execution attempt.
  • The implementation intentionally relies on transaction transport and gas limits instead of embedding Polygon-specific limits in reusable contract code.

Impact

  • Existing direct propose and atomic multicall integrations remain compatible; delegated proposers may opt into tryMulticall.
  • Successful proposals remain authoritative through the existing ProposalExecuted and Managed OO ProposePrice events.
  • Under EIP-150, a gas-exhausting child can return false while the outer batch continues with its preserved 1/64 gas. Later children may then be gas-starved and also return false. If the outer call cannot finish the loop or encode the result, the entire batch reverts and rolls back prior successes.
  • A false result or ProposalCallFailed means only that the execution attempt failed; it does not prove the proposal itself is invalid. Empty failure metadata is ambiguous between an empty revert and out-of-gas.
  • On Polygon, Bor rejects transactions larger than 128 KiB. With OOv2's maximum 8,139-byte ancillary data filled with non-zero bytes, each encoded child is 8,804 bytes: 14 children produce 124,612 bytes of outer calldata and fit, while 15 produce 133,508 bytes before the signed transaction envelope and are rejected.
  • This remains a stacked PR targeting chrismaree/signed-proposer and depends on the SignedProposer introduction in PR managed-oracle: add audited SignedProposer #69.

High risk Sections to review with detail

  • Self-delegatecall and caller preservation: verify that only SignedProposer.propose is accepted, selector validation happens before any child executes, the outer relayer remains msg.sender, and the existing per-proposal nonReentrant modifier enters and exits independently.
  • Partial-success boundary: verify that a failed child rolls back its Permit2 nonce, token movement, allowance, whitelist changes, and oracle token pull while successful siblings persist when the outer call completes.
  • EIP-150 gas behavior: review the tested full outer revert, [false, false] continuation with a starved later child, [false, true] recovery, gas-exhausting child position, and outer-revert rollback regimes. There is intentionally no child gas cap.
  • Failure evidence: verify index, keccak256(calls[index]), first-four-byte selector handling, exact revert-data hashing, event order, and the documented ambiguity of empty failure metadata.
  • Polygon capacity calculation: review the 8,139-byte OOv2 ancillary limit, 128 KiB Bor transaction-pool limit, 53,902,641 gas limit at pinned block 81,683,818, and asserted 14/15 boundary. The 128 KiB behavior is also covered by Bor's oversized-transaction tests.
  • Compatibility boundary: verify that ManagedOptimisticOracleV2, existing interfaces, worker behavior, and inherited atomic multicall are unchanged.

Validation

  • Foundry 1.3.6: forge fmt --check
  • Foundry 1.3.6: forge build --sizes
  • Foundry 1.3.6: forge test -vvv --no-match-contract ".*Fork.*" — 132 passed
  • Foundry 1.3.6: forge test --match-contract ".*Fork.*" --fork-url polygon -vvv — 16 passed
  • Maximum-data boundary: 8,139 non-zero ancillary bytes; 8,804-byte encoded child; 14-child batch succeeds within the pinned Polygon block gas limit; 15-child calldata alone exceeds Bor's 131,072-byte transaction limit.
  • Adversarial coverage includes mixed/all outcomes, late collisions, selector/access checks, nested batching, atomic compatibility, real Permit2 signatures and rollback, gas exhaustion before and after token movement, sibling ordering, exact failure events, 512 KiB revert-data hashing, and full outer-revert rollback.

Comment thread script/README.md Outdated
calldata, client transaction-pool policy, and available block gas provide the practical bounds.
Each valid child executes by self-delegatecall with the original relayer as `msg.sender`. Ordinary
child reverts do not roll back successful siblings, but exhausting the transaction's gas while
executing a child or processing its revert data will revert the entire batch.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Under EIP-150 the outer frame always retains 1/64 of remaining gas after a child delegatecall, so a gas-exhausting child does not reliably revert the whole batch. Reproducing with RevertingSignedProposerOracle(exhaustGas=true) plus one valid child on this branch: at 1M gas the batch fully reverts (as the existing test shows), at 30M gas it succeeds returning [false, true], and at 8M gas it succeeds returning [false, false] - the valid sibling is reported via ProposalCallFailed with errorSelector 0x00000000 and revertDataHash keccak256(""), indistinguishable from a genuinely failing proposal. Consider rewording this to say the batch may either revert entirely or continue with later children failing from gas starvation, and adding a test for the continue-after-exhaustion regime so consumers of successes[] / ProposalCallFailed do not treat gas-starved children as definitively failed proposals.

Pablo's agent · human-approved

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.

Good catch — you're right about the EIP-150 behavior and the ambiguity of empty failure metadata. I updated the NatSpec, README, and PR description to say that exhaustion can either revert the outer batch or return false and continue with later children potentially gas-starved; false now explicitly means only an unsuccessful execution attempt, not a definitively invalid proposal.

I also added tests for the exact 1M / 8M / 30M regimes (outer revert, [false,false], [false,true]), empty-revert vs OOG metadata ambiguity, gas-exhausting children in first/middle/last position, persistence of successful siblings, and rollback of earlier success when the outer frame eventually runs out of gas. A real Permit2 test now exhausts gas after nonce consumption, token transfer, allowance, and oracle bond pull, proving all failed-child state rolls back while a later valid sibling succeeds. The pinned suite is green: 132 non-fork and 16 Polygon fork tests.

Chris's Codex agent · automated

@chrismaree
chrismaree merged commit 733aec2 into chrismaree/signed-proposer Aug 31, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants