1 Commits
Author SHA1 Message Date
sneak 1e8691a3f6 fix: a split swap's Min. received is the sum of its steps' minimums (closes #647)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The step that sets the output side gives the minimum. When that step
and earlier swap steps each pay the output token to the sender, the
minimum is now the sum of their minimums. There is no figure when the
decoder reads none from one of them, or when one of them pays part of
that token elsewhere.

These steps drop earlier steps from the sum:

- a V2 or V3 swap step that spends the output token;
- a V4 step with a swap action that spends that token, or with a SETTLE
  or SETTLE_ALL of that token;
- a PERMIT2_TRANSFER_FROM of that token.

A step after the step that sets the output side drops nothing from the
sum.

Model: opus-5-5
2026-10-27 09:50:02 +00:00
3 changed files with 137 additions and 46 deletions

No files matched your search

+14 -13
View File
@@ -3469,19 +3469,20 @@ Currently supported:
spends the output token drops earlier steps from the sum. A V4 step spends spends the output token drops earlier steps from the sum. A V4 step spends
each token that its swap actions spend. A V4 `SETTLE` or `SETTLE_ALL` of the each token that its swap actions spend. A V4 `SETTLE` or `SETTLE_ALL` of the
output token also drops earlier steps. A `PERMIT2_TRANSFER_FROM` of the output output token also drops earlier steps. A `PERMIT2_TRANSFER_FROM` of the output
token drops earlier steps too. The `Amount` line gives the total the steps token drops earlier steps too. A step after the step that sets the output side
take of `Token In`, from the step that sets the input side on: the first V2 or drops nothing from the sum. The `Amount` line gives the total the steps take
V3 exact-in or exact-out, `WRAP_ETH` or V4 swap step that names an input token of `Token In`, from the step that sets the input side on: the first V2 or V3
or amount. A V4 step with a swap action sets the input side when no earlier exact-in or exact-out, `WRAP_ETH` or V4 swap step that names an input token or
step set it, even when it names neither (see Display Consistency). That step amount. A V4 step with a swap action sets the input side when no earlier step
counts its own amount, whoever pays it: a V2 or V3 exact-out step its set it, even when it names neither (see Display Consistency). That step counts
`amountInMax`, and a V4 step what it takes of its input token from the user's its own amount, whoever pays it: a V2 or V3 exact-out step its `amountInMax`,
wallet, as below, or, when the user pays it none of that token, the `amountIn` and a V4 step what it takes of its input token from the user's wallet, as
of the swap action that spends it. Each later V2 or V3 exact-in or exact-out below, or, when the user pays it none of that token, the `amountIn` of the
step whose `payerIsUser` is true takes its amount, found in the same way, from swap action that spends it. Each later V2 or V3 exact-in or exact-out step
the user's wallet. Every V4 step, with a swap action or without, takes from whose `payerIsUser` is true takes its amount, found in the same way, from the
the user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and user's wallet. Every V4 step, with a swap action or without, takes from the
its `SETTLE_ALL`s pay, for each token on its own; what the step that sets the user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and its
`SETTLE_ALL`s pay, for each token on its own; what the step that sets the
input side takes of any other token counts as a later step's take. A `SETTLE` input side takes of any other token counts as a later step's take. A `SETTLE`
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step `OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
+9 -5
View File
@@ -1477,14 +1477,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} }
}; };
// Each swap step that paid the sender some of the token it bought, // Each swap step that paid the sender some of the token it bought,
// as { step, token, amount, onlyToSender }: the step's index, that // as { step, token, amount, onlyToSender }:
// token, the step's stated minimum, and whether the step paid all of // - `step` is the step's index;
// that token to the sender. A later step that spends that token takes // - `token` is that token;
// the step off this list (see dropFromSum()). // - `amount` is the step's stated minimum;
// - `onlyToSender` is whether the step paid all of that token to the
// sender.
// A later step that spends that token takes the step off this list
// (see dropFromSum()).
let swapsPayingSender = []; let swapsPayingSender = [];
// The step being read spends `token`, so it may spend what an earlier // The step being read spends `token`, so it may spend what an earlier
// swap step paid the sender. Each step on swapsPayingSender that // swap step paid the sender. Each step on swapsPayingSender that
// bought `token` leaves the list. These steps spend a token: // bought `token` leaves the list. A step spends a token as follows:
// - a V2 or V3 swap step spends its input token; // - a V2 or V3 swap step spends its input token;
// - a V4 step spends each token that one of its swap actions spends, // - a V4 step spends each token that one of its swap actions spends,
// and each token that one of its SETTLEs or SETTLE_ALLs pays; // and each token that one of its SETTLEs or SETTLE_ALLs pays;
+114 -28
View File
@@ -3743,25 +3743,54 @@ describe("uniswap decoder", () => {
]); ]);
}); });
// The middle step spends the user's WETH, so the count of WETH // The middle step swaps 0.3 of the user's WETH for USDC, so the
// starts again after it. // sum of minimums starts again after it.
test("a step that spends WETH between them leaves the last step's minimum", () => { test.each([
const result = decodeSteps( {
[0x00, 0x00, 0x08], kind: "a V3 exact-in",
[ command: 0x00,
V3_TO_WETH, input: encodeV3SwapExactIn(USER_ADDR, WETH_03, 1n, [
encodeV3SwapExactIn(USER_ADDR, WETH_03, 1n, [ WETH_ADDR,
WETH_ADDR, USDC_ADDR,
USDC_ADDR, ]),
]), },
V2_TO_WETH, {
], kind: "a V2 exact-in",
); command: 0x08,
expect(minimum(result)).toEqual([ input: encodeV2SwapExactIn(USER_ADDR, WETH_03, 1n, [
"Min. received", WETH_ADDR,
"0.2000 WETH", USDC_ADDR,
]); ]),
}); },
{
kind: "a V2 exact-out",
command: 0x09,
input: encodeV2SwapExactOut(USER_ADDR, 1n, WETH_03, [
WETH_ADDR,
USDC_ADDR,
]),
},
{
kind: "a V3 exact-out",
command: 0x01,
input: encodeV3SwapExactOut(USER_ADDR, 1n, WETH_03, [
USDC_ADDR,
WETH_ADDR,
]),
},
])(
"$kind that spends WETH between them leaves the last step's minimum",
({ command, input }) => {
const result = decodeSteps(
[0x00, command, 0x08],
[V3_TO_WETH, input, V2_TO_WETH],
);
expect(minimum(result)).toEqual([
"Min. received",
"0.2000 WETH",
]);
},
);
// The middle step swaps 0.3 WETH and 400 DAI for USDC, both paid // The middle step swaps 0.3 WETH and 400 DAI for USDC, both paid
// by the user. It spends WETH whichever SETTLE comes last. // by the user. It spends WETH whichever SETTLE comes last.
@@ -3807,26 +3836,83 @@ describe("uniswap decoder", () => {
}, },
); );
// The middle step has no swap action. Its SETTLE pays 0.3 WETH // The middle step's swap actions buy WETH with the user's USDT,
// from the user, and its TAKE pays that WETH to another address. // then swap all of that WETH for USDC.
test("a V4 step whose SETTLE pays WETH between them leaves the last step's minimum", () => { test("a V4 step that swaps through WETH between them leaves the last step's minimum", () => {
const wethToOther = encodeV4Swap( const usdtToUsdcThroughWeth = encodeV4Swap(
new Uint8Array([V4_SETTLE_ID, V4_TAKE_ID]), new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[ [
settle(WETH_ADDR, WETH_03, true), encodeV4ExactInSingle(
take(WETH_ADDR, OTHER_ADDR, 0n), USDT_ADDR,
WETH_ADDR,
USDT_400,
WETH_01,
),
encodeV4ExactInSingle(WETH_ADDR, USDC_ADDR, 0n, 1n),
encodeV4Settle(USDT_ADDR), // paid by the user
encodeV4TakeAll(USDC_ADDR, 0n),
], ],
); );
const result = decodeSteps( const result = decodeSteps(
[0x00, 0x10, 0x08], [0x00, 0x10, 0x08],
[V3_TO_WETH, wethToOther, V2_TO_WETH], [V3_TO_WETH, usdtToUsdcThroughWeth, V2_TO_WETH],
); );
expect(minimum(result)).toEqual([ expect(minimum(result)).toEqual([
"Min. paid out", "Min. received",
"0.2000 WETH", "0.2000 WETH",
]); ]);
}); });
// The middle step has no swap action. Its SETTLE or SETTLE_ALL
// pays 0.3 WETH, and its TAKE pays that WETH to another address.
test.each([
{
payment: "a SETTLE of WETH paid by the user",
actions: [V4_SETTLE_ID, V4_TAKE_ID],
params: [
settle(WETH_ADDR, WETH_03, true),
take(WETH_ADDR, OTHER_ADDR, 0n),
],
},
{
payment: "a SETTLE of WETH paid from the router",
actions: [V4_SETTLE_ID, V4_TAKE_ID],
params: [
settle(WETH_ADDR, WETH_03, false),
take(WETH_ADDR, OTHER_ADDR, 0n),
],
},
{
payment: "a SETTLE_ALL of WETH",
actions: [V4_TAKE_ID, V4_SETTLE_ALL_ID],
params: [
take(WETH_ADDR, OTHER_ADDR, WETH_03),
encodeV4SettleAll(WETH_ADDR, WETH_03),
],
},
])(
"a V4 step with $payment between them leaves the last step's minimum",
({ actions, params }) => {
const wethToOther = encodeV4Swap(
new Uint8Array(actions),
params,
);
const result = decodeSteps(
[0x00, 0x10, 0x08],
[V3_TO_WETH, wethToOther, V2_TO_WETH],
);
expect(minimum(result)).toEqual([
"Min. paid out",
"0.2000 WETH",
]);
},
);
test("a PERMIT2_TRANSFER_FROM of WETH between them leaves the last step's minimum", () => { test("a PERMIT2_TRANSFER_FROM of WETH between them leaves the last step's minimum", () => {
const result = decodeSteps( const result = decodeSteps(
[0x00, 0x02, 0x08], [0x00, 0x02, 0x08],