Skip to content

feat(evm): ProofBridgeUtils, one linked library both escrows share, for AdManager's EIP-170 headroom - #54

Merged
JoE11-y merged 2 commits into
mainfrom
feat/escrow-linked-libraries
Oct 5, 2026
Merged

JoE11-y merged 2 commits into
mainfrom
feat/escrow-linked-libraries

Conversation

@JoE11-y

@JoE11-y JoE11-y commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What this does

AdManager had 135 bytes left under the EVM's 24,576-byte contract-size limit. One more small fix (the contracts#53 fix pass is next) could have stopped it deploying.

The fix is the one Aave and Uniswap use. The five order helpers both escrows carried a copy of (decimal scaling, the width check, the order digest) now live in one deployed contract per chain, ProofBridgeUtils, that both escrows call into. DecimalScaling and OrderHash are untouched and still internal; ProofBridgeUtils is a thin public door in front of them.

contract before after
AdManager 24,441 (135 spare) 23,936 (640 spare)
OrderPortal 15,102 14,912
ProofBridgeUtils — 2,567

Nothing about an order changes: same hash, same reverts, same ABI, same Soroban side (no mirror needed, the Soroban ad-manager WASM is at about 55% of its cap).

Why one library and not the finalize-clock library discussed earlier

Measured, not guessed. Moving the finalize clock out made AdManager 70 bytes bigger: it needs sixteen inputs read from storage, and encoding them for the call costs more than the code it moves. What pays is many call sites with a small interface. The measurements and the rejected options are in the size hand-off; the docs page in the monorepo PR carries the summary.

Two things the compiler changes underneath, handled

  • The library's errors leave the escrow ABI. A revert inside the library reaches the caller intact, but solc no longer lists DecimalScaling__* / OrderHash__* on the escrows. They are declared again on IEscrow (and DecimalScaling__NonExactDownscale on IAdManager, the only escrow that scales). Both error lists are byte for byte what they were, so the relayer and frontend keep decoding them by name.
  • Each call costs gas. On the CI gate tests: AdManager escrow-only unlock 81,259 → 84,439 (ceiling 91,000), OrderPortal full unlock 369,232 → 371,719 (ceiling 400,000), recordSettled 218,383 → 220,905 (ceiling 245,000). lockForOrder pays about 8k more (six library calls, one cold). The ceilings are unchanged.

Deploy CLI

ProofBridgeUtils is ownerless, one per chain, deployed before the escrows and linked into both (the AgentPolicyCodec pattern). The manifest records it as contracts.proofBridgeUtils, and as for the codec that entry is a record, never an input:

  • a reused escrow is reused with the library its own code names; an escrow whose code names no working library is refused ("redeploy it instead of reusing");
  • a library counts only if its on-chain code equals this build's artifact (allowing solc's own-address stamp), or, for an earlier build's copy, it answers the probe: vector 0 of order-hash-v2.json hashes through the link, scale(1e6, 6, 18) is 1e18, assertInRange(31) reverts with the typed error;
  • a wrong manifest entry is repaired from the chain; a fresh copy is probed before anything links it.

One quirk worth knowing: solc hashes a library's struct parameter by its declared name (digest(OrderHash.Order)), not by the tuple the ABI lists, so ethers and cast call a digest selector the library does not have. The escrows are unaffected. The probe reads the selector from the artifact's methodIdentifiers.

Tripwire

test/SizeBudget.t.sol fails when a deployed escrow passes 24,200 bytes, 376 before the wall, so the next feature gets a red test with room to measure instead of a red build at the limit.

Tests on the door itself (review pass 1)

The first push tested only the internal libraries: every hashing and decimals test called OrderHash / DecimalScaling directly, so the five-line door the escrows actually run through was tested by nothing. test/ProofBridgeUtils.t.sol now pins each door function against the internal library through a real DELEGATECALL (digest over every frozen vector of order-hash-v2.json), and three escrow-level tests prove a lock is refused through the link and decodes by the error IEscrow re-declares.

Proven to fail

  • Re-inlining the five calls in AdManager puts it at 24,523 and the size test goes red.
  • Breaking the door, one function at a time, and what goes red:
    • digest → bytes32(0): test_digest_everyVectorThroughTheDoor (and an unlock test, every order now hashes alike).
    • scale → return amount: the three scale door tests.
    • assertMatchesOnChain → no-op: the door test and both escrow guard tests (test_lock_rejects_adDecimalsNotTheTokens, test_createOrder_rejects_orderDecimalsNotTheTokens).
    • checkWidths → no-op: the door test and test_createOrder_rejects_amountTooWide.
  • On Anvil: a reused escrow whose library was swapped for another contract is refused with nothing sent; a wrong manifest entry is repaired; another contract at the library's address fails the probe with the mismatch named; a stripped library artifact is refused before the first transaction; a failure that is not the library's answer (a stripped artifact mid-probe) is rethrown as itself, never read as "does not answer".
  • Release bundle scan: removing ProofBridgeUtils from EVM_ARTIFACTS now fails the scan (the loaders use literals and deployedCode / methodIdentifier are scanned).

Checks

  • forge test (non-invariant): 2057 passed. forge fmt --check clean. Error lists 71 / 52, unchanged.
  • Deploy CLI: typecheck clean, 49/49 specs, handover-check.sh 33/33 on two Anvils.
  • Release bundle list carries ProofBridgeUtils.

Monorepo half

The schema entry, the bundle fetcher list and the docs page are on feat/escrow-linked-library in ProofBridge; its PR follows this one and bumps the submodule pointer. Then contracts#53 rebases onto main so its fixes have room.

… for AdManager's EIP-170 headroom

AdManager had 135 bytes left under the 24,576-byte limit. The five order helpers both escrows
inlined (DecimalScaling.scale / assertInRange / assertMatchesOnChain, OrderHash.checkWidths /
digest) now sit behind one `public` library, ProofBridgeUtils, that the escrows DELEGATECALL:
AdManager 24,441 → 23,936 (640 spare), OrderPortal 15,102 → 14,912. DecimalScaling and OrderHash
stay internal and untouched; the hash, the reverts and the ABI are unchanged.

What the compiler changes underneath, handled:
- the seven DecimalScaling__* / OrderHash__* errors leave the escrow ABIs (they arrive through the
  DELEGATECALL), so they are declared again on IEscrow, NonExactDownscale on IAdManager; both error
  lists are byte-for-byte what they were;
- each call is a DELEGATECALL with ABI encoding: AdManager escrow-only unlock 81,259 → 84,439
  (ceiling 91,000), OrderPortal full unlock 369,232 → 371,719 (400,000), recordSettled 218,383 →
  220,905 (245,000); lockForOrder about 8k more. Ceilings unchanged.
- the finalize-clock library idea measured +70 B and is not done; the rule is many call sites x
  small interface.

Deploy CLI: the library is deployed before the escrows and linked into both. The manifest entry
`contracts.proofBridgeUtils` is a record, never an input: a reused escrow is reused with the library
its own code names, a library counts only if its code equals this build's artifact (allowing solc's
own-address stamp) or it answers the probe (vector 0 of order-hash-v2.json through the link, scale,
the typed decimals revert), a wrong entry is repaired from the chain, and a fresh copy is probed
before anything links it. The probe takes `digest`'s selector from the artifact's methodIdentifiers:
solc hashes a library's struct parameter by its declared name, not the ABI's tuple.

Tripwire: test/SizeBudget.t.sol fails at 24,200 deployed bytes, 376 before the wall (re-inlining the
five calls puts AdManager at 24,523 and the test goes red). Specs on Anvil: the probe refuses another
contract at the address, a reused escrow whose library no longer answers is refused with nothing
sent, and a wrong manifest entry is repaired. Release bundle list carries ProofBridgeUtils.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1027d18c-a11c-47a0-8e2d-92fdda05387d
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…nternal libraries, lock refusals through the link; review 54 pass 1

L1: every test that checked hashing or decimals called the internal libraries, so the five-line
door the escrows actually run through was tested by nothing (four mutations, suite green).
test/ProofBridgeUtils.t.sol pins each door function against DecimalScaling / OrderHash through a
real DELEGATECALL (digest over every frozen vector), and three escrow-level tests prove a lock is
refused through the link and decodes by the IEscrow re-declaration. Each of the four door
mutations now turns at least one test red.

Deploy CLI: L4 the probe rethrows anything that is not a revert (an RPC failure or a stripped
artifact is its own error, never "does not answer… redeploy the escrow"); L5 the library's
runtime code and digest selector are read before the first transaction, so a stripped bundle is
refused with nothing sent; L6 the loaders pass literal names and the release scan covers
deployedCode / methodIdentifier, so dropping ProofBridgeUtils from the bundle list fails the
scan (proven locally); L7 the frozen vector's field order is asserted against the fixture; L8 two
escrows linking different libraries is refused, not half-recorded; L9 unused DecimalScaling
imports dropped; L10 one verdict per address across the two escrows. Specs for L4 and L5.
@JoE11-y
JoE11-y merged commit 4c28171 into main Oct 5, 2026
11 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.

1 participant