feat: announcement-anchored Dutch auction with fill-size pricing (standalone extension) - #223
feat: announcement-anchored Dutch auction with fill-size pricing (standalone extension)#223deacix wants to merge 6 commits into
Conversation
Move-only change: _getRateBump and _getAuctionBump leave SimpleSettlement for an abstract base with internal virtual visibility, so a standalone extension can reuse the time-curve math. No behavior change. Co-authored-by: Sergej Kunz <info@deacix.de>
FusionAnchoredAuction is a standalone amount getter and post-interaction, referenced per order and chainable behind the deployed settlement, so no settlement redeployment is needed. Opt-in per order via flags: an anchored auction starts at max(announcedAt, built start), resolver exclusivity windows measure from the announcement, and an announcement deadline stops fills strictly after announcedAt plus the delay. announcedAt is read from the limit-order-protocol#435 registrator through a one-function local interface. Co-authored-by: Sergej Kunz <info@deacix.de>
Volume-ladder fill pricing behind auction flags bit 0x02: a fill pays a premium read from a piecewise curve over its share of the remaining making amount, interpolated between points with an implied final point of zero premium at the full remainder, so whoever completes the order pays no premium. The walk enforces monotonicity lazily and reverts a rising stretch as NonMonotonicFillCurve. The making-amount direction estimates the share at the worst rate bump and reprices from it, which can only overstate the bump. Co-authored-by: Sergej Kunz <info@deacix.de>
…r-2b feat: price each fill by its share of the remainder
Co-authored-by: Sergej Kunz <info@deacix.de>
Co-authored-by: Sergej Kunz <info@deacix.de>
|
@SteMak — your review of 1inch/limit-order-protocol#430 drove the restructure that produced this PR; here is how each of your points that applies to this repo is addressed, so the review can continue here. "Fusion extends Limit Order Protocol. Limit Order Protocol is not a correct place to implement Fusion functionality." Agreed and done — the auction mechanics now live here. The only limit-order-protocol change left is the registrator's write-once "Has a lot of duplications from existing code. Has to be reorganized to demonstrate real code changes." The first commit ( "Tons of AI'sh comments are a headache." NatSpec here is deliberately terse — one line per member where possible, with multi-line blocks only where a packed byte layout has to be documented. Deployment shape, since it is the flip side of the layering point: this ships as a separately deployed, per-order opt-in extension rather than a settlement change, because avoiding a redeploy of the deployed settlement is a hard requirement. Folding the logic into a future settlement version stays available whenever one ships anyway, and would recover the chained-call overhead measured in 1inch/limit-order-protocol#430. For completeness: your registrator comments (signature removal, EIP-1271 compatibility, the |
|
@cursor Hey, can you justify the contract structure changes? |
|
All claims are verified against the code. No changes are needed — this is a justification question, so here is the answer. Fair question — and worth being upfront that you're partly right: nothing in this logic is technically impossible to write inside 1. Editing The chaining this relies on is not new machinery — it's the LOP's designed composition point, already in the deployed settlement bytecode. The deployed getter delegates its tail to the next getter by address: ) internal view virtual returns (uint256) {
if (extraData.length >= 20) {
return IAmountGetter(address(bytes20(extraData))).getMakingAmount(
order, extension, orderHash, taker, takingAmount, remainingMakingAmount, extraData[20:]
);
} else {
return order.makingAmount.mulDiv(takingAmount, order.takingAmount);
}
}and 2. 3. On the LoC estimate. In a world where a redeploy were acceptable, the honest in-settlement diff for the full agreed scope — anchor, announcement-relative exclusivity, deadline, and the fill ladder with its worst-case estimate/reprice in the making direction — is ~120–150 lines of logic, not 30–100; folding in would save only the ~30 lines of whitelist-walk and gas-bump scaffolding the standalone version has to carry itself. The extension file reads as 300 lines because half is NatSpec pinning the packed byte layouts. So the structure costs roughly one screen of scaffolding and one 86-line moved file, and buys shipping without a settlement release. 4. It doesn't foreclose your version. When a settlement v-next ships anyway, absorbing this into it is deliberately cheap — the math already sits in the base, so it's the ~15 lines of anchor/deadline plus the ladder functions moving one file over — and doing so recovers the ~15k gas chained-call overhead measured in #430. That's the sequencing being proposed: extension now so slow-signing makers stop bleeding to the floor, fold-in whenever a settlement redeploy happens for its own reasons. |
|
@cursor compress the answer to one paragraph, it either should start with "yes, it would be better to fold changes to SimpleSettlement" or "here is the list of objections behind the design, each consistent with solidity development best practices" |
|
Here is the compressed one-paragraph version of the answer. Here is the list of objections behind the design, each consistent with solidity development best practices: (1) deployed code is immutable — |
|
@cursor make the list a list, any referred links either should work or be removed |
|
Both cross-repo references resolve (verified through the GitHub CLI), so they stay as links; everything else is plain text. Here is the answer as a list. Here is the list of objections behind the design, each consistent with solidity development best practices:
|
|
|
His four objections share one root — my previous framing was imprecise — so this reply restates the single constraint exactly and answers each point from it. No code changes are requested yet, so I'm answering only. The imprecision is mine, so let me state the one claim everything rests on, exactly: the feature must be fillable through the settlement contracts already deployed at fixed addresses on 18 chains, and their bytecode cannot change. All four answers follow from that sentence.
|
|
@cursor Was it a reply containing justification of what I have asked? If so, I completely missed those in the text bloat |
|
Yes — it was points 1–2 of the previous reply. Here is the entire justification, minus everything else: The new auction logic must be usable by orders that settle through the already-deployed settlement contracts (immutable bytecode, 18 chains). Code added to |
|
@cursor |
|
Both questions verified directly against the pinned LOP 4.3.2 sources and the extension. No code changes needed — here is the answer. Call stack: yes, with one correction — the extension is in two legs of the fill, and the middle hop is optional.
Funds transfers: the extension adds zero. Its getters are
Total 3–5 ERC20 transfers with the settlement in front, same as now; 2 in the direct |
|
@cursor reduce answer bloat. no fee orders are not in scope |
|
Yes — Funds transfers: unchanged, the extension performs none (view getters; its |
|
@cursor Justify no redeploy of |
|
All three claims verified in the test suite and contracts. Answering directly, including where his implied point is right. Do users interact with settlement directly? No. Makers only sign an off-chain order; resolvers fill through Do users give allowances to settlement? No. Every approval in the flow targets the LOP or permit2 (maker and taker side alike). The settlement holds the taker asset only transiently, inside the fill transaction. Cost of a redeploy, in flow terms: for makers, zero — no allowance migration, no flow change. The cost is operational coordination: the settlement address is embedded in each order's signed extension bytes, so the order-building backend, SDK releases, and every resolver's config and address vetting must switch per chain; in-flight orders keep filling through the old address (a short dual-run — Fusion orders live minutes); deploy-and-verify on 18 chains; and, with this feature folded in, each chain's new settlement additionally gates on the 1inch/limit-order-protocol#435 registrator being live there first, since it becomes a constructor dependency. So, candidly: the no-redeploy requirement is not forced by user-facing friction — your questions establish that correctly. It is an operational and release-cadence constraint, and it came to this PR as an input (the team's split decision on 1inch/limit-order-protocol#430; PT1-724), not as my derivation. If the team is willing to pay the coordination cost and ship this inside the next settlement version, folding it in is the better end state — the extraction commit deliberately keeps that fold small. That call is the Fusion team's to make; the PR as written implements the constraint as given. |
|
@deacix I see, you put "no redeploy of |


Change Summary
What does this PR change?
Adds the announcement-anchored Dutch auction as a standalone, per-order opt-in extension, plus fill-size-dependent pricing over the order's volume ladder. The first commit is a pure move-only extraction of the existing auction math (
_getRateBump/_getAuctionBumpand their constants) fromSimpleSettlementinto an abstractDutchAuctionBase— bodies byte-identical, visibilityprivate→internal virtual— so the feature itself reads as a small diff on top of code this repo already ships.Why: a Fusion auction starts at an absolute timestamp baked into the order at build time, so a maker that signs slowly (a multisig collecting signatures) misses its own window and degrades to the floor price. Anchoring starts the schedule from the moment the order was first announced on-chain instead. This executes the team's agreed split of 1inch/limit-order-protocol#430 (the auction mechanics belong in this repo); the anchor is the write-once
announcedAtrecorded by the registrator in 1inch/limit-order-protocol#435.The scenario this exists for: a Safe taking 45 minutes to co-sign. Today the curve decays while owners are still signing and the order is at the floor by the time it can be filled — a free option for resolvers. Anchored, it is unfillable until announced, then runs the same full curve an EOA gets; the optional announcement deadline bounds how long it stays fillable at the floor. The initial bump is exaggerated to 5% for legibility.
Related Issue/Ticket: PT1-724; review thread and reference implementation: 1inch/limit-order-protocol#430.
Testing & Verification
How was this tested?
Unit tests
Integration tests
Manual testing (describe steps)
Verified on staging
Baseline suite: 115 passing. After the extraction commit the untouched existing suite still passes exactly 115 — the refactor is behaviorally identical by test evidence, not just by eyeball.
16 new tests for the anchored extension: pricing parity with the settlement's own auction, anchored-start cases (stale built start ignored, later built start wins, same-block announce-and-fill), unannounced anchored order reverting, exclusivity windows (anchored window held open, staggered resolvers, empty whitelist, non-whitelisted takers waiting out every window), the announcement deadline exact at its boundary, deadline-without-anchor failing closed, and chaining behind
SimpleSettlement.15 new tests for the ladder: matrix rows hit exactly on a fresh order, interpolation between and past rows, successive fills repriced on the remainder, zero-premium completion, lazy monotonicity rejection, a seeded superadditivity sweep (random partitions never undercut a single sweep, priced through the contract's own views), and the gas-bump interaction. Total: 146 passing.
New contracts at 100% line coverage;
yarn lintfully green — this branch carries a one-commit fix for the two pre-existingscripts/lint errors (space-before-function-pareninmint-kyc.jsandtransfer-ownership.js) that had master's lint job red.Risk Assessment
Risk Level:
Risks & Impact
SimpleSettlement's source is refactored (inheritance only; its untouched test suite pins identical behavior), and deployed settlement instances are unaffected. The extension is a new, separately deployed contract that orders opt into through their extension bytes.OrderRegistratorfrom feat: record first announcement time in OrderRegistrator limit-order-protocol#435 deploys first per chain;config/constantscarries the placeholder where those addresses land.OrderRegistratorMock) because the pinned@1inch/limit-order-protocol-contract4.3.2 predates theannouncedAtview; the production interface is declared locally as one view function.What's inside
Every feature is opt-in per order via a flags byte; with no flags set, pricing matches the settlement's existing auction exactly.
0x01): auction start =max(announcedAt, builtStartTime). An anchored order that was never announced revertsOrderNotAnnounced; a re-announcement can never move the anchor (write-once at the registrator).announcedAt + delay, revertingAuctionExpiredstrictly after. The deadline flag without the anchored flag revertsInvalidFlagCombination. It lives in the post-interaction because a post-interaction is not skippable by getter mis-assembly.0x02): a fill pays a premium by its share of the remaining amount (1e4 base) over a piecewise-linear curve — 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 pays no premium at all. The curve is enforced non-increasing lazily during the walk (NonMonotonicFillCurve), so splitting always costs takers more than sweeping. In the making-amount direction the share is estimated at the worst rate bump and repriced, so the estimate never exceeds the exact solution.Visualizations
One encoding for every schedule, drawn over the share of the remainder a fill takes: the matrix hits each quote row exactly at its rung 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.
Splitting always costs the takers more than one sweep, and the fill that completes the order pays no premium at all — superadditivity, pinned in this PR by the seeded random-partition test priced through the contract's own views.
Where the ladder earns its keep: on a thin book where a full sweep costs 4% in price impact, a resolver filling a tenth today keeps 2.66% of that fill purely because the slice was small; the ladder prices the slice at its own size and the value stays with the maker. Sweeping is unaffected. The impact model and its assumptions are documented in 1inch/limit-order-protocol#430; the matrix there is set to the full modelled edge, so production rows should sit below it to leave partial fills some margin.
Measured costs, from the reference implementation
Every number is
receipt.gasUsedfrom real fills of the reference implementation (1inch/limit-order-protocol#430 — identical pricing logic and the same chained-call structure as this PR), measured against both the live mainnet Settlement on a fork and a local mirror: chaining the anchored auction behind the deployed settlement costs about 15.0–15.3k gas per fill, the fill ladder adds 1,365 gas to a partial fill and 558 to a completing one, and the per-order announcement costs 31,132 gas for a lean order. The right panel'sDelegatedMakerbar belongs to a separate, currently parked proposal — not this PR. Reproduction harnesses live on the reference branch:scripts/gas-comparison.js,scripts/gas-comparison-fork.js,scripts/price-impact.js,scripts/fill-curve-data.js.Notes for reviewers
06fe413is pure code motion,0068ea0is the anchored MVP,13918b3is the ladder (originally the stacked PR feat: price each fill by its share of the remainder #224, merged into this branch, so this PR carries the complete feature).Math.mulDivto match the extracted base — a deliberate deviation from the reference implementation's 2^128 fast paths.