[simplex]: Match, reserve and value against limit orders - #1271
Conversation
a1a38a8 to
15cf6c5
Compare
Wizdave97
left a comment
There was a problem hiding this comment.
Reviewed against #1267 and hyperfx-orderbook@main.
One thing I want to record as verified rather than flagged, because it looks like a deviation and isn't: matchLimitOrder drops the issue's offer >= reqOut precondition and ranks by payout = min(offer, available) where §3 says rank by offer. Since the reduce takes the maximum payout, if any candidate can cover reqOut then the winner does too — no fill is lost, and fx.ts enforces the cross-chain all-or-nothing rule at the caller. Ranking by what is actually payable is the better rule and it answers open question 2. Worth saying so in the PR description.
The reservation lifecycle is the strongest part of this PR — claimReservation owning the hold from the moment a bid row exists, and the comment explaining why an overstated reservation is the safe way to be wrong, are both right.
Two feature gaps below (partial cross-chain fills, and combining levels) plus three defects.
501c7bc to
c209e18
Compare
9e3a1ad to
7a74b23
Compare
|
@Wizdave97 Five actioned here in Combining levels landed at the tip of the stack, in #1275 On the fee gate for partials: the cross-chain path uses the same |
Wizdave97
left a comment
There was a problem hiding this comment.
Re-reviewed at c56f8646. Both comments are addressed:
repost()now acts on the cancel: it proceeds only oncancelledor a genuineUNKNOWN_ORDER, and otherwise leaves the rowopenwith its commitment intact and the reason on it, so the next cycle retries instead of stacking a second entry. Paired with theCancelOrderResultchange in #1270, that closes the duplicate-entry path properly.drawDownandreleasethrowLimitOrderWriteErroron a lost CAS rather than returning a stale read.test:fillerpoints atfx.payout.test.ts, andfbbfbd2aremoves the curve and solver-link notes.
Cross-chain partials are enabled here too, and the fx.ts comment block correctly describes _partialFills / RedeemEscrowPartial from #980. One leftover: the matcher's own docstring still says "a cross-chain order reverts on any under-fill" — that line moved into #1275, so I have commented on it there.
No further comments from me on this one.
Wizdave97
left a comment
There was a problem hiding this comment.
Followed the overfill question down into IntentGatewayV2 on current main (post-#980). Short answer: a partial fill cannot overpay, but a full fill with targetOutput > output.amount donates the entire excess — and the comment below says the opposite.
Both IntrinsicIntents._fillSameChain and ExtrinsicIntents._fillCrossChain share one branch:
if (alreadyFilled == 0 && solverAmount > totalRequired) {
fillAmount = totalRequired;
(protocolShare, beneficiaryShare) = _splitSurplus(solverAmount - totalRequired, order.output.call.length > 0);
} else {
fillAmount = solverAmount > remaining ? remaining : solverAmount;
}The else branch is every partial fill — a first fill short of the ask (solverAmount < totalRequired) and any continuation (alreadyFilled > 0). Both clamp to min(solverAmount, remaining) and pull only fillAmount, so an over-sized options.outputs[i].amount on a partial costs nothing; the excess is ignored, not charged. The surplus branch is unreachable on a partial by construction, since it requires solverAmount > totalRequired, which is a full fill.
The other branch is the problem, and _splitSurplus distributes all of it:
if (hasOutputCall) return (dust, 0);
protocolShare = (dust * _params.surplusShareBps) / 10_000;
beneficiaryShare = dust - protocolShare;Then safeTransferFrom(msg.sender, beneficiary, fillAmount + beneficiaryShare) and safeTransferFrom(msg.sender, address(this), protocolShare) — the solver is debited exactly solverAmount. Nothing comes back.
|
The overfill fix landed at the tip, in #1275 Your |
c56f864 to
961a2e8
Compare
0262a45 to
405bbb3
Compare
501e684 to
ffff9ad
Compare
ffff9ad to
bee5720
Compare
0e032a8 to
d61d4a5
Compare
93e3cb5 to
91c537d
Compare
Release-3 gateways settle `fillOrder` as one quote per leg: the input beside each output is the most escrow the solver takes for it, and a take above the share the fill earns is refused as `RateBelowOrder`. The limit-order pricing path replaced the curve path that computed those takes, so its bids signed none at all. A fill that meets the ask takes the whole input; an under-fill takes the same proportion of it as the output it delivers, which is the escrow the gateway releases for that fill anyway. A payout too small to release any escrow is skipped rather than signed with a zero take.
Retraction names a bid by the identifier recorded on its row, so a row without one reads as nothing to retract.
91c537d to
846996a
Compare
The pieces the filler needs to price from the operator's limit orders rather than from pair curves: which limit order serves an incoming order, how much of it a bid may hold, and what an order is worth in dollars now the curves no longer say. Nothing is wired into
FXFilleryet, so behaviour is unchanged.reserveis a compare-and-set rather than a read then a write, since two chains bidding against one limit order would otherwise both see room in the gap and between them promise more output than it has. USD values come from the limit orders themselves, and a symbol with no route to a dollar stays unpriced rather than being guessed at, because the value sizes a confirmation wait.Switching
canFill,calculateProfitabilityand the valuation pass over follows separately. They reach the pair model through the same places, so that migration is one change rather than four, and it reviews better on its own.Review fixes follow in the last commit here and one at the tip of the stack.
repostnow acts on the cancel it asked for instead of posting regardless, which is what kept a refused or unanswered cancel from leaving two live entries behind one liability. Cross-chain fills may be partial, since_fillCrossChainhas kept per-token progress, cleared_filledon an under-fill and released escrow proportionally since #980; the comment that justified the old restriction is rewritten.drawDownandreleasereport a guarded write that did not apply rather than losing it silently, and the draw-down now precedes the release, so a crash between them understates capacity instead of advertising output already paid.On the matcher: dropping the
offer >= reqOutprecondition and ranking by payout rather than offer is deliberate, and it answers open question 2, since a thin order quoting a wonderful rate can no longer crowd out one that covers the swap. One swap can also draw on several limit orders now, walking levels best first until the ask is covered, because the orderbook already quotes a same-chain swapper the clearing price across every level that can fill together and honouring one of them advertises depth we then refuse to meet. That change lands at the tip of the stack, where the matcher is.Part of #1267.