Skip to content

feat: replace executor bond with execution signature#27

Draft
ozwaldorf wants to merge 6 commits into
masterfrom
feat/bondless-escrow
Draft

feat: replace executor bond with execution signature#27
ozwaldorf wants to merge 6 commits into
masterfrom
feat/bondless-escrow

Conversation

@ozwaldorf

@ozwaldorf ozwaldorf commented Jun 12, 2026

Copy link
Copy Markdown
Member

Implements the escrow side of docs/builder-funded-escrow-spec.md: removes the executor bond and gates collect() on an MPT proof plus an EIP-712 execution signature from the account that performed the transfer.

What changed

collect() no longer requires a bond or a bonded-executor claim. Instead:

  • the transfer sender is recovered on-chain from the proven transaction (recoverTxSender, tx-type aware: legacy / EIP-2930 / EIP-1559),
  • an EIP-712 ExecutionAuth signature from that sender authorizes the payoutAddress,
  • a single-shot collected flag replaces the bond lifecycle as the anti-race guard.

New collect signature (both flavors), matching the nomad bindings byte-for-byte:

collect(proof, targetBlockNumber, payoutAddress, executionSig)

This restores executor exclusivity (only the real transfer sender can direct the payout, defeating proof-replay front-running) without locking up capital.

Commits

feat: vendor OpenZeppelin ECDSA library audited recover + malleability/zero-address guards
feat: add tx sender recovery to ReceiptValidator on-chain from recovery from raw signed RLP
feat: add EIP-712 execution-signature verification to EscrowBase MirageEscrow/1 domain + ExecutionAuth digest
feat: replace executor bond with execution signature bond removal across base + both flavors, tests ported

Design notes

  • EIP-712 pinned to the nomad signer (crates/types/src/contracts.rs): domain MirageEscrow/1/chainId/escrow, struct ExecutionAuth(address expectedRecipient,uint256 expectedAmount,address payoutAddress). collect selectors verified: 0x3fbcc464 (Native), 0xec6b8596 (ERC20).

Gas

collect() costs ~+19k gas (~2%) for the signature work — but removing bond() drops a whole ~92-96k-gas transaction, so a full execution cycle is ~73-77k gas cheaper and requires zero locked capital.

Testing

47 tests pass. Spec cases covered: wrong-signer revert, observer-cannot-redirect, wrong-escrow replay, double-collect, payout-to-signed-address. Happy paths use a hermetic single-leaf MPT fixture built from a controlled key (so on-chain recoverTxSender == signer runs end-to-end). Real mainnet/Sepolia/Tempo fixtures still validate multi-node MPT branches at the library level.

Out of scope

EscrowERC20Delayed (deferred per spec) and all Titan/sponsorship/EOA-tier logic (nomad-side; zero Titan references in src/).

Recover the transfer tx's from on-chain from raw signed RLP, tx-type
aware (legacy/EIP-2930/EIP-1559). Reconstructs the per-type signing hash
and ecrecovers via the vendored ECDSA. Adds RLPParser.itemBounds and
readScalar helpers.
Adds the ExecutionAuth typed-data domain (MirageEscrow/1/chainId/escrow)
and digest, plus signer recovery, matching the off-chain nomad signer.
Bond machinery is untouched here and removed in a follow-up.
Remove the bond from EscrowBase, EscrowNative, and EscrowERC20. collect()
now takes (proof, targetBlockNumber, payoutAddress, executionSig) and is
gated on an EIP-712 ExecutionAuth signature that must come from the
transfer sender -- recovered tx `from` for native, Transfer event `from`
for ERC20 -- instead of a bonded-executor claim. A single-shot `collected`
flag replaces the bond lifecycle. Payout (reward + payment) goes to the
signed payoutAddress.

Tests ported: bond/deadline tests removed; collect happy-paths and the
front-run/replay/double-collect cases added with a hermetic single-leaf
proof fixture; real-fixture MPT validation preserved at the library level.
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.

1 participant