1 Commits
Author SHA1 Message Date
sneak 5575e8cfc5 fix: a V4 step of chained swap actions shows what the user receives (closes #563)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A V4 step's input side is now the currency its SETTLE or SETTLE_ALL
names, or else the one currency that some swap action spends and none
buys; its output side is the currency TAKE or TAKE_ALL names, or else
the one that some swap action buys and none spends. Amount and Min.
received come from the actions that spend the input and buy the output,
summed when several exact-in actions each state one. A step with an
unread swap action is named only by SETTLE, SETTLE_ALL, TAKE and
TAKE_ALL, and shows no figure. Before, the first swap action set both
sides, SETTLE_ALL and TAKE_ALL were not read, and a SWAP_EXACT_OUT was
read as spending its path's last currency.

Model: opus-5-5
2026-10-18 21:59:23 +00:00
3 changed files with 330 additions and 288 deletions
+24 -26
View File
@@ -1164,9 +1164,8 @@ read:
amount of one step that names an input token or amount: the last amount of one step that names an input token or amount: the last
`PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in, `PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in,
V2 exact-out, `WRAP_ETH` or V4 swap step. A V2 exact-out step gives its V2 exact-out, `WRAP_ETH` or V4 swap step. A V2 exact-out step gives its
`amountInMax`, and a V4 swap step the `amountIn` of the first of its swap `amountInMax`, and a V4 swap step the `amountIn` of the swap action that
actions as they are lined up below, or none when that action is exact-out, spends its input currency, as described below.
could not be read or does not spend the step's input currency.
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's - `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. `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 The router reads that zero as "the pair already holds the input tokens": the
@@ -1188,29 +1187,28 @@ The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
reads `Unknown (not named in the calldata)` when the decoder found no token for reads `Unknown (not named in the calldata)` when the decoder found no token for
that side, or `Unknown (could not be decoded)` instead when a V4 swap action in that side, or `Unknown (could not be decoded)` instead when a V4 swap action in
the transaction could not be read (see Transaction Decoding below). A V4 step's the transaction could not be read (see Transaction Decoding below). A V4 step's
input side is the currency its `SETTLE` or `SETTLE_ALL` names, and its output sides follow three rules:
side the currency its `TAKE` or `TAKE_ALL` names. A side none of them names
comes from the step's swap actions, lined up as the exact-out actions in reverse 1. Its input side is the currency its `SETTLE` or `SETTLE_ALL` names, or else
order, then the exact-in actions in the order they appear: the input side from the one currency that some swap action in the step spends and none buys. Its
the first action in that line, the output side from the last. The router runs output side is the currency its `TAKE` or `TAKE_ALL` names, or else the one
exact-in actions from the input end and a chain of exact-out actions from the currency that some swap action buys and none spends. If there is not exactly
output end, so a step whose swap actions are all of one kind is lined up from one such currency, that side is unknown. A step that buys WETH with USDT and
what the user pays to what the user receives. A step that mixes the two kinds USDC with that WETH, with nothing naming its sides, reads `Swap USDT → USDC`.
need not be: one that buys WETH with USDT exact-in and then USDC with that WETH 2. `Amount` is the `amountIn` of the swap action that spends the input currency.
exact-out is lined up with the USDC purchase first, so with no `SETTLE`, If several exact-in actions spend it, each with a stated amount, it is their
`SETTLE_ALL`, `TAKE` or `TAKE_ALL` it reads `Swap WETH → WETH`. `Amount` is the sum; in any other case there is no `Amount` line. The decoder reads no amount
first action's `amountIn`, shown only when that action spends the step's input from an exact-out action, and an open delta is not a stated amount.
currency, and `Min. received` the last action's `amountOutMin`, shown only when `Min. received` is found the same way, from the `amountOutMin` of the actions
that action buys the step's output currency, so no figure is shown against a that buy the output currency.
currency it is not counted in. The decoder reads no amount from an exact-out 3. If any swap action in the step could not be read, only `SETTLE`,
action. That mixed step, ending in `SETTLE_ALL` USDT and `TAKE_ALL` USDC, reads `SETTLE_ALL`, `TAKE` and `TAKE_ALL` name its sides, and there is no `Amount`
`Swap USDT → USDC` with neither figure, since its first action in the line is or `Min. received` line. Such a step still sets each side as a swap step
exact-out and its last buys WETH. A swap action that could not be read keeps its does, and no other step's token or figure is shown for a side they did not
place in the line and states nothing, so a side or figure that would come from name.
it is unknown, while one from a readable action is shown. Such a step still sets
each side as a swap step does, and no other step's token or figure is shown for An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
an unknown one. An `UNWRAP_WETH` step makes `Token Out` ETH only when the output on mainnet or Sepolia, or when no step set the output side; a WETH
side is WETH, on mainnet or Sepolia, or when no step set the output side; a WETH
`Min. received` figure then reads in ETH. Otherwise `Token Out` and `Min. received` figure then reads in ETH. Otherwise `Token Out` and
`Min. received` keep the output side's own token and figure, whether the swap `Min. received` keep the output side's own token and figure, whether the swap
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
+85 -56
View File
@@ -320,15 +320,6 @@ function decodeExactly(types, data) {
return decoded; return decoded;
} }
// What decodeV4Swap() knows of a swap action it could not read, and of the
// first and last swap action of a step that has none.
const NO_SWAP_READ = {
tokenIn: null,
tokenOut: null,
amountIn: null,
amountOutMin: null,
};
// Decode one V4 swap action's parameters into the currency it spends // Decode one V4 swap action's parameters into the currency it spends
// (`tokenIn`), the currency it buys (`tokenOut`) and, for an exact-in action, // (`tokenIn`), the currency it buys (`tokenOut`) and, for an exact-in action,
// its `amountIn` and `amountOutMin`. The exact-out actions decode no amounts. // its `amountIn` and `amountOutMin`. The exact-out actions decode no amounts.
@@ -411,25 +402,44 @@ function decodeV4SwapAction(actionId, param) {
}; };
} }
// The currencies in `list` that `other` does not hold, each listed once.
function currenciesNotIn(list, other) {
return [...new Set(list.filter((c) => !other.includes(c)))];
}
// The amount that the swap actions on one side of a V4 step state together:
// the one action's own amount, or the sum when there are several and each
// states a quantity. Exact-out actions state none here, and the open delta is
// not a quantity. Otherwise null.
function totalAmount(amounts) {
if (amounts.length === 1) return amounts[0];
if (amounts.length === 0) return null;
if (!amounts.every((a) => typeof a === "bigint")) return null;
return amounts.reduce((sum, a) => sum + a, 0n);
}
// Decode V4_SWAP (command 0x10) input bytes. // Decode V4_SWAP (command 0x10) input bytes.
// The input is ABI-encoded as (bytes actions, bytes[] params). // The input is ABI-encoded as (bytes actions, bytes[] params). Each swap
// SETTLE or SETTLE_ALL names the step's input currency, and TAKE or TAKE_ALL // action gives the currency it spends and the currency it buys (see
// its output currency, wherever in the step they stand. A side neither one // decodeV4SwapAction()). The step's sides then follow three rules:
// names is read from the swap actions, lined up as the exact-out actions in //
// reverse order, then the exact-in actions in the order they appear. The // 1. Token In is the currency SETTLE or SETTLE_ALL names; otherwise the one
// input comes from the first action in that line and the output from the // currency that some swap action spends and none buys. Token Out is the
// last. The router runs exact-in actions from the input end and a chain of // currency TAKE or TAKE_ALL names; otherwise the one currency that some
// exact-out actions from the output end, so a step whose swap actions are all // swap action buys and none spends. If there is not exactly one such
// of one kind is lined up from what the user pays to what the user receives. // currency, that side is unknown.
// A step that mixes the two kinds need not be: one that buys WETH with USDT // 2. Amount is the amountIn of the swap action that spends Token In. If
// exact-in and then USDC with that WETH exact-out is lined up with the USDC // several exact-in actions spend it, each with a stated amount, it is
// purchase first, so with no SETTLE, SETTLE_ALL, TAKE or TAKE_ALL both its // their sum; in any other case there is none. Min. received is the same
// sides are WETH. Amount is the first action's amountIn, kept only when that // for amountOutMin and the actions that buy Token Out.
// action spends the step's input currency, and Min. received the last // 3. If any swap action in the step cannot be read, only SETTLE, SETTLE_ALL,
// action's amountOutMin, kept only when that action buys the step's output // TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
// currency, so a figure is never shown against a currency other than the one // received.
// it is counted in. A swap action that could not be read keeps its place in //
// the line and states nothing. // A currency a swap action does not name, such as what an exact-in action
// with an empty path buys, is null and counts in rules 1 and 2 as a currency
// like any other: a step of only that action has an unknown Token Out and
// the action's amountOutMin as Min. received.
function decodeV4Swap(input) { function decodeV4Swap(input) {
try { try {
const d = coder.decode(["bytes", "bytes[]"], input); const d = coder.decode(["bytes", "bytes[]"], input);
@@ -438,8 +448,8 @@ function decodeV4Swap(input) {
let settleToken = null; let settleToken = null;
let takeToken = null; let takeToken = null;
// The step's swap actions, as decodeV4SwapAction() reads them, lined // The step's swap actions that could be read, as decodeV4SwapAction()
// up as the comment above says. // reads them.
const swaps = []; const swaps = [];
// Set when a swap action's parameters are not in the deployed shape // Set when a swap action's parameters are not in the deployed shape
// (see decodeExactly()), so nothing was read from that action. SETTLE, // (see decodeExactly()), so nothing was read from that action. SETTLE,
@@ -477,39 +487,58 @@ function decodeV4Swap(input) {
actionId === V4_SWAP_EXACT_OUT_SINGLE || actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT actionId === V4_SWAP_EXACT_OUT
) { ) {
let swap = NO_SWAP_READ;
try { try {
swap = decodeV4SwapAction(actionId, params[i]); swaps.push(decodeV4SwapAction(actionId, params[i]));
} catch { } catch {
swapUndecoded = true; swapUndecoded = true;
} }
// An exact-out action goes in front of the swap actions
// before it, an exact-in one after them.
if (
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT
) {
swaps.unshift(swap);
} else {
swaps.push(swap);
}
} }
} catch { } catch {
// Skip sub-actions we can't decode // Skip sub-actions we can't decode
} }
} }
const first = swaps.length > 0 ? swaps[0] : NO_SWAP_READ; // Rule 3.
const last = swaps.length > 0 ? swaps[swaps.length - 1] : NO_SWAP_READ; if (swapUndecoded) {
const tokenIn = present(settleToken) ? settleToken : first.tokenIn; return {
const tokenOut = present(takeToken) ? takeToken : last.tokenOut; tokenIn: settleToken,
return { tokenOut: takeToken,
tokenIn, amountIn: null,
tokenOut, amountOutMin: null,
amountIn: first.tokenIn === tokenIn ? first.amountIn : null, swapUndecoded,
amountOutMin: last.tokenOut === tokenOut ? last.amountOutMin : null, };
swapUndecoded, }
};
const spent = swaps.map((s) => s.tokenIn);
const bought = swaps.map((s) => s.tokenOut);
// Rules 1 and 2 for Token In.
let tokenIn = null;
let amountIn = null;
const spentOnly = currenciesNotIn(spent, bought);
if (present(settleToken) || spentOnly.length === 1) {
tokenIn = present(settleToken) ? settleToken : spentOnly[0];
amountIn = totalAmount(
swaps
.filter((s) => s.tokenIn === tokenIn)
.map((s) => s.amountIn),
);
}
// Rules 1 and 2 for Token Out.
let tokenOut = null;
let amountOutMin = null;
const boughtOnly = currenciesNotIn(bought, spent);
if (present(takeToken) || boughtOnly.length === 1) {
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
amountOutMin = totalAmount(
swaps
.filter((s) => s.tokenOut === tokenOut)
.map((s) => s.amountOutMin),
);
}
return { tokenIn, tokenOut, amountIn, amountOutMin, swapUndecoded };
} catch { } catch {
return null; return null;
} }
@@ -640,10 +669,10 @@ function decode(data, toAddress, sources) {
if (v4 && v4.swapUndecoded) { if (v4 && v4.swapUndecoded) {
// A swap action of this step went unread, so the step // A swap action of this step went unread, so the step
// is marked and still sets both sides as a swap step // is marked and still sets both sides as a swap step
// does. A currency or figure that neither SETTLE, // does, with no figure. A side its SETTLE, SETTLE_ALL,
// SETTLE_ALL, TAKE, TAKE_ALL nor a readable swap // TAKE or TAKE_ALL did not name is set undetermined,
// action gave decodeV4Swap() is set undetermined, not // not left to an earlier or later step's token and
// left to an earlier or later step's token and figure. // figure.
swapUndecoded = true; swapUndecoded = true;
commandNames[i] += " (could not be decoded)"; commandNames[i] += " (could not be decoded)";
if (!inputEstablished) { if (!inputEstablished) {
+221 -206
View File
@@ -303,6 +303,12 @@ function encodeV4TakeAll(currency, minAmount) {
return coder.encode(["address", "uint256"], [currency, minAmount]); return coder.encode(["address", "uint256"], [currency, minAmount]);
} }
// V4 CLOSE_CURRENCY (currency), which the decoder does not read.
const V4_CLOSE_CURRENCY_ID = 0x12;
function encodeV4CloseCurrency(currency) {
return coder.encode(["address"], [currency]);
}
function detail(result, label) { function detail(result, label) {
return result.details.find((d) => d.label === label); return result.details.find((d) => d.label === label);
} }
@@ -1159,11 +1165,14 @@ describe("uniswap decoder", () => {
expect(detail(result, "Min. received").value).toBe("0.9900 USDC"); expect(detail(result, "Min. received").value).toBe("0.9900 USDC");
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/563 — one V4 step of two // https://git.eeqj.de/sneak/AutistMask/issues/563 — a V4 step of several
// chained single-pool exact-in swap actions, USDT to WETH and then WETH to // swap actions. The screen showed the first action's output as Token Out.
// USDC. The user pays USDT and receives USDC, but the screen showed the // Every step below pays USDT and receives USDC, so it reads Swap USDT →
// first action's WETH as Token Out and its minimum as Min. received. // USDC whichever kind of swap actions it holds and in whatever order they
test.each([ // are written. Each is tried with three endings: SETTLE_ALL and TAKE_ALL,
// SETTLE and TAKE, and CLOSE_CURRENCY for each currency, which leaves the
// sides to the swap actions alone.
const V4_STEP_ENDINGS = [
{ {
ending: "SETTLE_ALL and TAKE_ALL", ending: "SETTLE_ALL and TAKE_ALL",
ids: [V4_SETTLE_ALL_ID, V4_TAKE_ALL_ID], ids: [V4_SETTLE_ALL_ID, V4_TAKE_ALL_ID],
@@ -1178,41 +1187,162 @@ describe("uniswap decoder", () => {
params: [encodeV4Settle(USDT_ADDR), encodeV4Take(USDC_ADDR)], params: [encodeV4Settle(USDT_ADDR), encodeV4Take(USDC_ADDR)],
}, },
{ {
// Only the swap actions name the currencies. ending: "CLOSE_CURRENCY",
ending: "nothing", ids: [
ids: [], V4_CLOSE_CURRENCY_ID,
params: [], V4_CLOSE_CURRENCY_ID,
V4_CLOSE_CURRENCY_ID,
],
params: [
encodeV4CloseCurrency(USDT_ADDR),
encodeV4CloseCurrency(WETH_ADDR),
encodeV4CloseCurrency(USDC_ADDR),
],
}, },
])( ];
"two chained V4 swap actions followed by $ending show the first one's input and the last one's output", // 1,000 USDT for at least 0.5 WETH
({ ids, params }) => { const USDT_FOR_WETH_IN = {
id: V4_SWAP_EXACT_IN_SINGLE_ID,
param: encodeV4ExactInSingle(
USDT_ADDR,
WETH_ADDR,
1000000000n,
500000000000000000n,
),
};
// 0.5 WETH for at least 990 USDC
const WETH_FOR_USDC_IN = {
id: V4_SWAP_EXACT_IN_SINGLE_ID,
param: encodeV4ExactInSingle(
WETH_ADDR,
USDC_ADDR,
500000000000000000n,
990000000n,
),
};
// exactly 0.5 WETH for at most 1,000 USDT
const USDT_FOR_WETH_OUT = {
id: V4_SWAP_EXACT_OUT_SINGLE_ID,
param: encodeV4ExactOutSingle(
USDT_ADDR,
WETH_ADDR,
500000000000000000n,
1000000000n,
),
};
// exactly 990 USDC for at most 0.5 WETH
const WETH_FOR_USDC_OUT = {
id: V4_SWAP_EXACT_OUT_SINGLE_ID,
param: encodeV4ExactOutSingle(
WETH_ADDR,
USDC_ADDR,
990000000n,
500000000000000000n,
),
};
// 400 USDT for at least 396 USDC
const USDT_FOR_USDC_IN = {
id: V4_SWAP_EXACT_IN_SINGLE_ID,
param: encodeV4ExactInSingle(
USDT_ADDR,
USDC_ADDR,
400000000n,
396000000n,
),
};
// 600 USDT through WETH for at least 594 USDC
const USDT_FOR_USDC_THROUGH_WETH_IN = {
id: V4_SWAP_EXACT_IN,
param: encodeV4ExactIn(
USDT_ADDR,
[WETH_ADDR, USDC_ADDR],
600000000n,
594000000n,
),
};
// the open delta of USDT through WETH for at least 594 USDC
const USDT_OPEN_DELTA_FOR_USDC_THROUGH_WETH_IN = {
id: V4_SWAP_EXACT_IN,
param: encodeV4ExactIn(
USDT_ADDR,
[WETH_ADDR, USDC_ADDR],
0n,
594000000n,
),
};
// Amount is the amountIn of the action that spends USDT, or the sum when
// several exact-in actions each state one. Min. received is the same for
// the actions that buy USDC. The decoder reads no amount from an exact-out
// action, and an open delta is not a stated amount.
const V4_STEPS = [
{
step: "USDT for WETH exact-in, then WETH for USDC exact-in",
swaps: [USDT_FOR_WETH_IN, WETH_FOR_USDC_IN],
amount: "1000.0000 USDT",
min: "990.0000 USDC",
},
{
step: "WETH for USDC exact-in, then USDT for WETH exact-in",
swaps: [WETH_FOR_USDC_IN, USDT_FOR_WETH_IN],
amount: "1000.0000 USDT",
min: "990.0000 USDC",
},
{
step: "WETH for USDC exact-out, then USDT for WETH exact-out",
swaps: [WETH_FOR_USDC_OUT, USDT_FOR_WETH_OUT],
amount: undefined,
min: undefined,
},
{
step: "USDT for WETH exact-out, then WETH for USDC exact-out",
swaps: [USDT_FOR_WETH_OUT, WETH_FOR_USDC_OUT],
amount: undefined,
min: undefined,
},
{
step: "USDT for WETH exact-in, then WETH for USDC exact-out",
swaps: [USDT_FOR_WETH_IN, WETH_FOR_USDC_OUT],
amount: "1000.0000 USDT",
min: undefined,
},
{
step: "WETH for USDC exact-out, then USDT for WETH exact-in",
swaps: [WETH_FOR_USDC_OUT, USDT_FOR_WETH_IN],
amount: "1000.0000 USDT",
min: undefined,
},
{
step: "USDT for WETH exact-out, then WETH for USDC exact-in",
swaps: [USDT_FOR_WETH_OUT, WETH_FOR_USDC_IN],
amount: undefined,
min: "990.0000 USDC",
},
{
step: "USDT for USDC, then USDT for USDC through WETH, both exact-in",
swaps: [USDT_FOR_USDC_IN, USDT_FOR_USDC_THROUGH_WETH_IN],
amount: "1000.0000 USDT",
min: "990.0000 USDC",
},
{
step: "USDT for USDC, then the open delta of USDT for USDC through WETH, both exact-in",
swaps: [USDT_FOR_USDC_IN, USDT_OPEN_DELTA_FOR_USDC_THROUGH_WETH_IN],
amount: undefined,
min: "990.0000 USDC",
},
];
test.each(
V4_STEPS.flatMap((step) =>
V4_STEP_ENDINGS.map((ending) => ({ ...step, ...ending })),
),
)(
"a V4 step of $step, then $ending, reads Swap USDT → USDC",
({ swaps, ids, params, amount, min }) => {
const data = buildExecute( const data = buildExecute(
"0x10", "0x10",
[ [
encodeV4Swap( encodeV4Swap(
new Uint8Array([ new Uint8Array([...swaps.map((s) => s.id), ...ids]),
V4_SWAP_EXACT_IN_SINGLE_ID, [...swaps.map((s) => s.param), ...params],
V4_SWAP_EXACT_IN_SINGLE_ID,
...ids,
]),
[
// 1,000 USDT for at least 0.4 WETH
encodeV4ExactInSingle(
USDT_ADDR,
WETH_ADDR,
1000000000n,
400000000000000000n,
),
// all of that WETH (the open delta) for at least
// 990 USDC
encodeV4ExactInSingle(
WETH_ADDR,
USDC_ADDR,
0n,
990000000n,
),
...params,
],
), ),
], ],
9999999999n, 9999999999n,
@@ -1221,9 +1351,63 @@ describe("uniswap decoder", () => {
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR);
expect(result.name).toBe("Swap USDT → USDC"); expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR); expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Token Out").address).toBe(USDC_ADDR); expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Min. received").value).toBe("990.0000 USDC"); expect(detail(result, "Amount")?.value).toBe(amount);
expect(detail(result, "Min. received")?.value).toBe(min);
},
);
// A step with a swap action that could not be read is named only by its
// SETTLE_ALL, TAKE_ALL, SETTLE and TAKE, and states no figure: its readable
// USDT for WETH action names neither side. The unread action is
// V4_SWAP_ACTION_SHAPES[0].newer, WETH for USDC exact-in in the newer
// shape.
test.each([
{
...V4_STEP_ENDINGS[0],
tokenIn: `USDT (${USDT_ADDR})`,
tokenOut: `USDC (${USDC_ADDR})`,
},
{
...V4_STEP_ENDINGS[1],
tokenIn: `USDT (${USDT_ADDR})`,
tokenOut: `USDC (${USDC_ADDR})`,
},
{
...V4_STEP_ENDINGS[2],
tokenIn: "Unknown (could not be decoded)",
tokenOut: "Unknown (could not be decoded)",
},
])(
"a V4 step of a readable swap action and an unread one, then $ending, shows no figure",
({ ids, params, tokenIn, tokenOut }) => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
USDT_FOR_WETH_IN.id,
V4_SWAP_ACTION_SHAPES[0].id,
...ids,
]),
[
USDT_FOR_WETH_IN.param,
V4_SWAP_ACTION_SHAPES[0].newer,
...params,
],
),
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
expect(detail(result, "Token In").value).toBe(tokenIn);
expect(detail(result, "Token Out").value).toBe(tokenOut);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
}, },
); );
@@ -1261,175 +1445,6 @@ describe("uniswap decoder", () => {
expect(detail(result, "Token Out").address).toBe(USDC_ADDR); expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
}); });
// The router runs a chain of exact-out actions from the output end: the
// first buys the USDC the user receives, owing WETH, and the second buys
// that WETH (the open delta) with the USDT the user pays.
test("two chained V4 exact-out actions show the last one's input and the first one's output", () => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SWAP_EXACT_OUT_SINGLE_ID,
]),
[
// exactly 990 USDC for at most 0.5 WETH
encodeV4ExactOutSingle(
WETH_ADDR,
USDC_ADDR,
990000000n,
500000000000000000n,
),
// exactly that WETH (the open delta) for at most
// 1,000 USDT
encodeV4ExactOutSingle(
USDT_ADDR,
WETH_ADDR,
0n,
1000000000n,
),
],
),
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
});
// An exact-in action buys WETH with USDT and an exact-out action then buys
// USDC with that WETH. The exact-out action is lined up first, so the last
// action in the line is the exact-in one, and its minimum is counted in
// WETH, not in the USDC the user receives. Shown as Min. received, it would
// read "400000000000.0000 USDC".
test("a V4 exact-in action, then an exact-out one that spends its output, shows no WETH figure as Min. received", () => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ALL_ID,
V4_TAKE_ALL_ID,
0x13, // CLEAR_OR_TAKE, which the decoder does not read
]),
[
// 1,000 USDT for at least 0.4 WETH
encodeV4ExactInSingle(
USDT_ADDR,
WETH_ADDR,
1000000000n,
400000000000000000n,
),
// exactly 990 USDC for at most 0.5 WETH
encodeV4ExactOutSingle(
WETH_ADDR,
USDC_ADDR,
990000000n,
500000000000000000n,
),
encodeV4SettleAll(USDT_ADDR, 1000000000n),
encodeV4TakeAll(USDC_ADDR, 990000000n),
// the WETH left over: (address currency, uint256 amountMax)
coder.encode(
["address", "uint256"],
[WETH_ADDR, 100000000000000000n],
),
],
),
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
});
// A swap action that could not be read still stands first or last in its
// step, so the side and figure it would have given are unknown, while
// those from the readable action are shown. The unread action is
// V4_SWAP_ACTION_SHAPES[0].newer, a SWAP_EXACT_IN_SINGLE of WETH to USDC.
test("a readable V4 swap action, then an unread one, shows the first one's input and no output", () => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SWAP_EXACT_IN_SINGLE_ID,
]),
[
encodeV4ExactInSingle(
USDT_ADDR,
WETH_ADDR,
1000000000n,
400000000000000000n,
),
V4_SWAP_ACTION_SHAPES[0].newer,
],
),
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Token Out").value).toBe(
"Unknown (could not be decoded)",
);
expect(detail(result, "Min. received")).toBeUndefined();
});
test("an unread V4 swap action, then a readable one, shows the last one's output and no input", () => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SWAP_EXACT_IN_SINGLE_ID,
]),
[
V4_SWAP_ACTION_SHAPES[0].newer,
encodeV4ExactInSingle(
USDC_ADDR,
DAI_ADDR,
0n,
990000000000000000000n,
),
],
),
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
expect(detail(result, "Token In").value).toBe(
"Unknown (could not be decoded)",
);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Token Out").address).toBe(DAI_ADDR);
expect(detail(result, "Min. received").value).toBe("990.0000 DAI");
});
// https://git.eeqj.de/sneak/AutistMask/issues/415 — the router's V2 // https://git.eeqj.de/sneak/AutistMask/issues/415 — the router's V2
// exact-in reads an amountIn of zero as universal-router // exact-in reads an amountIn of zero as universal-router
// Constants.ALREADY_PAID: an earlier step sent the tokens to the pair, and // Constants.ALREADY_PAID: an earlier step sent the tokens to the pair, and