feat: announcement-anchored Fusion auction, volume-ladder fill pricing, and signature-free maker flows - #430
Conversation
Store the timestamp and block number of the first registration of each order, keyed by order hash. The announcement is written once and never moved, so an auction anchored to it cannot be shifted by a later registration, while a repeated registration of an unchanged order still succeeds and re-emits its event as SafeOrderBuilder relies on. Redeploying the registrator needs an explicit create3 salt, since the default one resolves to the address the announcement-less deployment occupies. Co-authored-by: Sergej Kunz <info@deacix.de>
A Dutch auction that can start from the moment an order was announced on-chain rather than from a timestamp baked in at build time, so a maker that signs slowly — a multisig collecting signatures — gets the price curve a prompt maker gets instead of decaying to the auction floor before it can submit. Three features, each opt-in per order through a flags byte: - an anchored start at max(announcement + delay, built start), which also re-anchors resolver exclusivity so a late announcement does not dissolve it; - fill-scaled pricing, where a taker sweeping what is left pays the auction price and a smaller fill gets only its share of the discount, at a strength the order chooses; - a deadline relative to the auction's end, which bounds the floor-price tail that an absolute order expiry cannot express once the start is anchored. The contract is a standalone amount getter and post-interaction referenced by address from the order extension, so it composes with any deployed settlement version rather than requiring one to inherit from it. Co-authored-by: Sergej Kunz <info@deacix.de>
Also pin the gas price in the gas-bump test, as the fee history the provider infers one from predates the base fee the test forces, which fails under solidity-coverage. Co-authored-by: Sergej Kunz <info@deacix.de>
The auction has to reproduce the pricing of the SimpleSettlement contract in limit-order-settlement when none of its flags are set, and has to compose behind it when they are — neither could be checked here, because that contract compiles at an older Solidity version and cannot be imported. Add a mock mirroring its auction and whitelist, and test against it: identical prices across a multi-point curve at ten points in time in both getter directions, and a chained fill where the settlement's neutralized curve contributes nothing while its fees and the chained exclusivity still apply. Also reject an unset registrator when deploying the auction, since it is immutable and every anchored fill reads it. Co-authored-by: Sergej Kunz <info@deacix.de>
The fill-scaling premium is superadditive under splitting — each later part faces a smaller remainder, so its share of it is priced no better — which is what makes the penalty meaningful at all. Pin it with fills at the same auction state, and tighten the packing comment on the registrator slot. Co-authored-by: Sergej Kunz <info@deacix.de>
The script calls getNamedAccounts without destructuring it from the hardhat-deploy arguments, so it would throw the moment it ran, and the undefined reference has kept the CI lint job red since it merged. Co-authored-by: Sergej Kunz <info@deacix.de>
Three mainnet-fork tests, opt-in through MAINNET_RPC_URL (or FORK_RPC_URL) and skipped otherwise, with only the two new contracts deployed on top of live state: - an announced order priced and filled through the live Aggregation Router V6, signed against its own EIP-712 domain, which differs from the locally deployed protocol's; - composition behind the live Fusion settlement at 0x2Ad5004c60e16E54d5007C80CE329Adde5B51Ef5, including its surplus-fee fields and priority-fee validation, with its curve neutralized so the chained anchored auction owns the price and the chained exclusivity still holds; - the multisig flow on the live Safe 1.3.0 contracts: a Safe marks the order digest through SignMessageLib, the order is announced with an empty ERC-1271 signature, and the auction prices from that announcement. The pricing mirror shared by the unit and fork suites moves to test/helpers/fusionAuction.js. Co-authored-by: Sergej Kunz <info@deacix.de>
Boundary instants of the auction and of the post-auction window, a zero-duration step curve, a zero-delay point re-anchoring the curve, a gas bump that swallows the whole rate bump, gas estimates without a gas price, every optional field parsed at once in both fill directions, third-party announcements, the maker expiry outranking the announcement, a rising curve releasing no discount to withhold, the gas bump coming off the scaled bump, oversized taking-amount fills capped and repriced, orders that forbid partial fills, dust-sized rounding, the whitelist max() semantics, staggered whitelisted resolvers, and an empty anchored whitelist. Branch coverage of both contracts reaches 100%. Co-authored-by: Sergej Kunz <info@deacix.de>
Also record in the exclusivity docs that whitelisted addresses compare by their lowest ten bytes, the same size-versus-grinding trade-off the settlement contracts already make. Co-authored-by: Sergej Kunz <info@deacix.de>
Fusion quotes are built from a depth matrix carrying different rates for 1/10, 2/10 and so on of the swap amount, and that relationship is not linear — so the linear rule alone cannot encode what the quoter knows. A new fill-curve flag carries the matrix on-chain: a piecewise premium over the share of the remainder a fill takes, built exactly like the auction's own time curve, one point per matrix row, hit exactly at its share and interpolated linearly in between so there are no cliffs to game. The premium rides on top of the time curve, a full sweep still pays the plain curve price, and a single-row curve reproduces the linear rule. The two encodings are mutually exclusive per order. The conservative making-amount estimate anchors on the curve's true peak, so it stays no better than exact even for a matrix that peaks at an interior row. Branch coverage stays at 100%. Co-authored-by: Sergej Kunz <info@deacix.de>
Both fill-pricing modes move from a per-fill share of the remainder to the cumulative end of the fill on the order's original amount. The quote's matrix is indexed by cumulative volume — the 2/10 row is the rate at two tenths of the amount — so the rows are consumed in order: the first tenth prices at the 1/10 row, the next tenth at the 2/10 row, and successive fills walk down the ladder instead of re-measuring each fill against a shrinking remainder. First fills price exactly as before, whoever completes the order still pays the plain auction price, and splitting still always costs more than sweeping — all pinned. For the linear rule the change is one denominator. The path-independent alternative, integrating the ladder over each fill's span the way RangeAmountCalculator and gradual Dutch auctions do, was considered and rejected: path independence removes the full-fill incentive this feature exists to create. Co-authored-by: Sergej Kunz <info@deacix.de>
Six additions: an announce-and-fill in the same block, the atomic pattern a resolver actually uses, proven by receipts sharing a block number; the matrix's implied final segment between its last row and completion; the gas bump coming off a matrix premium; a matrix order chained behind the fee taker with fees; a matrix at the count byte's ceiling of 255 rows; and superadditivity as a property — a seeded sweep of random partitions of the order, priced through the contract's own views in both encodings, none of which may undercut a single sweep. Co-authored-by: Sergej Kunz <info@deacix.de>
Three adversarial pins: a taker presenting forged or stripped auction bytes never reaches the pricing, because the salt commits to the extension hash; a spent order yields nothing to any crafted follow-up fill; and a seeded sweep over random fill sizes, remainders and both encodings proves the maker's floor — the plain proportional rate — holds in both getter directions, with direction round-trips manufacturing nothing beyond two wei of ceiling-division dust. Co-authored-by: Sergej Kunz <info@deacix.de>
Registration turns signature-free and maker-only: registerOrder(order, extension) authenticates by msg.sender alone, writes the anchor once and emits OrderAnnounced exactly once; permissionless broadcast goes with the signature filter it depended on. SafeOrderBuilder drops its now-redundant empty signature, and the new SignAndAnnounce helper gives Safes the same one-execution mark-and-announce flow without the oracle logic. The fill curve is enforced non-increasing lazily inside the premium walk (NonMonotonicFillCurve), which makes the initial premium the worst case and deletes the max-premium scan. The exclusivity blob gains an optional announcement deadline (flag 0x04 + 3 bytes) that reverts AuctionExpired past announcedAt + delay and fails closed without the anchored bit — defense in depth against getter-side mis-assembly. Co-authored-by: Sergej Kunz <info@deacix.de>
A shared contract is the order's maker for all of its users: createOrder records the caller as the order's owner (the ERC-1271 presign read by empty-signature fills), validates maker/receiver/traits/pre-interaction target, refuses ever-registered hashes (including cancel-then-recreate), and announces through the registrator — anchoring the auction clock in the same transaction. preInteraction pulls exactly the filled amount from the owner just in time, so custody time is zero and proceeds land on the owner directly. approveRouter grants the protocol the contract's own allowance per token, permissionlessly. cancelOrder deletes the presign and invalidates at the protocol, both load-bearing since the core validates ERC-1271 only on the first fill. Covered by a lifecycle/validation/cancellation/isolation suite, an anchored-matrix composition test, and a fourth mainnet-fork test running the whole flow through the live router. Co-authored-by: Sergej Kunz <info@deacix.de>
…gistered Co-authored-by: Sergej Kunz <info@deacix.de>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-authored-by: Sergej Kunz <info@deacix.de>
FeeTaker pays the maker's share to order.maker unless its fee data names a custom receiver, and it only takes fees when the order's receiver is the fee taker itself. For a DelegatedMaker order that maker is the shared contract, which cannot withdraw, so the receiver rule refuses such an order rather than let a fill strand proceeds there permanently. Documents the resulting limitation — these orders price through the auction and take no fees — and adds the production-shape test with the pull and resolver exclusivity on the same order. Co-authored-by: Sergej Kunz <info@deacix.de>
The settlement repo lives at 1inch/fusion-protocol (still publishing the limit-order-settlement npm package). Verified against its master: solc 0.8.23 / zksolc 1.4.1, SimpleSettlementMock mirrors _getRateBump, _getAuctionBump and the whitelist walk line for line, the surplus fee sits in SimpleSettlement._getFeeAmounts, Settlement adds only the priority-fee gate before forwarding, and deployments/mainnet records the exact address the fork suite composes behind. Co-authored-by: Sergej Kunz <info@deacix.de>
A non-creator receiver is now accepted in exactly one shape: the order's post-interaction targets that receiver (FeeTaker's own distribution condition) and its fee data carries the custom-receiver flag naming the creator, so a conforming fee contract forwards the net to the wallet that funded the order. Anything else reverts InvalidFeeReceiver — the zero receiver, a missing hook trait, short fee bytes, a foreign target or a foreign custom receiver each strand or divert proceeds. createOrderWithPermit folds the EIP-2612 allowance into the create transaction with the permit owner pinned to the caller and failures swallowed the way the protocol treats maker permits: a front-run permit already granted the allowance, and a wrong one leaves the order anchored but unfillable until an approval revives it. Covered by the full production-shape unit test (fee taker + anchored auction getters + exclusivity through the fee taker's tail + the pull), five fee-shape reverts with a positive control, both permit paths, and a fifth mainnet-fork test through the live fee-collecting Settlement that pins the surplus-fee-on-premium number the rollout note warns about. Co-authored-by: Sergej Kunz <info@deacix.de>
The linear rule was the first iteration of fill-size pricing and the matrix is what the quoter produces, so the fill curve is now the only encoding: the fill-scaled flag, its strength byte, _scaleByFill and the two errors that existed to police two encodings are gone, the AuctionState struct flattens into plain (auctionBump, gasBump) returns, and FILL_CURVE takes the freed 0x02 slot. A linear schedule is a curve with no interior points — the premium falls straight from its initial value to zero at completion — which the renamed pointless-curve test pins. The three flag-independent behaviors the linear suite carried (oversized-fill cap, partial-fills-forbidden inertness, dust rounding) migrate to curve configurations; the rest had matrix twins. Accepted semantic difference, deliberate: the linear premium scaled the unreleased time-curve discount and decayed with the auction, the curve premium is flat over time. Co-authored-by: Sergej Kunz <info@deacix.de>
The getter-side post-auction deadline duplicated the post-interaction announcement deadline with a different reference point and a weaker guarantee — amount getters are skippable by mis-assembly, a post-interaction is not — so the getter-side flag, its window field and its branch are gone and the announcement deadline is the fill-by mechanism, taking the freed 0x02 slot in the exclusivity byte. An order that wants the deadline without resolver exclusivity carries the anchored blob with an empty whitelist, which the every-optional-field test now demonstrates. Two consequences documented in NatSpec: quoting through the getters no longer reverts past the deadline (the fill does, which resolver simulations catch), and the delay is conceptually startDelay + duration + the tolerated tail window. Co-authored-by: Sergej Kunz <info@deacix.de>
Nothing on-chain reads the announcement block number, and every log already carries the block it was emitted in, so announcedAtBlock, the timestamp/block packing and the event's block-number field are gone. The announcement is a public timestamp mapping whose generated getter is the interface view, and OrderAnnounced keeps only the indexed hash and the timestamp — the one field that saves indexers a block lookup. Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
SignAndAnnounce duplicated what two already-audited Safe components do in one co-signed execution: MultiSend delegatecalled by the Safe runs an inner delegatecall to SignMessageLib (the digest marking that authorizes later empty-signature fills) and an inner call to registerOrder (the anchor, since the registrator sees the Safe as sender). Same one-ceremony UX, one contract fewer, and no new Safe delegatecall target to security review — the only delegatecall surface is SignMessageLib, which the dApp already uses. The helper, its deploy script, salt and docs page are gone; the local and fork Safe tests drive the batch instead, the fork one against the live 1.3.0 MultiSend and SignMessageLib deployments. Co-authored-by: Sergej Kunz <info@deacix.de>
approveRouter is gone: preInteraction tops the protocol's allowance up to the maximum whenever it runs short, so a token's first fill provisions it and no separate setup step exists to forget. This also fixes a latent defect — the one-shot max approval would silently deplete for tokens that decrement even infinite allowances, eventually bricking fills until someone re-called approveRouter; the lazy check re-provisions automatically. Costs one warm allowance read per fill. Co-authored-by: Sergej Kunz <info@deacix.de>
OrderRegistered merged into OrderAnnounced, which now carries the order and extension payload after the indexed hash and the timestamp: one event on the first registration is both the anchor signal and the broadcast resolvers read presigned orders from. Repeat registrations become silent no-op successes — SafeOrderBuilder re-runs still work, and re-emission never carried information the permanent first log did not. Co-authored-by: Sergej Kunz <info@deacix.de>
The 3-byte announcement-relative start delay had no named use case: an anchored auction now starts at max(announcedAt, builtStartTime), so the flag carries no field and the SDK has nothing to compute. An absolute built start can still push the auction past its announcement, which the build-time-start test keeps pinning. Two tests that filled exactly at the announcement instant now fill one second in and price through the mirror; the exact top-of-curve fill stays pinned by the same-block announce-and-fill test. Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
The hard-cut round's docs-restore step ran git checkout over unmatched docs paths and silently reverted the page's deletion before it was staged, so the orphan survived the helper it documented. Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
… flow Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
feat: DelegatedMaker — one-transaction, no-signature, no-escrow maker flow
test: make yarn test pass under --parallel
Co-authored-by: Sergej Kunz <info@deacix.de>
…int math The registrator and DelegatedMaker compute the protocol's order hash locally, rebuilding the domain from ERC-5267 data captured at construction, instead of a cross-contract hashOrder call that carries the whole order. registerOrder returns (orderHash, firstRegistration), collapsing DelegatedMaker.createOrder from four external calls to one. The auction's fixed-point scaling takes AmountCalculatorLib's 128-bit-guarded fast path and falls back to full-width mulDiv. Measured on real fills: createOrder 87,871 -> 82,552; announcement 35,211 -> 31,132 (fork, lean order) and 62,392 -> 59,139 (local, fee order); every fill about 190-280 cheaper. The return values cost +92 on a plain announcement. Co-authored-by: Sergej Kunz <info@deacix.de>
… values Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
Team review caught the cumulative-on-original indexing under-paying makers on late partial fills: with 80% filled, a fill of 10% of the original order priced at the nearly-free 9/10 row despite being half of everything left. Each fill now reads the curve at its own share of the remaining amount - the remainder is a fresh ladder - which is never a smaller share than the cumulative end was, so on a non-increasing curve the maker is never paid less. A first fill's premium still equals the quote row for its size, and the completing fill still pays no premium at all. The original making amount drops out of pricing entirely. Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
SteMak
left a comment
There was a problem hiding this comment.
Interesting MVP, however, not the right place to implement it, Fusion functionality is located in fusion-protocol repo
Introduces some breaking changes violating the original architecture intents, particularly, order registrator completely drifts its announcement functionality as signature is not announced anymore
Delegated maker is inefficient by design due to additional token transfer and it is not clear which problem it solves
Some other minor inefficiencies flagged in comments
| import { AmountGetterBase } from "./AmountGetterBase.sol"; | ||
|
|
||
| /** | ||
| * @title FusionAnchoredAuction |
There was a problem hiding this comment.
Fusion extends Limit Order Protocol. Limit Order Protocol is not a correct place to implement Fusion functionality. Architecturally this creates circular dependencies, lowers readability, increases execution flow complexities. While abstraction layers could be neglected during MVP building in production code it is important to have separate objects keeping isolated responsibility
There was a problem hiding this comment.
Agreed, and restructured accordingly: the auction mechanics now live in fusion-protocol as 1inch/fusion-protocol#223, built the way this comment implies — a move-only commit extracts the settlement's own auction math into an inheritable base, and the anchored extension inherits it. The only limit-order-protocol change left is the registrator's write-once announcedAt (#435), which the extension reads through a locally declared one-view interface.
One nuance that is a requirement rather than a preference: it ships as a separately deployed, per-order opt-in extension because redeploying the deployed settlement is out of scope; folding into a future settlement version stays available whenever one ships anyway. This PR stays open only as the reference/discussion umbrella and closes unmerged.
| * | ||
| * The recorded announcement is what {FusionAnchoredAuction} anchors an auction to, so it is written | ||
| * once and never moved: a repeated registration of the same order is a silent success that changes | ||
| * nothing. The announcement is keyed by order hash alone, which already commits to the maker. |
There was a problem hiding this comment.
Imperative comment instead of declarative one
There was a problem hiding this comment.
Fixed in #435 — the registrator change was re-cut from master with declarative one-line NatSpec.
|
|
||
| /// @dev The protocol's order hash, computed locally: the domain is rebuilt from immutables captured | ||
| /// at construction (fork-safe via block.chainid), so no cross-contract call carries the order. | ||
| function _hashOrder(IOrderMixin.Order calldata order) private view returns (bytes32) { |
There was a problem hiding this comment.
Porting piece of code from one smart contract to another is architecturally incorrect and increases complexity for the future chagnes
Does the change justified by the Gas costs reduction? How much Gas does it save?
There was a problem hiding this comment.
Reverted in #435 — it calls hashOrder() again. Measured answer to the gas question: the optimization bundle this was part of (local hashing plus collapsing createOrder to a single external call) took the create flow down 6–12%; the hashOrder round-trip alone is roughly 3k per call (cold account access plus re-copying the order as calldata). At announcement frequency that does not justify duplicated domain logic, so it stays out; if wanted later it is a self-contained follow-up measurable with scripts/gas-comparison.js on this branch.
| * @notice See {IOrderRegistrator-registerOrder}. | ||
| */ | ||
| function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension, bytes calldata signature) external { | ||
| function registerOrder(IOrderMixin.Order calldata order, bytes calldata extension) external returns (bytes32 orderHash, bool firstRegistration) { |
There was a problem hiding this comment.
Does not announce signature anymore which makes the order registry to play no announcement role as event info is not enough to execute the order
Completely drifts the contract functionality and purpose breaking the initial intent
There was a problem hiding this comment.
Agreed and restored in #435: registerOrder(order, extension, signature) is permissionless again, validates ECDSA/ERC-1271 exactly as on master, and emits the signature-carrying OrderRegistered on every call — the event is sufficient-to-fill again. The only delta vs master is announcedAt[orderHash] = block.timestamp on the first registration, write-once so a re-announcement can never move an anchored auction.
|
|
||
| // Validate signature | ||
| if(!ECDSA.recoverOrIsValidSignature(order.maker.get(), _LIMIT_ORDER_PROTOCOL.hashOrder(order), signature)) revert IOrderMixin.BadSignature(); | ||
| if (msg.sender != order.maker.get()) revert AccessDenied(); |
There was a problem hiding this comment.
Breaks EIP-1271 compatibility
Makes multisig sign flow more complex as requires not only order sign but transaction as well
There was a problem hiding this comment.
Restored with the signature path in #435. A Safe signs once — it marks the digest via SignMessageLib — and the announcement can then be submitted by anyone with empty signature bytes, validated through ERC-1271. Pinned by tests there: third-party relay of a signed announcement, one MultiSend batch (signMessage + registerOrder with 0x), and the negative gate — an empty signature without a presign reverts BadSignature.
|
|
||
| IERC20 makerAsset = IERC20(order.makerAsset.get()); | ||
| if (makerAsset.allowance(address(this), address(_LIMIT_ORDER_PROTOCOL)) < makingAmount) { | ||
| makerAsset.forceApprove(address(_LIMIT_ORDER_PROTOCOL), type(uint256).max); |
There was a problem hiding this comment.
I'd avoid branching here, justmakerAsset.forceApprove(address(_LIMIT_ORDER_PROTOCOL), makingAmount);
There was a problem hiding this comment.
Declining this one with numbers. The protocol spends the allowance down to zero during each fill, so the unconditional shape pays a fresh zero-to-nonzero SSTORE (20k) plus the approve call on every fill; the branch pays one allowance SLOAD in steady state, because the infinite allowance is never decremented. Measured on this branch's harness, the lazy check costs 2.5–2.9k per fill all-in; the unconditional variant would add roughly 17–20k to the measured 100,715-gas steady-state fill.
There was a problem hiding this comment.
Did you counted for gas refund when the allowance is used fully
| if (makerAsset.allowance(address(this), address(_LIMIT_ORDER_PROTOCOL)) < makingAmount) { | ||
| makerAsset.forceApprove(address(_LIMIT_ORDER_PROTOCOL), type(uint256).max); | ||
| } | ||
| makerAsset.safeTransferFrom(owner, address(this), makingAmount); |
There was a problem hiding this comment.
Inefficient double-transfer design: maker -> delegated maker -> taker
Can we do smth with this..?
There was a problem hiding this comment.
Inherent to the shared-maker shape: the protocol itself executes transferFrom(maker → taker) and the maker is the shared contract, so the just-in-time pull must land there first. The alternative that removes the hop — a minimal proxy per user, each proxy its own maker — trades one warm ERC-20 transfer per fill (~7–12k depending on the token) for a clone deployment per user plus a per-user address resolvers must discover. Considered and rejected for now; revisitable if per-user fill counts flip the arithmetic.
| postInteractionData[20] & _CUSTOM_RECEIVER_FLAG == 0 || | ||
| address(bytes20(postInteractionData[_CUSTOM_RECEIVER_OFFSET:_CUSTOM_RECEIVER_OFFSET + 20])) != msg.sender | ||
| ) revert InvalidFeeReceiver(); | ||
| } |
There was a problem hiding this comment.
Costly on-chain verification that maker's request is well formed, cannot it be delegated to off-chain?
There was a problem hiding this comment.
It is cheap — calldata slicing and comparisons, no storage: a few hundred gas within the measured 82,552-gas createOrder. And it cannot move off-chain without changing what it protects against: the check binds receiver to the creator (or the verified fee-collection shape) at the contract boundary, so a hostile or buggy frontend cannot craft an order that strands or diverts the proceeds of the wallet funding it. Off-chain validation protects only users of that particular frontend.
| * creator's choice and trust, and naming a hostile one harms only the creator, whose funds alone back | ||
| * the order. | ||
| */ | ||
| contract DelegatedMaker is IERC1271, IPreInteraction { |
There was a problem hiding this comment.
It is not clear which problem the contract solves. It is a costly wrapper around Limit Order Protocol orders creation which pretends to hide complexity introduced by changes in order registrator but only causes additional Gas costs
There was a problem hiding this comment.
Btw, tons of AI'sh comments are a headache: while the half I read is factually correct, they are not helpful and rather distract attention
There was a problem hiding this comment.
Accepted. #435 carries one-line declarative NatSpec only, and the fusion-protocol port (1inch/fusion-protocol#223) follows the same rule; design rationale lives in PR descriptions where it can be skipped.
There was a problem hiding this comment.
The problem it solves is a product requirement from the Fusion side: a one-transaction, no-off-chain-signature, no-escrow flow for ERC-20 makers (prior art: CoW's presign plus its VaultRelayer just-in-time pull). Measured cost of that property: createOrder 82,552 gas — replacing both the separate approval transaction and the off-chain signature — and steady-state fills at 100,715 vs the 121,694 baseline settlement fill.
Whether that trade is worth shipping, and in which repo, is now an explicit open question for the team; the contract is parked and no longer blocks the anchored-auction work (#435 + 1inch/fusion-protocol#223).
| * quoter cannot pad it away exactly. Either accept that the protocol takes its surplus share of the | ||
| * premium, or set the estimate with that in mind. | ||
| */ | ||
| contract FusionAnchoredAuction is AmountGetterBase, IPostInteraction { |
There was a problem hiding this comment.
Interesting MVP, however, has a lot of duplications from existing code. Has to be reorganized to demonstrate real code changes to make a thorough review
There was a problem hiding this comment.
Agreed — and the duplication was structurally unavoidable in this repo, since the settlement's auction functions are private and live in another codebase; that is the strongest argument for the move. In 1inch/fusion-protocol#223 the same feature is exactly what this comment asks for: a move-only extraction commit first (bodies byte-identical, private → internal virtual, the untouched 115-test suite pinning behavioral identity), then the anchored feature as a small inheriting extension — reviewable commit-by-commit.
Important
Being restructured per review (Vadim's split + Stepan's layering comments): the registrator change is extracted to #435 (the only limit-order-protocol piece); the auction mechanics have landed as fusion-protocol#223 (a standalone extension inheriting the auction base extracted from
SimpleSettlement, fill ladder included);DelegatedMakeris parked pending a home decision. This PR stays open as the reference/discussion umbrella until the split PRs land, then closes unmerged. Detailed replies to each review comment are being posted on the threads.Implements PT1-724 — the announcement-anchored Dutch auction — plus fill-size-dependent pricing over the order's volume ladder, and the one-transaction / no-signature / no-escrow maker flow built on top of them.
Everything now lands here. #432 (
DelegatedMaker) and #433/#434 (two unrelated repo fixes) have been merged into this branch, so this is the single PR to review. Three new contracts, plus two small pre-existing bugs fixed along the way.Where to start reviewing
The diff is 31 files and about five thousand lines, but the trust-bearing surface is 751 lines across three contracts, best read in dependency order:
contracts/helpers/OrderRegistrator.sol(83 lines) — the anchor's only write path: maker-only, signature-free, idempotent, hashing locally against the protocol's ERC-5267 domain.contracts/extensions/FusionAnchoredAuction.sol(422 lines) — every pricing rule: the settlement-parity time curve, anchoring, the volume ladder with its monotonicity enforcement, exclusivity re-anchoring and the announcement deadline. Stateless views plus a revert-only post-interaction; it holds no funds and no allowances.contracts/helpers/DelegatedMaker.sol(246 lines) — the one custody-adjacent contract: a shared presigning maker concentrating standing allowances, with a just-in-time pull. Read it against the receiver and fee-shape paragraphs below; this is where review time is best spent.Everything else is evidence rather than trust surface: the 87 owned tests in
test/mirror this description's claims one-for-one,scripts/holds the measurement harnesses behind every number and chart here,contracts/mocks/SimpleSettlementMock.solis a test-only mirror of the deployed settlement, thedocs/pages are generated, and the two pre-existing repo fixes are a one-line deploy-script correction plus a two-line test guard.Why
A Fusion auction starts at an absolute timestamp baked into the order extension when it is built. A maker that signs slowly — a multisig collecting signatures — misses its own auction window, and the order degrades to the floor price. DAPP-6724 dropped its no-contract-change phase 1 on 2026-07-31 precisely because that floor-price execution is a bad deal for users, so the contract work is now the path forward. Prior art: UniswapX solves the same staleness problem by letting a trusted cosigner set
decayStartTime/decayStartBlockat broadcast; the announcement-anchored design achieves that re-anchoring trustlessly.The scenario this PR exists for. A Safe taking 45 minutes to co-sign: today the curve decays while owners are still signing, so the order is at the floor by the time it can be filled — a free option for resolvers (the red band, running on unbounded). Anchored, it is unfillable until announced, then runs the same full curve an EOA gets. Both paths bottom out at the same floor, the order's own proportional rate, since the rate bump is unsigned and can only price against the taker; what the optional announcement deadline changes is how long the order stays fillable down there (the short green segment, ended at the X). The initial bump is exaggerated to 5% for legibility; production bumps are usually below 1%.
How the flows fit together
Registration and fill authorization are separate checks the core protocol keeps separate, so each maker type gets one flow:
DelegatedMaker.createOrdertransaction — presign, broadcast and anchor together, nothing signed, no escrow, funds pulled just in time at fill.createOrderWithPermitfolds the EIP-2612 allowance into the same transaction, and fees are collected through the verified custom-receiver shape (below).execTransactiondelegatecalling the officialMultiSend— an inner delegatecall to the already-deployedSignMessageLibmarks the digest (the ERC-1271 presign later empty-signature fills validate against), and an inner call toregisterOrderwrites the anchor, since the registrator sees the Safe itself as sender. Nothing signed off-chain, no bespoke helper, and no new Safe delegatecall target: the only delegatecall surface isSignMessageLib, which the dApp already uses.Two pruning passes
After the feature set stabilized, two deliberate pruning passes removed everything with an overlapping sibling or a missing use case, so what ships is one mechanism per job:
OrderAnnounced(bytes32 indexed orderHash, uint256 timestamp, Order order, bytes extension)fires exactly once, on the first registration: it is both the anchor signal and the broadcast resolvers read presigned orders from. Repeat registrations are silent no-op successes; the block number was redundant with the log itself, andannouncedAtBlockis gone.SignAndAnnounceis deleted in favor of the MultiSend batch above — one contract fewer and one security-review item fewer.max(announcedAt, builtStartTime); the anchored flag carries no field, so there is nothing for the SDK to compute. An absolute built start can still push the auction past its announcement.approveRouteris gone; the pull tops up the protocol's allowance when it runs short. This also fixed a latent defect — a one-shot max approval would silently deplete for tokens that decrement even infinite allowances, eventually bricking fills.0x01 ANCHORED / 0x02 FILL_CURVE; exclusivity flags are0x01 ANCHORED / 0x02 ANNOUNCEMENT_DEADLINE.What changed
OrderRegistratorrecords the timestamp each order was first announced at. Registration is maker-only, signature-free and idempotent:registerOrder(order, extension)reverts unlessmsg.senderis the order's maker, takes no signature because the transaction itself is the maker's proof of intent — strictly stronger provenance than "someone held a valid signature" — and on the first call writes the anchor and emitsOrderAnnouncedwith the full order payload; a repeat call is a silent success that changes nothing, so an anchored auction can never be shifted. The signature check had become circular in every contract-maker flow anyway (a Safe markssignedMessagesin the same transaction that registers, so an ERC-1271 check would read back state the caller just wrote); removing it drops theECDSAdependency and the ERC-1271 roundtrip at registration. The order hash is computed locally, from the ERC-5267 domain captured at construction, rather than through a cross-contracthashOrdercall that carries the whole order — the fork tests pin the local hash bit-for-bit against the live router's — and the call returns(orderHash, firstRegistration)so contract callers need no separate lookup.DelegatedMaker(new helper) is the EOA flow — one transaction per order, no signature, no escrow. One shared contract is the maker for all of its users (CoW Protocol's presign plus its VaultRelayer just-in-time pull, as prior art):createOrderrecords the caller as the order's owner (the presign read byisValidSignature), validates that the order names this contract as maker, the remaining invalidator (the bit-invalidator nonce space would be shared across users) and a pre-interaction routed to this contract, then announces — anchoring the auction clock at the create block, through a singleregisterOrdercall whose returned first-registration flag doubles as the duplicate check. Funds stay in the owner's wallet until the fill: the pre-interaction pulls exactly the filled amount through the owner's standing allowance immediately before the maker-asset transfer, topping up the protocol's own allowance for the maker asset whenever it runs short, so custody time is zero and the contract holds no balances between transactions.createOrderWithPermitfolds the owner's EIP-2612 allowance into the create transaction itself — owner pinned to the caller, failures swallowed the way the protocol treats maker permits, so a front-run permit changes nothing and a wrong one leaves an order that a later approval revives.cancelOrderdeletes the presign and invalidates at the protocol as the maker — both load-bearing, since the core validates ERC-1271 only on the first fill (OrderMixin._fillContractOrder) — and the duplicate check reads the registrator's ever-registered state, so a cancelled order cannot be re-created into a presigned-but-protocol-dead shell. The contract concentrates standing allowances (the same trust shape as the router itself) and has no owner powers over user funds.Proceeds must route to the creator, one way or the other. The default rule is
receiver == creator. A non-creator receiver is accepted in exactly one shape — fee collection: the order's post-interaction must target the receiver itself (which isFeeTaker's own distribution condition,order.receiver.get() == address(this)), carry the custom-receiver flag, and name the creator in the custom-receiver slot, with the post-interaction maker trait set. That guarantees a conformingFeeTaker-layout settlement forwards the net to the wallet that funded the order; anything else — a zero receiver, missing hook trait, short fee bytes, a foreign target or a foreign custom receiver — revertsInvalidFeeReceiver, because each of those strands or diverts the taking asset (FeeTakerwould otherwise pay the maker's share toorder.maker, the shared contract, which deliberately cannot withdraw). The fee contract itself is the creator's choice and trust; naming a hostile one harms only the creator, whose funds alone back the order. This deliberately couplesDelegatedMakerto one fee layout, pinned by tests against a realFeeTakerand the live mainnetSettlement. Open product decision: if delegated orders can launch feeless, this whole validation collapses to the one-linereceiver == creatorrule and the only hardcoded dependency on another contract's byte layout disappears; re-adding it later is purely additive.FusionAnchoredAuctionis a new extension; every feature is opt-in per order through a flags byte, and with no flags set it prices exactly like the deployed settlement's auction:0x01) atmax(announcedAt, builtStartTime)— the auction starts the moment the maker announces, which is the product configuration, and an absolute built start can still defer it. The anchor fixes stale timing only: the curve's levels are baked at build time, which is what the announcement deadline bounds. Resolver exclusivity is re-anchored the same way in the post-interaction, since a late announcement leaves absolute exclusivity windows already elapsed and would let every resolver in at once. Announcing and filling can even share a block: the maker announces and a resolver fills later in the same block, priced at the very top of the curve.0x02). A fill is priced by its own share of what remains: the remainder is a fresh ladder for every fill, so a tenth of what is left prices at the 1/10 row whether it is the first fill or the fifth, and whoever completes the order — in one sweep or as the last of many fills — pays the plain auction price. This is the team review's finding folded in: the earlier revision indexed the curve by the fill's cumulative end on the original amount, which under-paid makers on late partials (with 80% filled, a fill of 10% of the original priced at the nearly-free 9/10 row despite being half of everything left — 0.40% instead of 2.00% on the chart's curve). The remainder share is never smaller than the cumulative end was, so on a non-increasing curve the maker is never paid less, and a fresh order's pricing is unchanged — the quote matrix still maps row-for-row. The premium is enforced non-increasing along the ladder, lazily — one comparison per visited point, validating exactly the prefix a fill is priced on (NonMonotonicFillCurve); a rising stretch would pay takers to split, so it reverts instead of pricing, and enforcement makes the initial premium the provable worst case the making-amount estimate anchors on. The path-independent alternative — integrating the ladder over each fill's span, asRangeAmountCalculatorand gradual Dutch auctions do — was considered and rejected: path independence makes splitting cost exactly what sweeping costs, removing the full-fill incentive this feature exists to create.0x02+ 3 bytes) — the fill-by mechanism for anchored orders, bounding the floor-price tail an absolute expiry cannot once the start is anchored (order expiry is auint40inmakerTraitsenforced by the protocol core, which an extension cannot move). It rides in the post-interaction because a post-interaction is not skippable: an order whose amount data was mis-assembled prices at its plain ratio and skips every getter-side check, but still dies at its deadline. Requires the anchored bit and fails closed (InvalidFlagCombination); conceptually the delay isduration + the tail window the maker tolerates.One encoding for every schedule, drawn over the share of the remainder a fill takes. The matrix hits each quote row exactly at its share (the rungs) and interpolates in between; a curve with no interior points (dashed) is the one-parameter schedule. Both end at zero, so completion is never penalized.
The incentive this creates, each fill priced by its share of what remains. Splitting always costs the takers more than sweeping, and a sweep pays nothing extra; in the 30/30/40 case the fill that completes the order pays no premium at all. Superadditivity is pinned twice in tests: exact values for hand-picked splits, and a seeded sweep of random partitions priced through the contract's own views over both curve shapes.
Where this matters most: a thin book with real price impact
The feature earns its keep on low-liquidity pairs. Sweeping such an order moves the price a lot, so the quoter has to price the whole order at the full-impact rate — and today that single rate is what every fill pays, whatever its size. A resolver who takes a tenth bears only a fraction of the impact but still pays the rate that was set to make a full sweep viable, and keeps the difference.
A pair where a full sweep costs 4% in impact. Today a resolver filling a tenth of the order keeps 2.66% of that fill purely because the slice was small; the ladder prices that slice at its own size and the money stays with the maker — and the remainder-based semantics extend that to late slices too, which is why ten tenths now recapture 2.00% (they recaptured 1.16% under the earlier cumulative indexing). Sweeping is unaffected — the completing fill never reads the curve — so the mechanism only bites the cherry-picking it is meant to discourage. Every "this PR" number is
getTakingAmounton a real order; the impact model (impact growing with the square root of size) is the stated assumption, and the matrix here is set to the full modelled edge, which is the upper bound of what can be recaptured. A production quoter would leave resolvers some margin, so real recapture sits below these numbers.Composition rather than inheritance
The settlement lives in fusion-protocol (still publishing the
@1inch/limit-order-settlementnpm package), and its master pins solc 0.8.23 with zksolc 1.4.1 — verified against the repo'shardhat.config.jsand contract pragmas — whilecross-chain-swappinspragma solidity 0.8.23too. This repo is on 0.8.30, so a settlement subclass written here could not be inherited downstream without upgrading the compiler in two repos, and a floating pragma on one file would not help because all of its imports pin 0.8.30.Instead this is a standalone amount getter and post-interaction, referenced by address from the order extension and chained after whatever settlement is deployed, through dispatch points that already exist (
AmountGetterBaseforwards toIAmountGetter(tail[0:20]),FeeTaker._postInteractionforwards toIPostInteraction(tail[0:20])— and fusion-protocol'sSettlement._postInteractiononly adds the priority-fee gate before calling straight through to that forwarding). No code and no compiler work in the other repos. Anchored orders carry a neutralized outer curve so the settlement's build-time auction contributes a factor of one; inlining it intoSimpleSettlementlater is optional cleanup, not a prerequisite.This holds for Fusion+ as well, verified against
cross-chain-swapsource:BaseEscrowFactory._postInteractionforwards a chained post-interaction (between its whitelist data and the escrow immutables) and leaves the amount-getter chain untouched, so cross-chain orders can adopt the anchored auction through the same composition. Note its fee layout differs from same-chainSettlement— it callsFeeTaker._getFeeAmountsdirectly, so it carries no surplus-fee fields.One interaction to know about, pinned empirically by fork test 5: the fill premium counts toward the settlement's surplus fee. Anything a fill pays above the scaled estimated taking amount is taxed at the surplus percentage, the estimate scales linearly with fill size while the premium does not, so the quoter cannot pad it away exactly — either the protocol takes its surplus share of the premium (the default) or the quoter sets the estimate with that in mind.
Both halves of the composition argument are tested rather than asserted, twice over: against
SimpleSettlementMock— a line-for-line mirror of fusion-protocol master's_getRateBump,_getAuctionBumpand whitelist walk, kept as a mock only because the real contract's compiler pins prevent importing it — and against the actually deployed settlement on a mainnet fork (below).What composition costs in gas
Measured, not estimated: every number below is
receipt.gasUsedfrom a real fill, once against the live mainnet Settlement on a fork and once against the local mirror of it, with identical orders on both sides of each pair. Reproduce with the committed harnesses:scripts/gas-comparison.js(local),scripts/gas-comparison-fork.js(fork, needsMAINNET_RPC_URL),scripts/price-impact.js(the thin-book charts) andscripts/fill-curve-data.js(the ladder and splitting charts).Chaining the anchored auction behind the settlement costs about 15.0–15.3k gas per fill — the figure barely moves between the two measurements (+15,321 live, +15,052 local), because it is the cost of one extra external call plus parsing the auction blob. The percentage differs only because the baselines do: the live-settlement order was configured with zero fees, so its baseline does less work.
The rest of the picture, from the same measurements:
DelegatedMaker.createOrdercosts 82,552 gas, but it is not an extra transaction: it replaces the off-chain signature and the separate approval, so that flow goes from "approve, sign, wait" to one transaction. Its first fill carries a further one-time ~24k of allowance self-provisioning; steady-state fills run at 100,715.A dedicated optimization round produced these numbers (they started ~6–12% higher): the helpers hash orders locally from the ERC-5267 domain instead of a cross-contract
hashOrderround trip,registerOrderreturns(orderHash, firstRegistration)socreateOrdercollapsed from four external calls to one, and the fixed-point scaling takesAmountCalculatorLib's 128-bit-guarded fast path with a full-widthmulDivfallback that tests pin at the 2^128 boundary. The one accepted toll is +92 gas on a plain announcement, paying for the return values. What deliberately stayed, with its measured price: the event payload (4.4–6k, it is the trustless broadcast), the registrator's extension validation (521, keeps every broadcast payload fillable), and the lazy allowance check (~2.5–2.9k perDelegatedMakerfill, the insurance that allowance-decrementing tokens cannot brick fills). The structural ~15k chaining cost is recoverable only by inlining the auction into the settlement — a fusion-protocol follow-up.Two pre-existing repo fixes folded in
Both were found while getting this branch green, both are unrelated to the feature, and both were briefly separate PRs (#433, #434) now merged here:
deploy/deploy-Permit2Proxy.jsnever destructuredgetNamedAccounts, so the script throws at runtime and ESLint reportsno-undef. Sincelint:jsandlint:solshare oneyarn lint, that single error kept master's lint job red. The corrected signature matches every other script indeploy/; I checked the other seven for the same mismatch and this was the only one.yarn testfailed under--parallel. hardhat-tracer's recorder lives in the main process, so under mocha's parallel workers it collects nothing,findTracegets an empty list, and twowiptests died onCannot read properties of undefined (reading 'top'). CI never saw it because CI runs the non-parallelyarn test:ci. The two opcode-count assertions already carried a guard for the analogous coverage case, so this extends it withtracingAvailable()(process.env.MOCHA_WORKER_ID === undefined, chosen by probing both modes;=== undefinedbecause worker zero reports"0"). Upgrading to hardhat-tracer 3.4.0 was tried first and reproduces the same failures, so no dependency bump is included. The assertions are skipped, not defeated: corrupting an expected count toMSTORE: 999still fails the serial run while parallel stays green.AGENTS.mddocumented both of these as durable workarounds and has been updated, since neither is true any more.Tests and review
87 unit tests across the three suites this branch owns (
FusionAnchoredAuction60,DelegatedMaker18,OrderRegistrator9), all againstLimitOrderProtocolwith expected prices computed independently in JS:AccessDenied, with nothing a signature could do about it), the payload-carryingOrderAnnounced, the returned(orderHash, firstRegistration)pair with the locally computed hash pinned bit-for-bit againsthashOrderon the protocol, and contract-maker announcements through both theSafeOrderBuilderdelegatecall and the MultiSend batch — whose digest marking is verified against the Safe's own ERC-1271 view;mulDivfallback pinned to the identical formula on both sides of the 2^128 boundary, in both getter directions and through the ladder's share measurement;DelegatedMaker: the one-transaction lifecycle (create → anchored at the create block → filled with the empty signature, wallet untouched until the fill, the shared contract holding nothing before or after, and the protocol allowance self-provisioned on the first fill); exact just-in-time pulls across partial fills with proceeds landing on the owner directly; every creation rule rejected (wrong maker, zero receiver, bit-invalidator traits, missing pre-interaction hook, foreign pre-interaction target, duplicate hash, cancel-then-recreate); fee collection in the full production shape — fee taker as receiver paying the creator as custom receiver, anchored auction through the fee taker's getters, exclusivity through its post-interaction tail, the pull on the same order — plus five fee-shape reverts (missing flag, foreign custom receiver, foreign target, short bytes, missing hook trait) with a positive control; both permit paths — the allowance folded into one create transaction, and a front-run permit swallowed with the order still created and fillable; cancellation of a half-filled order killing the rest at the protocol (the first-fill-only ERC-1271 nuance); pull authorization (non-protocol callers and ownerless orders both refused); and two users on the shared maker isolated from each other's orders and cancellations.5 mainnet-fork integration tests (
yarn test:fork, opt-in viaMAINNET_RPC_URLorFORK_RPC_URL, skipped otherwise so CI is unaffected), with only the new contracts deployed on top of live state:0x2Ad5004c60e16E54d5007C80CE329Adde5B51Ef5, the address recorded in fusion-protocol'sdeployments/mainnet/Settlement.json), including its surplus-fee fields and priority-fee validation, with its curve neutralized so the chained anchored auction owns the price and the chained exclusivity still rejects an early resolver;AccessDenied), then one co-signed MultiSend batch through the live MultiSend and SignMessageLib deployments marks the digest and anchors the auction, and the order fills with the empty ERC-1271 signature;DelegatedMakerflow end to end through the live router: allowance, onecreateOrdertransaction, and a fill that pulls just in time — nothing signed, nothing escrowed, the protocol allowance self-provisioned inside the fill;surplusFee%of it, pinning the surplus-on-premium interaction with a real number.Review trail: Bugbot on eleven rounds with one finding across all of them — in the remainder-ladder round it caught the JS pricing mirror omitting the gas-bump offset the contract applies, fixed in the same round (the initial implementation, fill-curve parsing, cumulative-ladder arithmetic, maker-only authorization, fee-shape and permit, simplification, hard-cut, the
DelegatedMakersplit, the two repo fixes, the gas-optimization round, and the remainder-ladder round were otherwise clean); Slither 0.11.6 at solc 0.8.30 (only the timestamp-comparison and default-false-local patterns inherent to a Dutch auction, plus known false positives inside OpenZeppelin'sMath.mulDiv); checked against OpenZeppelin's published audits of the settlement refactor and LOP v4 diff (no contradicted assumptions; the 10-byte whitelist-address comparison is a known, accepted trade-off there and is noted in the NatSpec here); checked against the Solidity known-bugs list — the repo's 0.8.30 + viaIR configuration falls inside the affected ranges of SOL-2026-1 (transientdelete, fixed 0.8.34) and SOL-2026-2 (mutual-recursion spill, fixed 0.8.36), but neither trigger pattern exists in this codebase, so neither is reachable here.Correction to an earlier revision of this description, which suggested a repo-wide bump to solc ≥0.8.36 as a follow-up hygiene ticket: that upgrade is not available to this repo and the suggestion should not be acted on. zksolc supports solc only up to 0.8.30 (per the ZKsync compiler toolchain docs, whose solc fork tops out at
0.8.30-1.0.2), and zkSync is a live target here —deployments/zksync/FeeTaker.jsonrecords zksolc 1.4.0 with solc 0.8.23, andWethUnwrapper.jsonrecords zksolc 1.3.10 with solc 0.8.19. The repo's 0.8.30 pin therefore sits exactly on zksolc's ceiling, and moving past it would make these sources uncompilable for zkSync Era. Closing SOL-2026-1/2 by upgrading has to wait for zksolc to support a newer solc, or would require a split compiler configuration per target — a real architectural change rather than hygiene. Neither bug is reachable in this code, so nothing here is blocked on it.Verification of the consolidated branch:
yarn lintclean;yarn test(parallel) andyarn test:ci(serial) both 257 passing, 0 failing;yarn coverage244 passing with 100% statements, branches, functions and lines on all three contracts this PR ships — including the full-width fallback branches;yarn test:fork5 passing against live mainnet state. Generateddocs/pages are regenerated for the changed contracts.Resolved decision: registration is maker-only and signature-free
An earlier revision left registration permissionless for anyone holding a valid signature, which let the first caller start the auction clock on orders whose signature was public. That is now closed: only the maker's own transaction registers and anchors, which eliminates the early clock-setting vector outright instead of bounding it, and the signature went with it because a maker-sent transaction authenticates more strongly than any signature check (which had become circular for contract makers anyway). Accepted costs, deliberately: no relayer-paid broadcast or anchoring; no atomic third-party announce-and-fill (one block of latency instead); contract makers that implement ERC-1271 but cannot make arbitrary calls cannot use anchored orders; and a plain-EOA maker who self-announces distributes the fill signature off-chain, since the event is maker-authenticated broadcast rather than a signature channel — or uses
DelegatedMakerand has no signature anywhere. Self-announced-but-unfillable orders are self-sabotage only; resolvers simulate before filling.Follow-ups for other teams
OrderAnnounced, which fires exactly once per order and carries the timestamp and the full order payload.SignMessageLibdelegatecall +registerOrdercall) — one co-signing ceremony, built from components the dApp already trusts, with no new delegatecall target to security review.DelegatedMakerorders couple it toFeeTaker's custom-receiver layout by design; if the layout ever moves, the offsets in_createOrdermove with it.SimpleSettlement._getFeeAmountswould read the partial-fill premium as surplus and tax it, so the estimated taking amount needs scaling by the same factor. Inlining would also recover most of the ~15k gas the chained call costs. Its master has no anchored-auction work yet, so this branch remains the reference implementation for the team's split.registerOrder, now returning(orderHash, firstRegistration), and the single payload-carryingOrderAnnounced— has no deployed victim since the legacy deployment keeps serving legacy semantics. Anchored orders only work when announced through the new deployment.DelegatedMakerships with its own create3 salt and deploy script; deploy order is registrator first, then the auction and the helper.NativeOrderFactoryescrow-per-order pattern stays the native answer, with the clone's initialization callingregisterOrderso the anchor is maker-written in the create transaction.DelegatedMaker: a delegated EOA can hold the presign locally with no allowance concentration and the same one-transaction UX, and needs nothing from this repo.