BM-3078: feat(contracts): run the router market on Taiko via the shared Shanghai profile - #2044
Merged
Conversation
Introduces the verification engine that decouples per-class verification dispatch from BoundlessMarket. The router owns a two-mapping registry (entries + classes) with namespace invariants, dispatches per-fill verification on interfaceTag (verifier or joint), and forwards to the class's required assessor when the verifier class is per-fill. UUPS upgradeable, governance-gated `addClass` / `instantiate` / `removeClass` / `removeEntry`, ERC-165 conformance check at instantiation, gas-capped adapter calls so a misbehaving impl can self-rug its sub-batch but not starve sibling sub-batches in the same transaction. Three seam interfaces: - IBoundlessVerifier: per-fill cryptographic check (claim digest only). - IBoundlessJointVerifierAssessor: per-fill combined check + binding. - IBoundlessAssessor: per-batch binding seam. Tests will land in a follow-up.
Phase B of the verifier-router-and-assessor-decoupling epic. Adds two adapter contracts that bridge the existing R0 STARK verification path into the router's universal seam interfaces, so today's R0 Groth16, set-inclusion, and Blake3-Groth16 selectors plug into the new dispatch unchanged. R0BoundlessVerifierAdapter: thin wrapper around any IRiscZeroVerifier. One adapter per BoundlessRouter selector entry, each pinned to a specific underlying verifier — no transitive trust of the upstream R0 router's selector set. Phase C deployment script wires up one adapter per existing R0 selector under the R0_VERIFIER class. R0BoundlessAssessorAdapter: reconstructs today's assessor journal digest verbatim from a seal envelope, then forwards to IRiscZeroVerifier.verify against the pinned image id. The narrow IBoundlessAssessor interface stays universal (rds, cds, seal); journal extras (per-fill id and fulfillmentDataDigest; per-batch callbacks, selectors, prover) ride inside the seal as an envelope, so other assessor classes (signature-batch, threshold-attested, future SP1) plug into the same interface without inheriting R0-STARK-specific fields. The adapter is fully immutable — image-id rotation happens by deploying a new adapter and registering a new R0_ASSESSOR selector in parallel, then tombstoning the old one when ready (mirrors today's DEPRECATED_ASSESSOR_EXPIRES_AT pattern but managed via governance rather than an in-contract timestamp). A TODO calls out post-Phase C guest cleanup (drop redundant journal fields once the market sources them from the verified ProofRequest) for the next image rotation. Also includes a one-line whitespace fix on BoundlessRouter.sol from forge fmt. Tests will land in a follow-up.
… seams Widens IBoundlessAssessor.verifyAssessor and IBoundlessJointVerifierAssessor.verifyJoint to take `address prover` as a universal arg, alongside the existing request/claim digests and seal. The router's verifySubBatch gains the arg and forwards it to both per-fill (joint) and per-batch (assessor) dispatch sites. The market needs a trusted prover address for crediting and slashing. Today's R0 STARK assessor binds it via the journal commitment, but that's adapter-specific — a future signature-based assessor would have to commit to it in the signing payload, threshold-attested impls in their committee message, etc. Making `prover` part of the universal interface forces every adapter to verify the binding via its own mechanism, so the market can trust the value uniformly across class types. R0BoundlessAssessorAdapter: drops `prover` from Envelope (it's now an arg), uses the arg directly in journal reconstruction. The R0 STARK fails if the seal was produced against a different prover than the one passed by the caller. Tests will land in a follow-up.
…sRouter Reshapes the market entrypoints around `SubBatch[]` and ProofRequest- based fulfill, dispatching all verification through the router. Constructor: drops the verifier / applicationVerifier / assessorId / deprecatedAssessorId / deprecatedDuration immutables. Now takes `(BoundlessRouter router, address collateralToken)`. The router holds the verification engine; image-id rotation is adapter-level. Entrypoints: `fulfill`, `fulfillAndWithdraw`, `verifyDelivery`, and the four `submitRootAndFulfill*` variants now take `SubBatch[]`. Each sub-batch carries its own per-fill `ProofRequest[]` and `Fulfillment[]` plus a single `bytes assessorSeal` and `address prover`. The market re-derives `requestDigest` from each request, asserts integrity against the lock (locked path) or signature (priceAndFulfill, where `bytes[][] clientSignatures` is provided per sub-batch), and forwards `(rds, cds, seals, signedSelectors, prover, assessorSeal)` to `router.verifySubBatch`. `signedSelectors` and per-fill callbacks are sourced from the verified `ProofRequest`, not from any assessor journal commitment — `AssessorReceipt` is dropped entirely. The internal `_fulfillAndPay*` helpers take the verified `requestDigest` rather than reading `fill.requestDigest`. The new `MismatchedRequestId` error guards against a `Fulfillment.id` that disagrees with `request.id`. `verifyDelivery` is now just a per-sub-batch loop into the router; the old per-fill merkle reconstruction lives in `R0BoundlessAssessorAdapter`. The deprecated-assessor try/catch fallback at the market level is gone — image rotation is handled by deploying a fresh adapter under a new `R0_ASSESSOR` selector and manually tombstoning the old one when ready. Also: - `imageInfo()`, `setImageUrl()`, and the `imageUrl` storage variable are removed (slot reserved as `__deprecated_imageUrl` to preserve storage layout for upgrades). - `BoundlessMarketLib.encodeConstructorArgs` updated to the new shape. - `Deploy.s.sol` / `Manage.s.sol` updated to read `BOUNDLESS_ROUTER` from the env until the deployment.toml schema is updated to carry it. Tests will land in a follow-up.
…Router
Splits BoundlessRouter operational tooling into its own scripts so the
market scripts stay focused on the market lifecycle.
`Deploy.Router.s.sol` — bootstrap-only. Deploys the router UUPS proxy
and registers the two curated R0 classes:
- `R0_ASSESSOR` (id 0xAA000002) — terminal assessor seam.
- `R0_VERIFIER` (id 0xAA000001) — chain default; required assessor
class is `R0_ASSESSOR`.
Reads `ROUTER_ADMIN` and `DEPLOYER_PRIVATE_KEY` from env.
`Manage.Router.s.sol` — three operations as separate Script contracts:
- `RegisterR0Verifier`: deploy an `R0BoundlessVerifierAdapter` for
one R0 selector and `instantiate` it under `R0_VERIFIER`. Looks up
the underlying impl via the upstream `RiscZeroVerifierRouter`.
- `RegisterR0Assessor`: deploy an `R0BoundlessAssessorAdapter`
pinned to one image id and `instantiate` it under `R0_ASSESSOR` at
a chosen selector. Brokers put that selector in the first 4 bytes
of the assessor seal.
- `RemoveEntry`: tombstone an entry (e.g. a deprecated adapter
after a broker rollover).
The market scripts (`Deploy.s.sol`, `Manage.s.sol`) stay untouched
here — they consume an already-deployed router via the BOUNDLESS_ROUTER
env var. Market upgrades to the router-aware implementation use
`Manage.s.sol::UpgradeBoundlessMarket` after this script set has run
to set up the router infrastructure.
Also: rename the deprecated `__deprecated_imageUrl` storage slot back
to its original `imageUrl` name. The market no longer reads or writes
this field, but keeping the original name preserves the storage
layout for the OZ Upgrades safety check without needing a rename
annotation.
Mirrors the contract-layer rewrite: - IBoundlessMarket.sol artifact picks up the SubBatch-based entrypoints and drops AssessorReceipt + imageInfo. - SubBatch.sol artifact is new. - bytecode.rs regenerated for the new market implementation. CI verifies these checked-in artifacts haven't drifted from the source contracts, so they need to land alongside the contract changes.
Replace ProofRequest in SubBatch with a slim per-fill payload carrying only what the market and assessor need at fulfill time. The market reconstructs each requestDigest from the slim payload and asserts it matches the value stored at lock time (or via FulfillmentContext for the priced path) before dispatching, so downstream consumers can trust the payload without re-verification. Highlights: - New SlimRequest type + reconstruction library; predicate / callback / selector in full, plus pre-computed imageUrlHash / inputDigest / offerDigest. - SubBatch.requests is now SlimRequest[]. - Fulfillment drops the redundant id and requestDigest fields. - IBoundlessAssessor.verifyAssessor widened to (SlimRequest[], Fulfillment[], requestDigests[], prover, seal); BoundlessRouter.verifySubBatch matches. - BoundlessMarket inlines verifyDelivery into fulfill, adds explicit _verifyBinding before router dispatch, and drops the ProofRequest arg from _fulfillAndPay. - priceAndFulfill / submitRootAndPriceAndFulfill take a parallel ProofRequest[][] for the priced path (slim alone can't verify client signatures). - R0BoundlessAssessorAdapter fits the new interface, sourcing every journal field (ids, callbacks, selectors, fulfillment-data digests) from the trusted slim payload; the seal carries only the inner STARK proof. - FulfillmentLibrary gains a calldata-friendly fulfillmentDataDigest overload.
Native Solidity implementation of IBoundlessAssessor that evaluates each
fill's predicate directly on-chain. No zkVM, no merkle tree, no STARK
proof. The market binds each SlimRequest to a signed lock before dispatch,
so the adapter trusts the supplied predicate.
Per-fill checks:
- Predicate satisfaction via PredicateLibrary.eval (DigestMatch, PrefixMatch,
ClaimDigestMatch).
- Claim-digest binding: ReceiptClaimLib.ok(imageId, sha256(journal)) must
reconstruct to fill.claimDigest. Without this, the prover could submit a
valid seal for a different computation entirely.
Per sub-batch:
- Prover signature: ECDSA over the EIP-712 SubBatchAuth(prover, requestDigests,
claimDigests) carried in assessorSeal. The adapter recovers the signer and
asserts it equals the supplied prover address. This is the on-chain
equivalent of the R0 STARK adapter's prover commitment in the journal.
Ships with a Foundry gas bench measuring per-fill cost across N in
{1, 5, 10, 50, 100} for both DigestMatch and ClaimDigestMatch predicates,
through both direct-call and BoundlessRouter-dispatch paths, plus
regression tests for the four revert paths (binding mismatch, predicate
failure, claim-digest mismatch, prover-signature mismatch).
…ced-path args
Address review feedback on the priced-fulfillment API:
- Rename `SubBatch` -> `FulfillmentBatch` across types, externals, router, and
market for clearer naming. `verifySubBatch` -> `verifyBatch`,
`MixedClassWithinSubBatch` -> `MixedClassWithinBatch`,
`EmptySubBatch` -> `EmptyBatch`, `SubBatchAuth` -> `FulfillmentBatchAuth`.
- Introduce `ProofRequestBatch { ProofRequest[] requests; bytes[] signatures }`
and update `priceAndFulfill` / `priceAndFulfillAndWithdraw` /
`submitRootAndPriceAndFulfill*` to take `ProofRequestBatch[]` instead of
parallel `ProofRequest[][] + bytes[][]` arrays. Symmetric to the
`FulfillmentBatch[]` argument so the priced-path API reads cleanly:
priceAndFulfill(
ProofRequestBatch[] requestBatches,
FulfillmentBatch[] fulfillmentBatches
)
Replace the single OnChainAssessorBench file with three focused files sharing a common base: - `BenchBase` (abstract) — router/adapter setup, prover wallet, fixture builders, and three harnesses (DirectHarness, RouterHarness, MultiCallRouterHarness). Registers three sibling assessor entries (OnChainAssessor, R0BoundlessAssessorAdapter via mock IRiscZeroVerifier, NullAssessor) under one assessor class to enable cross-adapter comparison through the router. - `AdapterBench` — measures the assessor adapters in isolation (direct call, no router). Includes DigestMatch and ClaimDigestMatch per-fill gas sweeps for OnChain vs R0, plus a journal-size sweep showing how Steel-style large journals affect per-fill cost. Order- generator commits a 16-byte journal (crates/order-generator/src/main.rs:356), used as the default fixture; 128 B and 512 B variants are also measured. - `RouterBench` — measures the router architecture cost from the market's perspective: "what does the market pay per batch to drive the verification engine, vs. the absolute minimum it could pay if it hardcoded a single assessor adapter and skipped routing entirely?" Includes a framing-cost row and a cold-vs-warm comparison. Harnesses no longer perform the binding check (that's market work; out of scope for adapter/router measurement). Callers pre-compute requestDigests once at fixture build time.
- Slim `_classOf` to `_classTagOf`: read only slot 0 of `ClassMetadata`
instead of copying all 5 slots into memory. The hot path only needs
`interfaceTag` (and `requiredAssessorClass`, also in slot 0).
- Defer the `tombstoned[]` check to the error path in `_entryOf` and
`_classTagOf` — a registered selector cannot simultaneously be
tombstoned (remove clears the value before tombstoning), so the
happy path skips one SLOAD per lookup.
- Add `signedSel == sealSel` and `signedSel == sealClassId` fast paths
to `_matchSignedSelector`: the common case now does zero SLOADs.
- Hoist the verifier/joint tag dispatch out of the per-fill loop and
reuse `firstEntry` for i=0, removing the redundant `_entryOf` call
for the first fill. Loop counter uses `unchecked { ++i; }`.
B.1 router framing overhead (NullVerifier + NullAssessor):
N=1 44,252 -> 24,203 (-45%)
N=10 95,400 -> 68,130 (-29%)
N=50 330,146 -> 270,783 (-18%)
- Cache the last-seen (sealSel, Entry) across the per-fill loop so a batch sharing one selector pays one entry lookup instead of N. This is the common case when a single verifier serves a whole batch. - Use `_isVerifierTag` / `_isJointTag` / `_isAssessorTag` helpers in the hot path instead of inlining `type(I).interfaceId` comparisons. Zero runtime cost (interface ids are compile-time constants), reads cleaner. - Tighten `_entryOf`'s error diagnostics so a malformed seal whose first 4 bytes resolve to a class id or the chain-default sentinel reverts with `EntryIsClass` / `ZeroSelectorReserved` instead of the generic `EntryUnknown`. Cold path only — no hot-path SLOADs added. B.1 router framing overhead vs prior commit (NullVerifier + NullAssessor): N=10 68,130 -> 62,884 (-7.7%) N=50 270,783 -> 240,760 (-11.1%) B.2 cold/warm vs prior commit: N=10 cold 90,740 -> 85,494 (-5.8%); warm 71,680 -> 66,434 (-7.3%) N=50 cold 368,974 -> 338,951 (-8.1%); warm 342,813 -> 312,790 (-8.8%) N=100 cold 740,709 -> 676,445 (-8.7%); warm 698,781 -> 634,517 (-9.2%) Per-fill warm cost drops from ~6,700 to ~6,100 gas.
Add `_forwardCalldataAsStaticCall(impl, gasLimit, selector)` and use it for the per-batch assessor dispatch. The helper writes only the 4-byte destination selector into scratch memory, then `calldatacopy`s the entry-point's calldata tail into the outgoing call -- never copying or re-encoding the args Solidity would otherwise traverse. Reverts bubble verbatim via `returndatacopy + revert`. This works inside an `internal` helper because in the EVM calldata belongs to the current message-call frame, not to a Solidity function; internal calls are JUMPs within the same frame, so `calldatasize()` still references the outer (entry-point) calldata -- exactly the bytes we want to forward. ABI-stability invariant: `verifyBatch` and `IBoundlessAssessor.verifyAssessor` must keep byte-identical calldata tails. The OnChainAssessor and R0BoundlessAssessorAdapter end-to-end tests catch any drift because those adapters fully decode the forwarded args. via_ir inlines the helper at the single call site, so the bench numbers are identical to the equivalent inline-assembly form: B.1 N=50 router-framing overhead 270,783 -> 157,506 (-30.7%) B.2 N=50 cold 338,951 -> 255,697 (-24.6%); warm 312,790 -> 229,536 (-26.6%) Cumulative vs pre-optimization baseline at N=50: framing 330,146 -> 157,506 (-52.3%); per-fill warm ~7,500 -> ~4,700 gas.
…_000 The router and its adapters are the hot path on every market settlement and are deployed once. Bumping their optimizer_runs from the size-tuned default of 100 to 1_000_000 trades a small bytecode-size increase for faster runtime. Rest of the project unchanged. B.1 router framing overhead vs prior commit: N=1 21,807 -> 21,309 (-2.3%) N=10 45,630 -> 43,377 (-4.9%) N=50 157,506 -> 147,453 (-6.4%) B.2 cold/warm vs prior commit: N=10 cold 68,240 -> 65,075 (-4.6%); warm 49,180 -> 46,027 (-6.4%) N=50 cold 255,697 -> 241,372 (-5.6%); warm 229,536 -> 215,223 (-6.2%) N=100 cold 510,691 -> 482,416 (-5.5%); warm 468,763 -> 440,500 (-5.7%) Per-fill warm cost: ~4,700 -> ~4,400 gas. Cumulative vs pre-optimization baseline at N=50: B.1 overhead 330,146 -> 147,453 (-55.3%) B.2 warm 386,176 -> 215,223 (-44.3%)
Extract the always-passing IBoundlessVerifier / IBoundlessAssessor / IRiscZeroVerifier mocks from BenchBase into a shared contracts/test/mocks/RouterMocks.sol so both bench files and future unit-test files can reuse them. Move the OnChainAssessor sanity tests (predicate-failure revert, prover-signature mismatch, claim-digest mismatch, slim-payload reconstruction parity, single-fill happy path) out of AdapterBench.t.sol into a dedicated contracts/test/router/adapters/OnChainAssessor.t.sol. Remove the matching test_router_singleFill_passes from RouterBench.t.sol (belongs in router unit tests; the bench's own pass/fail is sufficient sanity here). Net effect: AdapterBench.t.sol and RouterBench.t.sol now contain only test_bench_* gas-measurement functions; correctness tests live in adapter- and router-specific unit files.
Bring the test file's setUp + harness back into compiling shape against the slim/router architecture. All 133 tests are wrapped in a single TODO(MIGRATE-MARKET) block comment so they can be ported incrementally without compile errors blocking the rest of the suite. Setup now: - Deploys a BoundlessRouter UUPS proxy. - Registers NullVerifier under a default verifier class and NullAssessor under its required-assessor class. Market state-machine tests don't exercise real cryptographic verification; the mocks short-circuit verifier + assessor dispatch so each test runs through the production fulfill path without paying for a STARK. - Deploys BoundlessMarket with the new (BoundlessRouter, collateralToken) constructor. Old AssessorReceipt-based helpers (createFills, createFillAndSubmitRoot, submitRoot, createDeprecatedFills) are also commented out — they relied on AssessorReceipt + set-builder root inclusion proofs that no longer exist. A minimal createFulfillmentBatch helper will be added before the first fulfill test is restored. Inherited helpers preserved verbatim: - Client / SmartContractClient / prover funding and snapshotting - expectMarketBalanceUnchanged, snapshot/expect collateral helpers - newBatch* (build locked-request batches; locks don't touch fulfill, so they port cleanly)
Restore 32 tests that don't depend on the (still TODO) fulfill helper: - 13 account / admin tests (deposit, depositTo, deposits, withdraw, withdrawals, collateral variants, stake withdraw, bytecode size, admin role setup). - 19 lock + submit-request tests covering both the EOA-signed lockRequest path and the lockRequestWithSignature path: happy paths, already-locked / already-fulfilled, bad client signature, prover signature variants (wrong-request, wrong-domain), insufficient funds, expired/lock-expired, and the two invalid-request shapes. Two prover-signature regression tests (testLockRequestWith- SignatureProverSignatureIncorrectRequest /IncorrectDomain) had hardcoded recovered-signer addresses that change with deploy nonce. Switched them to `expectPartialRevert` so they keep their regression purpose without breaking on contract-layout changes.
`BoundlessMarket._lockRequest` writes the domain-bound `requestHash` into `RequestLock.requestDigest`, but the post-refactor `_verifyBinding` was comparing it against the raw EIP-712 struct hash produced by `SlimRequestLibrary.reconstructRequestDigest`. Result: every locked fulfill reverted with `RequestIsNotLockedOrPriced` because the two sides hashed differently. Fix: keep the slim library producing the pure struct hash (its natural output), but have the market wrap each reconstruction with `_hashTypedDataV4` once per fill before comparing. This matches what both `lockRequest` and `priceRequest` write into storage. The priced path inside `_verifyBinding` no longer needs its own `_hashTypedDataV4` call either — both branches compare directly. To absorb the extra local variables the wrap introduces without tripping the Yul stack-too-deep limit, `fulfill` now delegates to two new internal helpers (`_bindAndCollectDigests` and `_settleBatch`). NatSpec on `SlimRequestLibrary.reconstructRequestDigest` and `_verifyBinding` updated to document the struct-hash vs. domain-bound contract. Also ports the first fulfill helper (`_testFulfillSameBlock`) and three tests that consume it (`testFulfillLockedRequest`, `testFulfillLockedRequestWithSig`, `testFulfillNeverLocked`) — these served as the regression check that caught the binding mismatch. New test-side helpers (`createFulfillmentBatch`, `_asArray` overloads for single-element batches) live alongside.
- ClaimDigestMatch fills now post FulfillmentDataType.None with empty fulfillmentData, matching the production shape where the journal doesn't need to be on-chain. Result: ClaimDigestMatch per-fill cost is now perfectly journal-independent (14,375 across 16/128/512 B). - Pad the journal tail with non-zero bytes (0x80..0xff) so any tx-intrinsic gas measurement (4 vs 16 gas per zero/non-zero byte) reflects real journals instead of getting the zero-byte discount. Inner-frame bench numbers don't move (precompile + memory costs are value-independent), but the fixture no longer misleads tx-level measurements. - Add N=2 row to test_bench_adapters and shrink the journalSize sweep to n=1 so the cost of journal-length itself isolates cleanly.
Unwrap and migrate the fulfill/slash families of BoundlessMarket.t.sol to the new FulfillmentBatch + ProofRequestBatch wire shape. Tests retain their original line positions and call into the existing _testFulfillSameBlock / _testFulfillRepeatIndex / _testFulfillAlreadyFulfilled helpers so the diff is bound to body changes, not restructuring. Brings the suite from 36 to 73 passing tests: ranges + large-journal, other-prover-fulfills, already-fulfilled, fully-expired, multiple-same-index, the wasLocked family (incl. stake-rollover, double-fulfill, locker-after-other), the neverLocked family, the dedicated testSlash* block, and the invalid-smart-contract-signature path.
Extends BoundlessMarket.t.sol from 73 to 96 passing tests:
* batch tests (testFulfillLockedRequests, …NoJournal, …AndWithdraw),
* smart-contract-signature tests (priceRequest + lockRequest +
priceAndFulfill variants),
* single-request priceAndFulfill,
* callback / claim-digest tests (11 ports).
Registers `R0BoundlessVerifierAdapter(setVerifier)` in the router under
setVerifier.SELECTOR() so callback fixtures produce one seal that
satisfies both the router's per-fill verifier dispatch and the
BoundlessMarketCallback re-verify. Modifies `createFills` /
`createFillsAndSubmitRoot` / `createFillAndSubmitRoot` in place to
return `FulfillmentBatch`, build set-builder seals over the slim
payload, and drop the assessor-journal aggregation (selector + callback
now live on `SlimRequest` per fill). The deprecated-assessor helper
variants are gone — replaced by router tombstones.
Tests that don't need callback verification keep using the cheap
NullVerifier path under VERIFIER_ENTRY_SEL.
Brings the suite from 96 to 103 passing tests: * `_testSubmitRootAndFulfillSameBlock` + AndWithdraw helpers (in place), * `testSubmitRootAndFulfillLockedRequest`, …WithSig, …AndWithdraw, * `testSubmitRootAndFulfillNeverLocked` + …ProverNoStake, * `testSubmitRootAndPriceAndFulfillLockedRequest`, * `testSubmitRootAndFulfill` (2-request batch). Splits the set-builder fixture back into `createFills` (pure compute, returns `(FulfillmentBatch, bytes32 root)`) and `createFillsAndSubmitRoot` (wrapper that also submits the root via setVerifier), mirroring the original layout. Helpers reuse `_asArray` overloads for singleton calls. Deprecated-assessor helpers and the matching test are restored wrapped (not deleted) so the migration retains a paper trail until equivalent coverage exists at the router-tombstone level.
Wires `R0BoundlessAssessorAdapter(setVerifier, ASSESSOR_IMAGE_ID)` into
the router under `ASSESSOR_R0_SEL = 0x24` (alongside `NullAssessor`)
and adds a broker+guest fixture (`createFillAndSubmitRootR0`,
`createFillsAndSubmitRootR0`) that produces what a broker would hand
the market: per-fill set-builder seals + a journal-bound STARK seal
over the assessor's `(root, callbacks, selectors, prover)` commitment.
The market then drives both adapters end-to-end.
Per-fill construction is now a shared `_buildFillsAndSlim` helper, used
by both `createFills` (NullAssessor path) and the R0 fixture, so the
loop lives in one place.
Ports three tests to the R0 path:
* testPriceAndFulfillWithSelector — happy-path with signed selector,
* testFulfillLockedRequestProverAddressNotMatchAssessorReceipt —
tampered `batch.prover` desyncs the journal digest from the
broker's seal → setVerifier rejects with `VerificationFailed`,
* testFulfillShuffleFills — swapped claimDigest/fulfillmentData
desyncs each per-fill seal from its claim → `VerifierFailed`.
`testFulfillShuffleIds` is dropped (with note): slim-id tampering now
breaks `_verifyBinding` first, already covered by
`testFulfillLockedRequestMultipleRequestsSameIndex`.
`testFulfillRequestWrongSelector` and the two
`*VerificationGasLimit*` tests stay wrapped — selector mismatch is
enforced by `BoundlessRouter._matchSignedSelector` and the per-fill
gas budget is the router entry's `gasLimit`; equivalent coverage
belongs in `BoundlessRouter.t.sol`.
103 → 106 passing tests.
Updates `testUnsafeUpgrade` to the new market constructor `(BoundlessRouter, collateralToken)` and single-arg `initialize(owner)`. The pre-upgrade `imageInfo()` invariant is gone with the slim-payload refactor — both market versions are constructed with the same router and collateral token, and the test just asserts the implementation address rotated. `testGrantAdminRole` is unchanged (admin role lives on the market directly).
Wires the 20 `testBench*` entrypoints to drive the production verification path — `R0BoundlessAssessorAdapter` + setVerifier inclusion proofs — by routing the 3 bench helpers (`benchFulfill`, `benchFulfillWithSelector`, `benchFulfillWithCallback`) through `createFillsAndSubmitRootR0` and the new `fulfill(FulfillmentBatch[])` ABI. Snapshot labels carry a `:v2` suffix so the new numbers coexist with the legacy entries in `BoundlessMarketBench.json` for side-by-side review. Side-by-side: the new fulfill path costs ~40% more per batch than the legacy market — overhead is dominated by `_bindAndCollectDigests` (the slim-payload security gain that the market now does on-chain instead of trusting the assessor STARK), the adapter's per-fill `AssessorCommitment` reconstruction (redundant until the next assessor-image rotation, see adapter NatSpec), larger calldata, and the two-hop market → router → adapter dispatch. The % delta shrinks with callbacks (+22% at N=32) because their fixed `verifyIntegrity` cost dilutes the routing overhead.
…verifyAssessor Aligns `IBoundlessRouter.verifyBatch` and `IBoundlessAssessor.verifyAssessor` on `(FulfillmentBatch calldata batch, bytes32[] calldata requestDigests)`, collapsing the previous five-arg form (slim requests, fills, digests, prover, assessor seal). The market's call site becomes `ROUTER.verifyBatch(batch, requestDigests)` instead of unpacking the batch field-by-field. Both interfaces stay shape-identical so the router can continue to forward its calldata tail verbatim to the assessor adapter via `_forwardCalldataAsStaticCall`. All three adapters (Null, OnChain, R0) and the bench harnesses (DirectHarness, RouterHarness, MultiCallRouterHarness) updated to match. A `_makeBatch(slim, fills, prover, seal)` helper in BenchBase keeps the call sites in AdapterBench / RouterBench / OnChainAssessor.t.sol short. Trade-off: the market->router hop now copies the FulfillmentBatch struct into a contiguous top-level calldata layout (previously each inner calldata field was passed by pointer), costing ~2-3% gas on fulfill. Acceptable for now -- the loop-in-both-router-and-assessor refactor that would recover this is a separate, larger change.
Widens `IBoundlessJointVerifierAssessor.verifyJoint` from `(requestDigest, claimDigest, prover, seal)` to `(SlimRequest request, Fulfillment fill, bytes32 requestDigest, address prover)`. The adapter receives the entire per-fill payload — the slim request (selector, callback, predicate, pre-computed digests) and the full fulfillment (claimDigest, fulfillmentDataType, fulfillmentData, seal) — and chooses what its mechanism actually needs. Rationale: keeps the joint seam future-proof for adapters that need more than just `(requestDigest, claimDigest)` — e.g. predicate-aware joint verifiers, attestation paths that bind to the callback, or journal-reconstructing implementations. Avoids interface churn each time a new joint mechanism wants visibility into another field. Router's call site collapses to `IBoundlessJointVerifierAssessor.verifyJoint(batch.requests[i], batch.fills[i], requestDigests[i], batch.prover)`, removing the separate `claimDigest` and `seal` extractions. No production adapter implements the joint interface yet; existing tests don't exercise this path, so behavior is unchanged for current fulfillments.
…ndlessRouter
* `forge fmt` over the touched contracts/tests (line wraps in multi-arg
function signatures, trailing commas, etc.).
* Extract `IBoundlessRouter` (one method: `verifyBatch`). The market
now depends on the abstract seam; `BoundlessRouter` declares
`implements IBoundlessRouter`. Admin/registration entry points
(`addClass`, `instantiate`, `removeClass`, `removeEntry`) stay on the
concrete contract -- admin tooling only.
* Regenerate Rust artifacts in `crates/boundless-market/src/contracts/`:
- Copy `SlimRequest.sol`, `FulfillmentBatch.sol`, `ProofRequestBatch.sol`,
`IBoundlessRouter.sol` into the artifact folder (build.rs).
- Delete stale `AssessorReceipt.sol` + `SubBatch.sol`.
- Refresh `Fulfillment.sol`, `IBoundlessMarket.sol`, `bytecode.rs` to
reflect the new ABI.
Known follow-up: the Rust SDK at `crates/boundless-market/src/contracts/
boundless_market.rs` still uses the old market ABI and has 13 compile
errors (`Fulfillment.id`, `fulfill(fills, receipt)`, arity drift on
`submitRootAndFulfill` / `priceAndFulfill`). Tracked as Phase D
(broker/SDK port); to be tackled in a focused follow-up PR.
Moves `OnChainAssessor` (the native Solidity assessor adapter) and its unit tests off this branch to keep the audit scope tight. They live on `jonas/onchain-assessor` (branched from this commit's parent) for a follow-up PR. * Delete `contracts/src/router/adapters/OnChainAssessor.sol`. * Delete `contracts/test/router/adapters/OnChainAssessor.t.sol`. * `BenchBase`: drop the `OnChainAssessor` adapter wiring, the `directOnChain` harness, the `ASSESSOR_ON_CHAIN_SEL` selector, and the `_buildOnChainSeal` ECDSA seal builder. * `AdapterBench`: collapse the side-by-side OnChain vs R0 comparison to R0-only. * `IBoundlessAssessor` / `BoundlessRouter`: trim NatSpec references to OnChainAssessor. R0 adapter remains as the v1 assessor. Router stays pluggable, so a re-introduction in a future PR only needs a fresh `instantiate` call against the same `ASSESSOR_CLASS_ID`.
This reverts commit e1c5da1 to re-introduce `OnChainAssessor` and its unit tests/benches on top of `jonas/router-decoupling`. The parent branch keeps the OnChain code out of its scope; this branch re-adds it for review in its own PR.
…to jonas/router-shanghai-port
…aded event (#2057) ## Summary Addresses the **Unused Code** audit finding (Info / Maintainability). Removes two dead constructs from the market contracts that could drift from the implementation and mislead integrators. ## Changes - **`contracts/src/BoundlessMarket.sol`** — remove `error MismatchedRequestId(uint256 expected, uint256 received)`. It was declared but never referenced anywhere in the codebase. - **`contracts/src/IBoundlessMarket.sol`** — remove the custom `event Upgraded(uint64 indexed version)`. It was never emitted by the implementation: `BoundlessMarket` is UUPS and upgrades emit OpenZeppelin's standard ERC1967 `Upgraded(address indexed implementation)` event. The custom event was misleading for integrators and monitoring tools relying on an event that never fires. - **`crates/boundless-market/src/contracts/artifacts/IBoundlessMarket.sol`** — regenerated (via `build.rs`) so the checked-in interface artifact stays in sync and passes the CI drift check. ## Out of scope (intentionally left unchanged) - `contracts/shanghai/src/` — a separate, currently-lagging pre-router copy compiled under the shanghai EVM profile; not part of the finding's scope. - `contracts/src/legacy/IBoundlessMarketLegacy.sol` — a frozen historical ABI snapshot kept for pre-router client compatibility; the `Upgraded` declaration is retained there for fidelity to the deployed legacy interface. ## Verification - `forge build` (compiles src + scripts + tests): passes. - `cargo build -p boundless-market`: passes; artifact regenerates with no additional drift. Both removals are pure dead-code deletions with no behavioral change.
…2054) ## Summary Remediation for audit finding **1257** (assessor half): *Class-based request selectors can accept attacker-registered fake verifiers.* A request commits to a verifier **class**, but never to a specific **assessor** entry — the assessor is inherited via the verifier class's `requiredAssessorClass`, and the **prover** selects which entry in that class to use (via `assessorSeal`). So the requestor has no say over the assessor. If governance ever set an assessor class to `permissionlessInstantiate = true`, anyone could register a no-op assessor (which only has to pass an ERC-165 check) and route fulfillment through it — bypassing the predicate-satisfaction and prover-binding checks the honest assessor enforces, and `verifyBatch` would still succeed. Because the requestor cannot opt out of the assessor, assessor classes must always be governance-curated. ## Fix `addClass` now reverts **`AssessorClassMustBeCurated`** when an assessor-tagged class sets `permissionlessInstantiate = true`: ```solidity } else { if (metadata.requiredAssessorClass != bytes4(0)) revert AssessorClassMustBeZero(); if (_isAssessorTag(tag) && metadata.permissionlessInstantiate) revert AssessorClassMustBeCurated(); } ``` Joint classes (`IBoundlessJointVerifierAssessor`) may remain permissionless — they are selected directly by the requestor's signed selector, so the opt-in protection applies to them. ## Verifier half — not a contract change The reported verifier case is **intended, opt-in** behavior and is handled by documentation, not code: a permissionless verifier class is reachable only when (1) governance has explicitly enabled it and (2) the requestor explicitly signs that permissionless class/selector. That is a deliberate, caveat-emptor choice; ERC-165 conformance is not evidence of honest verification. We will recommend in the docs that requestors sign a specific verifier **entry** selector (pinning the exact implementation) rather than a class. ## Changes - `BoundlessRouter.sol`: new `AssessorClassMustBeCurated` error + the `addClass` guard. - Test: `test_addClass_revertsForPermissionlessAssessorClass`; three existing instantiate/reserved-prefix tests migrated from a permissionless *assessor* class to a permissionless *verifier* class (they only used a permissionless class incidentally). - Regenerated `bytecode.rs` (the router's embedded bytecode changed). Builds on #1982 (router decoupling).
…t-running (#2052) ## Summary Remediation for an audit finding on `OnChainAssessor`. This description first frames the **general problem** (proof front-running) and the **solution space**, then describes the fix shipped here: a commit-reveal gate on the open fulfillment paths. ## The general problem: proof front-running A ZK proof / seal is a **bearer artifact**: it attests a *result*, not *who produced it*. But producing it is the expensive work, and in an open marketplace whoever submits it on-chain first collects the payment. So unless the protocol binds the proof to its producer, anyone who observes a submitted (or pending) proof can copy it, present it as their own, and steal the fulfillment reward — without doing any of the work. The gap is the missing, unforgeable link between *the proof* and *the prover who should be paid*. Any fix has to establish that binding **before the proof becomes a public, actionable artifact**. There are only three places to anchor it: - **Authorize** — pin the eligible prover in on-chain state before the proof exists (e.g. locking). - **Hide / delay** — keep the proof unusable-by-others until it's attributed to the prover (e.g. commit-reveal). - **Bind-in-proof** — make the proof itself commit to the prover (e.g. in-circuit, or a trusted attestation). ## Where it applies By definition the problem exists only on the **open fulfillment paths**, where the protocol has no prior on-chain binding of the prover: - **Never-locked** (priced-at-fulfill, e.g. `priceAndFulfill`) → **vulnerable**: full payment goes to whoever submits. - **After lock deadline** (was-locked, anyone may fulfill) → **vulnerable**: whoever submits is recorded as the fulfiller. The auction price is ~0 here, but the real prize is the **slashing reward**: the late fulfillment overwrites `lock.prover` with the submitter, so when `slash` is later called (after the full deadline) that submitter receives **50% of the defaulted locker's collateral**. So this path is *not* low-value — the stake reward is typically larger than a single proof's price, capped at 50% of the locker's collateral. **Not a problem:** - **Locked, before lock deadline** → already protected. The lock pre-authorizes the prover (`lock.prover == batch.prover` is enforced), so a copied proof can't redirect payment. This is the **authorize** family, already in place. (`R0BoundlessAssessorAdapter` is out of scope here: its prover binding lives in the STARK journal, so rebinding requires regenerating the proof — a *soft*, proving-time deterrent rather than a hard guarantee.) ## The solution space Four properties you'd want from a fix: - **Hard** — security does not depend on the attacker's resources or speed. (Opposite = *soft*: a margin an attacker erodes with faster proving or more hashpower — e.g. R0's ~14s, or PoW.) - **Trustless** — no off-chain party you have to trust. - **Open** — permissionless: anyone who actually produced a proof can fulfill and get paid, with no pre-registration (the never-locked "whoever finishes first wins" model survives). - **Cheap** — no extra transaction, no added block latency, no recurring compute. You can't have all four — each option gives up exactly one: | Option (family) | Hard | Trustless | Open | Cheap | |---|:---:|:---:|:---:|:---:| | **Commit-reveal** (hide / delay) | ✅ | ✅ | ✅ | ❌ +1 tx, +1 block | | **Lock / claim** (authorize) | ✅ | ✅ | ❌ *closes the set* |⚠️ +collateral to be grief-safe | | **Attestation** (bind-in-proof, trusted) | ✅ | ❌ *trust attestor* | ✅ | ✅ | | **R0 in-circuit / PoW** (bind / delay, soft) | ❌ *soft margin* | ✅ | ✅ | ✅ | **Commit-reveal is the only option that is hard *and* trustless *and* open** — it buys all three by spending latency. R0-in-circuit and PoW are the same idea at the cheap end (a soft time/compute margin an attacker can erode); attestation is hard + open but reintroduces a trusted signer; locking is hard + trustless but closes the open set (and needs collateral to be grief-safe). ## The fix in this PR: commit-reveal on the open paths Before settling any never-locked or was-locked fill, `fulfill` requires a prior `commitFulfillment(keccak256(abi.encode(fulfillmentBatches)))` recorded in a **strictly earlier block**: ``` block N commitFulfillment( keccak256(abi.encode(fulfillmentBatches)) ) ← only a hash; seal stays hidden block N+1 fulfill / priceAndFulfill / submitRoot… → commitment at N (< N+1) ✓ → settles a front-runner who first sees the seal at N+1 cannot have a block-N commitment for it, and cannot fabricate one in the same block (strictly-earlier rule) → copied proof rejected ``` - **Binding the seals** makes the commitment un-precomputable — you can't commit without already holding the proof. - The **strictly-earlier-block** rule (`COMMIT_REVEAL_MIN_BLOCKS = 1`) defeats same-block / sequencer-reordering attempts. One block suffices on a single sequencer; it's a named constant so it can be raised on reorg-prone chains. - Applied **uniformly** to all open-path fulfillment regardless of assessor — the market can't cheaply tell which assessor a fill used at settle time, and this also hardens R0's soft proving-time margin. The **locked-before-deadline path is unaffected and stays single-tx.** This is the hide/delay anchor with the cheapest possible clock (block height): hard, trustless, open — paying only ~1 block of latency on the two open paths. ## Changes - `BoundlessMarket.sol`: `commitFulfillment`, `_hasOpenPathFill`, `_consumeCommitment`, and the gate in `fulfill`. New storage slot `fulfillmentCommitBlock` appended after `imageUrl`. - `IBoundlessMarket.sol`: `commitFulfillment` + `MissingFulfillmentCommitment` (so the SDK/broker bindings expose it). Regenerated SDK artifact + `bytecode.rs`. - Storage-layout parity preserved: slots 0/1/2 (shared with the legacy impl via the delegatecall fallback) are unchanged; `verify-storage-layout.py` passes. - Contract size: **+394 B** (22,890 B runtime; 1,686 B under the EIP-170 limit). ## Follow-ups (not in this PR) - **Broker/SDK**: brokers must `commitFulfillment` one block ahead before fulfilling never-locked / was-locked orders. Separate broker-crate change. Builds on #1982 (router decoupling) and #2005 (`OnChainAssessor`).
…ation (#2055) ## Summary Fixes the audit discussion item *"Error encoding is not compatible with interface."* `RequestIsExpired` was declared `RequestIsExpired(RequestId, uint64 deadline)` (2 args) but both fulfillment sites encode it with only the request id: ```solidity paymentError = abi.encodeWithSelector(RequestIsExpired.selector, RequestId.unwrap(id)); ``` The declared 2-arg shape never matched the emitted 1-arg payload, so off-chain decoders — notably the Rust `boundless-market` client — could not decode the error. ## Fix Drop the unused `deadline` arg from the declaration so it matches what is actually emitted: ```solidity error RequestIsExpired(RequestId requestId); ``` Selector changes `0x873fd26b` → `0xfc54471a`. Every encoding site and test already passes only the id via `.selector`, so **on-chain behavior is unchanged** and the existing tests cover it. ## Why drop the arg rather than add it to the encoding - The deadline was **never actually emitted** at either site, so dropping it from the declaration removes nothing that was ever there — it just makes the declaration honest. - It is **not available on the priced / never-locked path**: that site only has the transient `FulfillmentContext` (`{valid, expired, price}`). Emitting the deadline there would require adding a field to that transient struct (+ `priceRequest` storing it) — disproportionate for an ABI-decodability fix, and it would leave the two sites asymmetric otherwise. - It is a diagnostic-only `paymentError` (surfaced via `PaymentRequirementsFailed`, not a revert); the `requestId` already identifies the request. (If the team prefers to keep the deadline for consistency with `RequestLockIsExpired` / `RequestIsNotExpired`, the alternative is to plumb it through `FulfillmentContext` — happy to switch.) ## Changes - `IBoundlessMarket.sol`: `RequestIsExpired` → 1-arg + updated selector/NatSpec. - Regenerated SDK artifact + `bytecode.rs`. - Fixed a stale test comment that referenced the wrong error. Full suite green (654/654); no settlement-logic change.
…fication-decoupling
The #2052 front-running guard requires open-path fulfillments (never-locked or past-lock-deadline) to be committed one block before the reveal. Implement the off-chain side, which #2052 (contract-only) left to callers: - SDK: BoundlessMarketService::fulfill() sends commitFulfillment and awaits its receipt before the reveal whenever the batch is on the open path, so the reveal lands >= 1 block later. Adds a commit_fulfillment wrapper and a unit test pinning the commitment preimage to the contract's keccak256(abi.encode(fulfillmentBatches)). - Pricing: charge the extra commitFulfillment tx (commit_fulfillment_gas_estimate, default 50k) in the lock_expired branch of order pricing. - deployment-test: commit-then-reveal before the open-path priceAndFulfill (the suite #2052 did not patch). Claude-Session: https://claude.ai/code/session_01Mx4DvQNNSUFzT43cABVthH
…coupling' into jonas/router-shanghai-port
test_request_status_lock_expired_then_slashed fulfilled a was-locked (lock-expired) request via the non-priced submitRootAndFulfill path. Post-#2052 the contract treats was-locked fills as open-path and requires a commitFulfillment one block ahead, so that call now reverts MissingFulfillmentCommitment. Route the late fulfillment through the open/priced path (with_unlocked_request), matching how the broker and CLI fulfill after lock expiry, so the SDK auto-commits ahead of the reveal. The secondary-fulfillment classification is timestamp-based, so the assertions are unchanged. Claude-Session: https://claude.ai/code/session_01Mx4DvQNNSUFzT43cABVthH
…arket variant The #2052 front-running guard added commitFulfillment to IBoundlessMarket and the mainline market, but not to the shanghai variant, breaking the shanghai profile build. Port the full guard (fulfillmentCommitBlock, COMMIT_REVEAL_MIN_BLOCKS, open-path check in fulfill, commitFulfillment/_hasOpenPathFill/_consumeCommitment) verbatim — the scheme uses only plain storage and block.number, so it works unchanged on Shanghai EVM, and the shared contract test suite asserts its behavior under this profile.
…coupling' into jonas/router-shanghai-port
jonastheis
marked this pull request as ready for review
July 7, 2026 01:10
jonastheis
requested review from
a team,
Wollac,
bobbobbio,
capossele,
nahoc,
unizarr,
willemolding and
zeroecco
as code owners
July 7, 2026 01:10
|
Bugbot is not enabled for this team, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
jonastheis
added a commit
that referenced
this pull request
Jul 8, 2026
…t remapping (#2062) ## Summary The last two nightly examples runs failed ([2026-07-08](https://github.com/boundless-xyz/boundless/actions/runs/28914585920), [2026-07-07](https://github.com/boundless-xyz/boundless/actions/runs/28838757981)). This fixes the code regression behind one of the two failures and makes the other fail loudly at its actual source. ## Changes - **Add `bytes-compat/` remapping to `examples/smart-contract-requestor/remappings.txt`.** #2044 added `import {Bytes} from "bytes-compat/Bytes.sol"` to `contracts/src/types/Predicate.sol` and the matching remapping to the root `foundry.toml`, but the example keeps its own `remappings.txt`, which was not updated. Since the example imports `boundless-market/types/Predicate.sol` (directly and via `ProofRequest.sol`), its forge build broke in the first nightly run on that commit. The new remapping points at the root OpenZeppelin checkout, matching how the rest of `contracts/src` resolves OZ imports in this build. Verified locally with `forge build` and `forge test` in the example. - **Use `curl --fail` for the blake3 groth16 artifact download in `nightly-examples.yml`.** The blake3-groth16 job failed both nights because `blake3_groth16_artifacts.tar.xz` is gone from the staging signal-artifacts bucket — the URL now serves a Cloudflare 404 HTML page, which curl happily saved and tar then rejected with a confusing "File format not recognized". With `--fail`, the download step itself fails on HTTP errors. Note: this PR does **not** fix the blake3-groth16 job — the artifact needs to be re-uploaded to the bucket (likely deleted by the R2 retention policy; `docker-services.yml` bakes in the same URL and is affected too). That is an ops action tracked separately.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the router-decoupling architecture (BoundlessRouter + batched
FulfillmentBatchAPI + legacy-ABI fallback) to the Shanghai-EVM variant (FOUNDRY_PROFILE=shanghai, deployed to Taiko mainnet/staging, chain 167000), so Taiko can run the router market — deployed as an in-place upgrade of the live market0xb3f5…b28b, preserving the pre-router ABI for existing clients via thedelegatecallfallback.Instead of maintaining
contracts/shanghai/as a full mirror ofcontracts/src(which had silently drifted), the shanghai profile now compiles the samecontracts/srctree, swapping only the few EVM-version-divergent files via per-profile remappings. Net result is ~14k fewer lines.Why a shared tree
Solidity has no conditional compilation, and a
tstorein deployed bytecode reverts on a pre-Cancun chain even if never executed — so the Shanghai variant must be a real separate compile, not a runtime branch. Butsolcauto-downgrades everything it generates (incl.mcopy) underevm_version=shanghai; only hand-written assembly opcodes need shims, and in the whole router tree that is a single one (tstore/tloadinFulfillmentContext). The mirror was therefore almost entirely redundant.What changes
Shared tree + per-profile remappings.
[profile.shanghai]pointssrc/script/testat the sharedcontracts/{src,scripts,test}and swaps three prefixes:Market variant. The Cancun market is left byte-identical. A
contracts/shanghai/variants/BoundlessMarket.solcopy carries an explicitFulfillmentContext.clear()— a no-op on transient storage,sstore(0)on persistent storage (required because persistent storage does not auto-clear and must enforce the same single-transaction price-then-fulfill semantics). It is the one contract whose body must differ.Frozen Taiko legacy.
contracts/shanghai/legacy/freezes the pre-router market deployed on Taiko (impl0x6c2d…523b), compiled under shanghai. It byte-matches the deployed bytecode (all constructor immutables verified) and shares the new market's storage layout, so the new market candelegatecallit for the legacy ABI across the in-place upgrade.verify-legacy-bytecode.py/verify-storage-layout.pyare parameterized (BOUNDLESS_OUT_DIR/BOUNDLESS_LEGACY_SNAPSHOT_DIR) to cover both the Base/Cancun and Taiko/Shanghai legacies.Mirror removed. The obsolete
contracts/shanghai/{src,scripts,test}mirror is deleted; the profile now compiles the shared tree with only thevariants/,compat/, andlegacy/deltas.Out of scope. PoVW (a separate rewards suite whose zkc
Supplylibrary uses transient storage) is skipped under shanghai and would need its own port.Validation
Default-profile bytecode is unchanged (market + adapters byte-identical); the full contract suite passes under
FOUNDRY_PROFILE=shanghai; the Taiko legacy bytecode-parity and new-vs-legacy storage-layout checks pass.How to review
The headline diff (+1257 / −14093, 101 files) is misleading: ~16.7k of the deletions are the old
contracts/shanghai/{src,scripts,test}mirror, and the two largest additions are copies — the market variant and the frozen legacy — not new logic. The genuinely hand-written surface is 7 files / ~76 lines.Review commit-by-commit (the three commits are self-contained):
foundry.toml, the variant, Predicate's 1-line import). Start here; the design lives in this commit.Read closely (the real surface):
foundry.toml[profile.shanghai]+[profile.default].remappings(the three swapped prefixes, theskiplist, the isolatedsnapshotsdir);contracts/src/types/Predicate.sol(1 line);contracts/scripts/{Deploy,Manage}.s.sol(2 lines each); the twoverify-*.pyparameterizations;contracts/test/BoundlessMarket.t.sol(1 line).Collapse the two copies to their true diffs:
Reproduce the invariants by running (these are the load-bearing guarantees):
These same parity checks now run in CI (the
legacy-bytecode-parityjob builds the shanghai profile and runs both with the env vars above) and viajust check, so the storage-layout invariant that protects the in-place upgrade can't silently drift.Worth scrutinizing (not mechanical):
clear()call is the one piece of new logic. Sanity-check the rationale: persistent storage must clear-on-consume, or a request priced in tx A could be fulfilled in tx B at a stale price/expiry. The market reads via the non-clearingload(binding check + settle) and clears once in_fulfillAndPay.Supplylib) — confirm that's acceptable, or it's a follow-up.UpgradeBoundlessMarketauto-resolvesLEGACY_IMPLfrom the proxy's current impl on first upgrade (so it picks up the deployed0x6c2d…523b).upgradeToAndCall+ fallback is the one path not yet exercised by running (a Taiko-fork dry-run is the remaining optional validation).Related
Builds on the router-decoupling work in
jonas/adopt-router-in-broker(this PR targets that branch).