fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The swap's Amount line took only the first swap step's amount. Each later
V2, V3 or V4 swap step the user pays for now adds what it spends of
Token In to that line, and one that spends another token gets Moved from
the sender, Token moved and Moved to lines of its own. Steps paid from
the router add nothing, and a PERMIT2_PERMIT keeps its own amount.

Model: opus-5-5
This commit is contained in:
2026-10-19 22:53:31 +00:00
committed by sneak
parent 5376723c41
commit 8382fab3b6
4 changed files with 365 additions and 64 deletions
+49 -30
View File
@@ -1189,14 +1189,8 @@ number. They can read:
`amountOut`, the exact amount it buys.
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
whole open delta", so the calldata states no quantity. The line shows the
amount of the step that sets the input side: the last `PERMIT2_PERMIT` step if
there is one, otherwise the first V2 or V3 exact-in, V2 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, as described below. A V2 exact-out step gives its `amountInMax`, and
a V4 swap step the `amountIn` of the swap action that spends its input
currency.
whole open delta", so the calldata states no quantity. Transaction Decoding
below gives the steps whose amounts the line shows.
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's
`Amount` line, when the amount it shows is a V2 exact-in `amountIn` of zero.
The router reads that zero as "the pair already holds the input tokens": the
@@ -2374,10 +2368,11 @@ view would leave a wallet one click from deletion.
- Site origin (bold, scheme and port included) + "wants to send a
transaction"
- Decoded action (if calldata is recognized): action name, token details,
amounts, what each `PERMIT2_TRANSFER_FROM` moves and where to, steps,
deadline, and unless a swap pays only the From address, a `Paid to` line
for each other address it pays, or saying that a token stays in the router
or who it pays is unknown (see Transaction Decoding)
amounts, what each swap step the user pays for spends of a token other
than `Token In`, what each `PERMIT2_TRANSFER_FROM` moves and where to,
steps, deadline, and unless a swap pays only the From address, a `Paid to`
line for each other address it pays, or saying that a token stays in the
router or who it pays is unknown (see Transaction Decoding)
- From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link, token symbol label if
known; for a contract creation, the same sentence as on WaitTx
@@ -3127,24 +3122,48 @@ Currently supported:
name); and one more reads `Unknown (could not be decoded)` when a step that
may pay an address went unread. When no step pays anyone, a single line reads
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
minimum reads `Min. paid out`. After the `Amount` line (after `Token In` when
there is none), each `PERMIT2_TRANSFER_FROM` but one into the router, in step
order, gives three lines: `Moved from the sender`, the amount it moves, for
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
`Token In` line does; and `Moved to`, which reads
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
on every router but `UniversalRouterV1`, and otherwise gives the address. A
move into the router needs none of them, because its `Paid to` line and
`Token moved into the router` line already give the amount, the token and
where it goes, and so its amount is shown once. These lines change neither the
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
does not decode as an address, or when it is a command the decoder does not
read: every command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
(`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and
the NFT commands among them), or a type its release names no command for.
minimum reads `Min. paid out`. The `Amount` line gives the amount of the step
that sets the input side: the last `PERMIT2_PERMIT` step if there is one,
otherwise the first V2 or V3 exact-in, V2 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). A V2 exact-out step gives its `amountInMax`, and a V4
swap step the `amountIn` of the swap action that spends its input currency.
Each later swap step that the user pays for also takes its amount, found in
the same way, from the user's wallet: a V2 or V3 exact-in or V2 exact-out step
whose `payerIsUser` is true, or a V4 step with a swap action whose input
currency a `SETTLE` paid by the user (`payerIsUser` true) or a `SETTLE_ALL`
names. When it spends `Token In`, its amount is added to the line's, unless a
`PERMIT2_PERMIT` set the input side, whose amount the line keeps. So a V3
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user,
read `1000.0000 USDT`. When either amount is not a quantity (a V4 open delta,
`ALREADY_PAID`, or an amount the decoder did not read), the total is not known
and there is no `Amount` line. A step paid from the router spends what the
router holds and adds nothing, and so does a V2 exact-in whose `amountIn` is
`ALREADY_PAID`, which takes nothing from the user. A step the user pays for
that spends a token other than `Token In` gives three lines after the `Amount`
line (after `Token In` when there is none), in step order:
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder read none; `Token moved`,
giving the token's address as the `Token In` line does; and `Moved to`, which
reads `The swap step that spends it`. Then each `PERMIT2_TRANSFER_FROM` but
one into the router, in step order, gives three lines:
`Moved from the sender`, the amount it moves, for example `1000000.0000 USDT`;
`Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The V2 pair the next swap spends` for a hop and
`The sender` for `address(1)` on every router but `UniversalRouterV1`, and
otherwise gives the address. A move into the router needs none of them,
because its `Paid to` line and `Token moved into the router` line already give
the amount, the token and where it goes, and so its amount is shown once.
These lines change neither the `Paid to` lines nor the minimum's label. A step
goes unread when its recipient does not decode as an address, or when it is a
command the decoder does not read: every command other than `PERMIT2_PERMIT`,
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
them), or a type its release names no command for.
### Non-Goals Forever
+114 -33
View File
@@ -335,7 +335,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
// commands this decoder does not read. That action names both of its
// currencies, and those commands may name one, so saying the calldata did not
// name it would be false. The Paid to line uses it too, when a step may pay an
// address this decoder did not read.
// address this decoder did not read, and so does the amount a swap step the
// user pays for spends of another token, when this decoder read none.
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
// The Paid to line for each token paid to the router, the swap's output or
@@ -566,6 +567,7 @@ function decodeV2SwapExactIn(input) {
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
// address[] path, bool payerIsUser)
// With `payerIsUser` false the router pays the input from its own balance.
function decodeV2SwapExactOut(input) {
try {
const d = coder.decode(
@@ -578,6 +580,7 @@ function decodeV2SwapExactOut(input) {
amountInMax: d[2],
tokenIn: d[3][0],
tokenOut: d[3][d[3].length - 1],
payerIsUser: d[4],
};
} catch {
return null;
@@ -904,7 +907,9 @@ function totalAmount(amounts) {
// read. `settledFromRouter` is every SETTLE that the router pays from its own
// balance (`payerIsUser` false), as its currency and the amount it pays: a
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
// decoder cannot tell (see below).
// decoder cannot tell (see below). `payerIsUser` is true when the user pays
// the step's Token In: a SETTLE paid by the user (`payerIsUser` true) or a
// SETTLE_ALL, which V4Router always has the user pay, names it.
function decodeV4Swap(input) {
try {
const d = coder.decode(["bytes", "bytes[]"], input);
@@ -919,6 +924,8 @@ function decodeV4Swap(input) {
const routerSettles = [];
// The currency of every SETTLE and SETTLE_ALL.
const settledCurrencies = [];
// The currency of every SETTLE and SETTLE_ALL the user pays.
const settledByUser = [];
// The index of the step's last swap action, read or not, or -1.
let lastSwapAction = -1;
// Set when an action of the step could not be read.
@@ -943,7 +950,9 @@ function decodeV4Swap(input) {
);
settleToken = s[0];
settledCurrencies.push(s[0]);
if (!s[2]) {
if (s[2]) {
settledByUser.push(s[0]);
} else {
routerSettles.push({
currency: s[0],
amount: s[1],
@@ -955,6 +964,7 @@ function decodeV4Swap(input) {
const s = coder.decode(["address", "uint256"], params[i]);
settleToken = s[0];
settledCurrencies.push(s[0]);
settledByUser.push(s[0]);
} else if (actionId === V4_TAKE) {
// TAKE: (address currency, address recipient, uint256 amount)
const t = coder.decode(
@@ -1055,6 +1065,7 @@ function decodeV4Swap(input) {
recipientUnread,
swapUndecoded,
hasSwapAction: true,
payerIsUser: settledByUser.includes(settleToken),
};
}
@@ -1097,6 +1108,7 @@ function decodeV4Swap(input) {
recipientUnread,
swapUndecoded,
hasSwapAction: swaps.length > 0,
payerIsUser: present(tokenIn) && settledByUser.includes(tokenIn),
};
} catch {
return null;
@@ -1125,6 +1137,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let inputToken = null;
let inputAmount = null;
let inputEstablished = false;
let inputFromPermit = false;
let outputToken = null;
let minOutput = null;
let outputEstablished = false;
@@ -1135,8 +1148,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
const commandNames = [];
// THE INVARIANT: an amount and the token it is counted in always come
// from the same hop. A figure is never rendered against a token that
// did not supply it.
// from the same hop, or from hops that all spend that token. A figure
// is never rendered against a token that did not supply it.
//
// Both sides are therefore set as a PAIR — never field by field, and
// never on truthiness. An address is never falsy once set but an
@@ -1152,7 +1165,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// states neither half: the output side always, and the input side
// when no earlier hop set it. A half the establishing hop did not
// state stays null and the line says so, rather than being filled in
// from a different hop.
// from a different hop. Each later swap step the user pays for adds
// what it spends, in addUserPaid(), the one place the total is formed.
const setInput = (token, amount) => {
inputToken = present(token) ? token : null;
inputAmount = present(amount) ? amount : null;
@@ -1170,6 +1184,33 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
outputEstablished = true;
};
// What each swap step the user pays for spends of a token other than
// the input side's, as { token, amount }, in step order.
const otherTokensSpent = [];
// A swap step the user pays for (`payerIsUser` true), after the input
// side is set, takes `amount` of `token` from the sender. Of the input
// side's token it adds to the input side's amount, so the Amount line
// is what the steps spend of it together; when either amount is not a
// quantity, the total is not known and the amount is null. Of another
// token it goes in `otherTokensSpent`. A V2 exact-in of ALREADY_PAID
// takes nothing from the sender, and a permit's amount stays as the
// permit states it.
const addUserPaid = (token, amount) => {
if (amount === ALREADY_PAID) return;
const sameToken =
present(inputToken) &&
token.toLowerCase() === inputToken.toLowerCase();
if (!sameToken) {
otherTokensSpent.push({ token, amount });
return;
}
if (inputFromPermit) return;
inputAmount =
typeof inputAmount === "bigint" && typeof amount === "bigint"
? inputAmount + amount
: null;
};
// Who the steps pay, in whichever step and of whatever token.
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
// its own address, holds what it is paid until a later step pays it
@@ -1309,7 +1350,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// A permit states both halves itself, so it may replace an
// input side an earlier hop established without breaking
// the invariant.
if (p) setInput(p.token, p.amount);
if (p) {
setInput(p.token, p.amount);
inputFromPermit = true;
}
} else if (command === "PERMIT2_TRANSFER_FROM") {
const p = decodePermit2TransferFrom(inputs[i]);
if (!p) {
@@ -1375,7 +1419,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V3_SWAP_EXACT_IN") {
const s = decodeV3SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn);
}
// Always update output: in multi-step swaps (V3 → V4),
// the last swap step determines the final output token
// and minimum received amount.
@@ -1392,7 +1440,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V2_SWAP_EXACT_IN") {
const s = decodeV2SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn);
}
setOutput(s.tokenOut, s.amountOutMin);
if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, s.amountIn);
@@ -1408,7 +1460,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
hasV2ExactOut = true;
const s = decodeV2SwapExactOut(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountInMax);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountInMax);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountInMax);
}
setOutput(s.tokenOut, s.amountOut);
if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut);
@@ -1550,6 +1606,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// other half.
if (!inputEstablished) {
setInput(v4.tokenIn, v4.amountIn);
} else if (v4.payerIsUser) {
addUserPaid(v4.tokenIn, v4.amountIn);
}
outputToken = v4.tokenOut;
minOutput = v4.amountOutMin;
@@ -1672,37 +1730,43 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
});
}
if (present(inputAmount)) {
// Three amounts need no scale to describe and are named rather
// than formatted: V4's open delta and V2's already-paid zero,
// neither of which is a quantity at all (see OPEN_DELTA and
// ALREADY_PAID), and an unbounded permit. Those two tests come
// first — the sentinels are not bigints and cannot be compared
// with one.
let amount;
if (inputAmount === OPEN_DELTA) {
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
} else if (inputAmount === ALREADY_PAID) {
amount = {
// The text of an amount the swap steps spend, for the Amount line and
// the lines for other tokens below it. Three amounts need no scale to
// describe and are named rather than formatted: V4's open delta and
// V2's already-paid zero, neither of which is a quantity at all (see
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
// tests come first — the sentinels are not bigints and cannot be
// compared with one.
const spentText = (amount, info) => {
if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
}
if (amount === ALREADY_PAID) {
return {
raw: ALREADY_PAID_AMOUNT,
display: ALREADY_PAID_AMOUNT,
};
} else if (inputAmount >= MAX_UINT160) {
amount = { raw: "Unlimited", display: "Unlimited" };
} else if (hasV2ExactOut) {
}
if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" };
}
if (hasV2ExactOut) {
// A V2 exact-out swap spends at most this figure, whichever
// step set the line (its amountInMax, the WRAP_ETH of a swap
// paid in ETH, a permit), so it is said to be a maximum, in
// `raw` too: the wait, success and error screens show `raw` as
// the transaction's amount.
const most = amountText(inputAmount, inInfo);
amount = {
// steps gave it (an amountInMax, the WRAP_ETH of a swap paid
// in ETH, a permit, or several steps' total), so it is said to
// be a maximum, in `raw` too: the wait, success and error
// screens show `raw` as the transaction's amount.
const most = amountText(amount, info);
return {
raw: "Up to " + most.raw,
display: "Up to " + most.display,
};
} else {
amount = amountText(inputAmount, inInfo);
}
return amountText(amount, info);
};
if (present(inputAmount)) {
const amount = spentText(inputAmount, inInfo);
details.push({
label: "Amount",
value: amount.display,
@@ -1710,6 +1774,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
});
}
// What each swap step the user pays for spends of another token, in
// the shape of the lines below for a PERMIT2_TRANSFER_FROM. An amount
// this decoder did not read is said so.
for (const s of otherTokensSpent) {
details.push({
label: "Moved from the sender",
value: present(s.amount)
? spentText(s.amount, tokenInfo(s.token, sources)).display
: UNDECODED_CURRENCY,
});
details.push(tokenMovedLine("Token moved", s.token, sources));
details.push({
label: "Moved to",
value: "The swap step that spends it",
});
}
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
// where it goes, an address on a line of its own.
for (const m of movedFromSender) {
+188
View File
@@ -1289,6 +1289,194 @@ describe("uniswap decoder", () => {
expect(detail(result, "Amount").value).not.toContain("500000000000");
});
// https://git.eeqj.de/sneak/AutistMask/issues/625 — a route split across
// two swap steps the user pays for, with no PERMIT2_PERMIT, takes both
// steps' amounts from the wallet. On next at 671b4d3 the Amount line read
// only the first step's 600.0000 USDT.
describe("a swap split across steps the user pays for", () => {
const USDT_600 = 600000000n;
const USDT_400 = 400000000n;
// The lines from Amount on, as [label, value].
function linesFromAmount(result, count) {
const lines = result.details.map((d) => [d.label, d.value]);
const at = lines.findIndex(([label]) => label === "Amount");
return lines.slice(at, at + count);
}
test("a split across a V3 and a V2 step the user pays for shows the total", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
[
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
USDT_ADDR,
USDC_ADDR,
]),
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
USDT_ADDR,
USDC_ADDR,
]),
],
9999999999n,
);
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(detail(result, "Token In").address.toLowerCase()).toBe(
USDT_ADDR.toLowerCase(),
);
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Amount").rawValue).toBe("1000.0000");
expect(detail(result, "Moved from the sender")).toBeUndefined();
});
test("a split across a V3 step and a V4 step the user pays for shows the total", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
[
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
USDT_ADDR,
USDC_ADDR,
]),
encodeV4Swap(
new Uint8Array([
V4_SETTLE_ID,
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_TAKE_ID,
]),
[
encodeV4Settle(USDT_ADDR), // paid by the user
encodeV4ExactInSingle(
USDT_ADDR,
USDC_ADDR,
USDT_400,
390000000n,
),
encodeV4Take(USDC_ADDR),
],
),
],
9999999999n,
);
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Moved from the sender")).toBeUndefined();
});
test("a second step the user pays for that spends another token shows that token's amount", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
[
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
USDT_ADDR,
USDC_ADDR,
]),
encodeV2SwapExactIn(
USER_ADDR,
400000000000000000000n, // 400 DAI
390000000n,
[DAI_ADDR, USDC_ADDR],
),
],
9999999999n,
);
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(linesFromAmount(result, 4)).toEqual([
["Amount", "600.0000 USDT"],
["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"],
["Moved to", "The swap step that spends it"],
]);
});
// A PERMIT2_PERMIT that sets the input side keeps its own amount: the
// steps it lets the router spend do not add to it.
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
[
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), // 5000 USDT
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
USDT_ADDR,
USDC_ADDR,
]),
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
USDT_ADDR,
USDC_ADDR,
]),
],
9999999999n,
);
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(detail(result, "Amount").value).toBe("5000.0000 USDT");
expect(detail(result, "Moved from the sender")).toBeUndefined();
});
// A V2 exact-out step makes every amount the swap steps spend a maximum,
// the total included.
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {
const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x00, 0x09]),
[
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
USDT_ADDR,
USDC_ADDR,
]),
encodeV2SwapExactOut(USER_ADDR, 390000000n, USDT_400, [
USDT_ADDR,
USDC_ADDR,
]),
],
9999999999n,
);
const result = uniswap.decode(
data,
ROUTER_ADDR,
"0x1",
{},
USER_ADDR,
);
expect(result).not.toBeNull();
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
// the shape the issue measured: a V4 step that states a minimum of zero
// and names no output currency.
+14 -1
View File
@@ -671,6 +671,7 @@ describe("any token left in the router is not received", () => {
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
),
left: tokenLine(WETH, "WETH"),
spent: "2000.0000 USDT",
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
@@ -697,6 +698,7 @@ describe("any token left in the router is not received", () => {
v2Swap(0x08, MSG_SENDER, ONE_WETH, USDC_1990, [WETH, USDC]),
),
left: tokenLine(WETH, "WETH"),
spent: "1.0000 WETH",
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V3 swap of 1 WETH that the user pays for",
@@ -705,6 +707,7 @@ describe("any token left in the router is not received", () => {
v3SwapExactIn(MSG_SENDER, ONE_WETH, USDC_1990, WETH, USDC),
),
left: tokenLine(WETH, "WETH"),
spent: "1.0000 WETH",
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE of 1 WETH by the user",
@@ -717,6 +720,7 @@ describe("any token left in the router is not received", () => {
),
),
left: tokenLine(WETH, "WETH"),
spent: "1.0000 WETH",
},
{
kind: "a swap paying WETH to the router, a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH from the router",
@@ -792,6 +796,7 @@ describe("any token left in the router is not received", () => {
),
),
left: tokenLine(WETH, "WETH"),
spent: "1.0000 WETH",
},
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
// the decoder cannot read, so the SETTLE from the router pays nothing.
@@ -873,12 +878,17 @@ describe("any token left in the router is not received", () => {
),
left: { label: "Token moved into the router", value: "ETH" },
},
])("$kind", ({ data, left }) => {
])("$kind", ({ data, left, spent }) => {
const result = decode(data);
expect(labels(result)).toEqual([
"Protocol",
"Token In",
"Amount",
// A later swap step the user pays for spends `spent` of a token
// other than Token In (https://git.eeqj.de/sneak/AutistMask/issues/625).
...(spent
? ["Moved from the sender", "Token moved", "Moved to"]
: []),
"Token Out",
"Paid to",
"Token moved into the router",
@@ -886,6 +896,9 @@ describe("any token left in the router is not received", () => {
"Steps",
"Deadline",
]);
if (spent) {
expect(detail(result, "Moved from the sender").value).toBe(spent);
}
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
expect(detail(result, "Token moved into the router")).toEqual(left);
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");