1 Commits
Author SHA1 Message Date
sneak a0cde7987b 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. There is also none when the decoder did not read
one of them, or a step between them, in full.

These steps drop earlier steps from the sum, so a multi-hop route reads
as before:

- 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.

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

No files matched your search

+13 -14
View File
@@ -3469,20 +3469,19 @@ 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. A step after the step that sets the output side token drops earlier steps too. The `Amount` line gives the total the steps
drops nothing from the sum. The `Amount` line gives the total the steps take take of `Token In`, from the step that sets the input side on: the first V2 or
of `Token In`, from the step that sets the input side on: the first V2 or V3 V3 exact-in or exact-out, `WRAP_ETH` or V4 swap step that names an input token
exact-in or exact-out, `WRAP_ETH` or V4 swap step that names an input token or or amount. A V4 step with a swap action sets the input side when no earlier
amount. A V4 step with a swap action sets the input side when no earlier step step set it, even when it names neither (see Display Consistency). That step
set it, even when it names neither (see Display Consistency). That step counts counts its own amount, whoever pays it: a V2 or V3 exact-out step its
its own amount, whoever pays it: a V2 or V3 exact-out step its `amountInMax`, `amountInMax`, and a V4 step what it takes of its input token from the user's
and a V4 step what it takes of its input token from the user's wallet, as wallet, as below, or, when the user pays it none of that token, the `amountIn`
below, or, when the user pays it none of that token, the `amountIn` of the of the swap action that spends it. Each later V2 or V3 exact-in or exact-out
swap action that spends it. Each later V2 or V3 exact-in or exact-out step step whose `payerIsUser` is true takes its amount, found in the same way, from
whose `payerIsUser` is true takes its amount, found in the same way, from the the user's wallet. Every V4 step, with a swap action or without, takes from
user's wallet. Every V4 step, with a swap action or without, takes from the the user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and
user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and its its `SETTLE_ALL`s pay, for each token on its own; what the step that sets the
`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
+5 -9
View File
@@ -1477,18 +1477,14 @@ 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 }: // as { step, token, amount, onlyToSender }: the step's index, that
// - `step` is the step's index; // token, the step's stated minimum, and whether the step paid all of
// - `token` is that token; // that token to the sender. A later step that spends that token takes
// - `amount` is the step's stated minimum; // the step off this list (see dropFromSum()).
// - `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. A step spends a token as follows: // bought `token` leaves the list. These steps spend a token:
// - 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;
+28 -114
View File
@@ -3743,54 +3743,25 @@ describe("uniswap decoder", () => {
]); ]);
}); });
// The middle step swaps 0.3 of the user's WETH for USDC, so the // The middle step spends the user's WETH, so the count of WETH
// sum of minimums starts again after it. // starts again after it.
test.each([ test("a step that spends WETH between them leaves the last step's minimum", () => {
{ const result = decodeSteps(
kind: "a V3 exact-in", [0x00, 0x00, 0x08],
command: 0x00, [
input: encodeV3SwapExactIn(USER_ADDR, WETH_03, 1n, [ V3_TO_WETH,
WETH_ADDR, encodeV3SwapExactIn(USER_ADDR, WETH_03, 1n, [
USDC_ADDR, WETH_ADDR,
]), USDC_ADDR,
}, ]),
{ V2_TO_WETH,
kind: "a V2 exact-in", ],
command: 0x08, );
input: encodeV2SwapExactIn(USER_ADDR, WETH_03, 1n, [ expect(minimum(result)).toEqual([
WETH_ADDR, "Min. received",
USDC_ADDR, "0.2000 WETH",
]), ]);
}, });
{
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.
@@ -3836,83 +3807,26 @@ describe("uniswap decoder", () => {
}, },
); );
// The middle step's swap actions buy WETH with the user's USDT, // The middle step has no swap action. Its SETTLE pays 0.3 WETH
// then swap all of that WETH for USDC. // from the user, and its TAKE pays that WETH to another address.
test("a V4 step that swaps through WETH between them leaves the last step's minimum", () => { test("a V4 step whose SETTLE pays WETH between them leaves the last step's minimum", () => {
const usdtToUsdcThroughWeth = encodeV4Swap( const wethToOther = encodeV4Swap(
new Uint8Array([ new Uint8Array([V4_SETTLE_ID, V4_TAKE_ID]),
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[ [
encodeV4ExactInSingle( settle(WETH_ADDR, WETH_03, true),
USDT_ADDR, take(WETH_ADDR, OTHER_ADDR, 0n),
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, usdtToUsdcThroughWeth, V2_TO_WETH], [V3_TO_WETH, wethToOther, V2_TO_WETH],
); );
expect(minimum(result)).toEqual([ expect(minimum(result)).toEqual([
"Min. received", "Min. paid out",
"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],