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