1 Commits
Author SHA1 Message Date
sneak f4f2c549f5 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, and its output side the currency its TAKE or TAKE_ALL names. A
side none of them names comes from the swap actions, put in the order
the currencies pass through them: each exact-in action after the ones
before it, each exact-out action in front of them, since the router runs
exact-out actions from the output end. The input and Amount come from
the first action in that order, the output and Min. received from the
last. SETTLE_ALL and TAKE_ALL were not read before, and the first swap
action set both sides, so a chain showed the first one's output.

Model: opus-5-5
2026-10-18 19:35:29 +00:00
3 changed files with 405 additions and 144 deletions
+26 -13
View File
@@ -1164,8 +1164,9 @@ 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 its first readable `amountInMax`, and a V4 swap step the `amountIn` of the swap action that
exact-in action. spends what the user pays (see below), or none when that action is exact-out
or could not be read.
- `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
@@ -1186,17 +1187,29 @@ read:
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each 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 the transaction could not be read (see Transaction Decoding below). A V4 step's
whose swap action could not be read still sets each side as a swap step does, input side is the currency its `SETTLE` or `SETTLE_ALL` names, and its output
and a side its `SETTLE` or `TAKE` did not name is then unknown: no other step's side the currency its `TAKE` or `TAKE_ALL` names. A side none of them names
token or figure is shown for it. An `UNWRAP_WETH` step makes `Token Out` ETH comes from the step's swap actions, put in the order the currencies pass through
only when the output side is WETH, on mainnet or Sepolia, or when no step set them, from what the user pays to what the user receives. The router runs
the output side; a WETH `Min. received` figure then reads in ETH. Otherwise exact-in actions from the input end, so each one goes after the actions before
`Token Out` and `Min. received` keep the output side's own token and figure, it, whose output it spends. It runs exact-out actions from the output end, so
whether the swap was paid in ETH or in a token: a V2 exact-out swap that buys the first one buys what the user receives and each later one goes in front of
USDC and then unwraps the WETH it did not spend shows USDC. The token permission the actions before it, buying what they spend. The input side and `Amount` come
warning on the signature screen has its own amount wording, including `Unknown`; from the first action in that order, and the output side and `Min. received`
the SignApproval section below describes it. from the last; the decoder reads no amount from an exact-out action. A swap
action that could not be read keeps its place in that order and states nothing,
so a side or figure that would come from 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 unknown one. An
`UNWRAP_WETH` step makes `Token Out` ETH only when the output 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` 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 unwraps the WETH it did not
spend shows USDC. The token permission warning on the signature screen has its
own amount wording, including `Unknown`; the SignApproval section below
describes it.
The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g. The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g.
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00 `2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
+140 -111
View File
@@ -290,7 +290,9 @@ const V4_SWAP_EXACT_IN = 0x07;
const V4_SWAP_EXACT_OUT_SINGLE = 0x08; const V4_SWAP_EXACT_OUT_SINGLE = 0x08;
const V4_SWAP_EXACT_OUT = 0x09; const V4_SWAP_EXACT_OUT = 0x09;
const V4_SETTLE = 0x0b; const V4_SETTLE = 0x0b;
const V4_SETTLE_ALL = 0x0c;
const V4_TAKE = 0x0e; const V4_TAKE = 0x0e;
const V4_TAKE_ALL = 0x0f;
// A V4 exact-in `amountIn`, read the way V4Router reads it: zero is // A V4 exact-in `amountIn`, read the way V4Router reads it: zero is
// ActionConstants.OPEN_DELTA, not a quantity of zero. See the OPEN_DELTA // ActionConstants.OPEN_DELTA, not a quantity of zero. See the OPEN_DELTA
@@ -318,10 +320,107 @@ 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
// (`tokenIn`), the currency it buys (`tokenOut`) and, for an exact-in action,
// its `amountIn` and `amountOutMin`. The exact-out actions decode no amounts.
// A field the action does not state is null. Throws when the parameters are
// not in the deployed shape (see decodeExactly()).
function decodeV4SwapAction(actionId, param) {
if (actionId === V4_SWAP_EXACT_IN) {
// ExactInputParams: (address currencyIn,
// tuple(address,uint24,int24,address,bytes)[] path,
// uint128 amountIn, uint128 amountOutMin)
const s = decodeExactly(
[
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
],
param,
);
const path = s[0][1];
return {
tokenIn: s[0][0],
tokenOut: path.length > 0 ? path[path.length - 1][0] : null,
amountIn: v4ExactInAmount(s[0][2]),
amountOutMin: s[0][3],
};
}
if (actionId === V4_SWAP_EXACT_IN_SINGLE) {
// ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
// bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
const s = decodeExactly(
[
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
],
param,
);
const poolKey = s[0][0];
const zeroForOne = s[0][1];
return {
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
amountIn: v4ExactInAmount(s[0][2]),
amountOutMin: s[0][3],
};
}
if (actionId === V4_SWAP_EXACT_OUT) {
// ExactOutputParams: (address currencyOut,
// tuple(address,uint24,int24,address,bytes)[] path,
// uint128 amountOut, uint128 amountInMaximum)
const s = decodeExactly(
[
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
],
param,
);
const path = s[0][1];
return {
tokenIn: path.length > 0 ? path[path.length - 1][0] : null,
tokenOut: s[0][0],
amountIn: null,
amountOutMin: null,
};
}
// V4_SWAP_EXACT_OUT_SINGLE. ExactOutputSingleParams: (poolKey as above,
// bool zeroForOne, uint128 amountOut, uint128 amountInMaximum,
// bytes hookData)
const s = decodeExactly(
[
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
],
param,
);
const poolKey = s[0][0];
const zeroForOne = s[0][1];
return {
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
amountIn: null,
amountOutMin: null,
};
}
// 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).
// We extract token addresses from SETTLE (input) and TAKE (output) sub-actions, // SETTLE or SETTLE_ALL names the step's input currency, and TAKE or TAKE_ALL
// and swap amounts from the swap sub-actions. // its output currency, wherever in the step they stand. A side neither one
// names is read from the swap actions, put in the order the currencies pass
// through them, from what the user pays to what the user receives. The router
// runs exact-in actions from the input end: each one spends what the actions
// before it bought, so it goes after them. It runs exact-out actions from the
// output end: the first one buys what the user receives, and each later one
// buys what the actions before it spend, so it goes in front of them. The
// input and Amount come from the first action in that order, and the output
// and Min. received from the last. A swap action that could not be read keeps
// its place and states nothing.
function decodeV4Swap(input) { function decodeV4Swap(input) {
try { try {
const d = coder.decode(["bytes", "bytes[]"], input); const d = coder.decode(["bytes", "bytes[]"], input);
@@ -330,11 +429,12 @@ function decodeV4Swap(input) {
let settleToken = null; let settleToken = null;
let takeToken = null; let takeToken = null;
let amountIn = null; // The step's swap actions, as decodeV4SwapAction() reads them, from
let amountOutMin = null; // what the user pays to what the user receives.
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,
// and TAKE may still name both currencies. // SETTLE_ALL, TAKE and TAKE_ALL may still name both currencies.
let swapUndecoded = false; let swapUndecoded = false;
for (let i = 0; i < actions.length; i++) { for (let i = 0; i < actions.length; i++) {
@@ -347,6 +447,10 @@ function decodeV4Swap(input) {
params[i], params[i],
); );
settleToken = s[0]; settleToken = s[0];
} else if (actionId === V4_SETTLE_ALL) {
// SETTLE_ALL: (address currency, uint256 maxAmount)
const s = coder.decode(["address", "uint256"], params[i]);
settleToken = s[0];
} else if (actionId === V4_TAKE) { } else if (actionId === V4_TAKE) {
// TAKE: (address currency, address recipient, uint256 amount) // TAKE: (address currency, address recipient, uint256 amount)
const t = coder.decode( const t = coder.decode(
@@ -354,107 +458,29 @@ function decodeV4Swap(input) {
params[i], params[i],
); );
takeToken = t[0]; takeToken = t[0];
} else if (actionId === V4_TAKE_ALL) {
// TAKE_ALL: (address currency, uint256 minAmount)
const t = coder.decode(["address", "uint256"], params[i]);
takeToken = t[0];
} else if ( } else if (
actionId === V4_SWAP_EXACT_IN_SINGLE ||
actionId === V4_SWAP_EXACT_IN || actionId === V4_SWAP_EXACT_IN ||
actionId === V4_SWAP_EXACT_IN_SINGLE actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT
) { ) {
// Extract amounts from exact-in swap actions let swap = NO_SWAP_READ;
if (actionId === V4_SWAP_EXACT_IN) { try {
// ExactInputParams: (address currencyIn, swap = decodeV4SwapAction(actionId, params[i]);
// tuple(address,uint24,int24,address,bytes)[] path, } catch {
// uint128 amountIn, uint128 amountOutMin) swapUndecoded = true;
try {
const s = decodeExactly(
[
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
],
params[i],
);
if (!present(settleToken)) settleToken = s[0][0];
const path = s[0][1];
if (path.length > 0 && !present(takeToken)) {
takeToken = path[path.length - 1][0];
}
if (!present(amountIn)) {
amountIn = v4ExactInAmount(s[0][2]);
}
if (!present(amountOutMin)) {
amountOutMin = s[0][3];
}
} catch {
swapUndecoded = true;
}
} else {
// ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
// bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
try {
const s = decodeExactly(
[
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
],
params[i],
);
const poolKey = s[0][0];
const zeroForOne = s[0][1];
if (!present(settleToken))
settleToken = zeroForOne
? poolKey[0]
: poolKey[1];
if (!present(takeToken))
takeToken = zeroForOne
? poolKey[1]
: poolKey[0];
if (!present(amountIn)) {
amountIn = v4ExactInAmount(s[0][2]);
}
if (!present(amountOutMin)) {
amountOutMin = s[0][3];
}
} catch {
swapUndecoded = true;
}
} }
} else if ( if (
actionId === V4_SWAP_EXACT_OUT || actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT_SINGLE actionId === V4_SWAP_EXACT_OUT
) { ) {
if (actionId === V4_SWAP_EXACT_OUT) { swaps.unshift(swap);
try {
const s = decodeExactly(
[
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
],
params[i],
);
if (!present(takeToken)) takeToken = s[0][0];
const path = s[0][1];
if (path.length > 0 && !present(settleToken)) {
settleToken = path[path.length - 1][0];
}
} catch {
swapUndecoded = true;
}
} else { } else {
try { swaps.push(swap);
const s = decodeExactly(
[
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
],
params[i],
);
const poolKey = s[0][0];
const zeroForOne = s[0][1];
if (!present(settleToken))
settleToken = zeroForOne
? poolKey[0]
: poolKey[1];
if (!present(takeToken))
takeToken = zeroForOne
? poolKey[1]
: poolKey[0];
} catch {
swapUndecoded = true;
}
} }
} }
} catch { } catch {
@@ -462,11 +488,13 @@ function decodeV4Swap(input) {
} }
} }
const first = swaps.length > 0 ? swaps[0] : NO_SWAP_READ;
const last = swaps.length > 0 ? swaps[swaps.length - 1] : NO_SWAP_READ;
return { return {
tokenIn: settleToken, tokenIn: present(settleToken) ? settleToken : first.tokenIn,
tokenOut: takeToken, tokenOut: present(takeToken) ? takeToken : last.tokenOut,
amountIn, amountIn: first.amountIn,
amountOutMin, amountOutMin: last.amountOutMin,
swapUndecoded, swapUndecoded,
}; };
} catch { } catch {
@@ -597,11 +625,12 @@ function decode(data, toAddress, sources) {
if (cmdId === 0x10) { if (cmdId === 0x10) {
const v4 = decodeV4Swap(inputs[i]); const v4 = decodeV4Swap(inputs[i]);
if (v4 && v4.swapUndecoded) { if (v4 && v4.swapUndecoded) {
// The unread swap action named this step's currencies // A swap action of this step went unread, so the step
// and amounts, so the step is marked and still sets // is marked and still sets both sides as a swap step
// both sides as a swap step does. A side its SETTLE or // does. A currency or figure that neither SETTLE,
// TAKE did not name is set undetermined, not left to // SETTLE_ALL, TAKE, TAKE_ALL nor a readable swap
// an earlier or later step's token and figure. // action gave decodeV4Swap() is set undetermined, not
// left to an earlier or later step's token and figure.
swapUndecoded = true; swapUndecoded = true;
commandNames[i] += " (could not be decoded)"; commandNames[i] += " (could not be decoded)";
if (!inputEstablished) { if (!inputEstablished) {
+239 -20
View File
@@ -107,6 +107,7 @@ function encodeV4Swap(actions, params) {
// V4 inner action IDs, as src/shared/uniswap.js names them. // V4 inner action IDs, as src/shared/uniswap.js names them.
const V4_SWAP_EXACT_IN = 0x07; const V4_SWAP_EXACT_IN = 0x07;
const V4_SWAP_EXACT_IN_SINGLE_ID = 0x06; const V4_SWAP_EXACT_IN_SINGLE_ID = 0x06;
const V4_SWAP_EXACT_OUT_SINGLE_ID = 0x08;
const V4_SETTLE_ID = 0x0b; const V4_SETTLE_ID = 0x0b;
const V4_TAKE_ID = 0x0e; const V4_TAKE_ID = 0x0e;
const ZERO_ADDR = "0x0000000000000000000000000000000000000000"; const ZERO_ADDR = "0x0000000000000000000000000000000000000000";
@@ -163,6 +164,31 @@ function encodeV4ExactInSingle(currency0, currency1, amountIn, amountOutMin) {
); );
} }
// Helper: V4 ExactOutputSingleParams —
// (tuple(address,address,uint24,int24,address) poolKey, bool zeroForOne,
// uint128 amountOut, uint128 amountInMaximum, bytes hookData)
function encodeV4ExactOutSingle(
currency0,
currency1,
amountOut,
amountInMaximum,
) {
return coder.encode(
[
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
],
[
[
[currency0, currency1, 100, 1, ZERO_ADDR],
true, // zeroForOne: in = currency0, out = currency1
amountOut,
amountInMaximum,
"0x",
],
],
);
}
// Each V4 swap action's parameters in two shapes: the struct V4Router has // Each V4 swap action's parameters in two shapes: the struct V4Router has
// deployed on mainnet and Sepolia, and the one v4-periphery `main` has since // deployed on mainnet and Sepolia, and the one v4-periphery `main` has since
// moved to, which adds a `minHopPriceX36` field. Each swaps WETH for USDC. // moved to, which adds a `minHopPriceX36` field. Each swaps WETH for USDC.
@@ -267,9 +293,7 @@ function v4SwapCalldata(actionId, swapParams) {
); );
} }
// V4 SETTLE_ALL (currency, maxAmount) and TAKE_ALL (currency, minAmount). The // V4 SETTLE_ALL (currency, maxAmount) and TAKE_ALL (currency, minAmount).
// decoder reads neither, so in a V4_SWAP of a swap action and these, only the
// swap action names the currencies.
const V4_SETTLE_ALL_ID = 0x0c; const V4_SETTLE_ALL_ID = 0x0c;
const V4_TAKE_ALL_ID = 0x0f; const V4_TAKE_ALL_ID = 0x0f;
function encodeV4SettleAll(currency, maxAmount) { function encodeV4SettleAll(currency, maxAmount) {
@@ -358,10 +382,9 @@ describe("uniswap decoder", () => {
}, },
); );
// Only the unread swap action names the currencies here. The token lines // SETTLE_ALL and TAKE_ALL name both sides as SETTLE and TAKE do.
// must not say the calldata did not name them.
test.each(V4_SWAP_ACTION_SHAPES)( test.each(V4_SWAP_ACTION_SHAPES)(
"a V4 $action in the newer shape, then SETTLE_ALL and TAKE_ALL, says both tokens could not be decoded", "a V4 $action in the newer shape, then SETTLE_ALL and TAKE_ALL, takes both tokens from them",
({ id, newer }) => { ({ id, newer }) => {
const data = buildExecute( const data = buildExecute(
"0x10", "0x10",
@@ -381,6 +404,27 @@ describe("uniswap decoder", () => {
expect(detail(result, "Steps").value).toBe( expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)", "V4 Swap (could not be decoded)",
); );
expect(detail(result, "Token In").address).toBe(WETH_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
},
);
// Only the unread swap action names the currencies here. The token lines
// must not say the calldata did not name them.
test.each(V4_SWAP_ACTION_SHAPES)(
"a V4 step of only a $action in the newer shape says both tokens could not be decoded",
({ id, newer }) => {
const data = buildExecute(
"0x10",
[encodeV4Swap(new Uint8Array([id]), [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").value).toBe( expect(detail(result, "Token In").value).toBe(
"Unknown (could not be decoded)", "Unknown (could not be decoded)",
); );
@@ -394,7 +438,7 @@ describe("uniswap decoder", () => {
// action names. The V3 step's WETH and its Min. received figure were // action names. The V3 step's WETH and its Min. received figure were
// shown instead. // shown instead.
test.each(V4_SWAP_ACTION_SHAPES)( test.each(V4_SWAP_ACTION_SHAPES)(
"a V3 step, then a V4 $action in the newer shape and TAKE_ALL, does not show the V3 step's output", "a V3 step, then a V4 step of only a $action in the newer shape, does not show the V3 step's output",
({ id, newer }) => { ({ id, newer }) => {
const data = buildExecute( const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x00, 0x10]), solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
@@ -403,10 +447,7 @@ describe("uniswap decoder", () => {
USDT_ADDR, USDT_ADDR,
WETH_ADDR, WETH_ADDR,
]), ]),
encodeV4Swap(new Uint8Array([id, V4_TAKE_ALL_ID]), [ encodeV4Swap(new Uint8Array([id]), [newer]),
newer,
encodeV4TakeAll(USDC_ADDR, 990000n),
]),
], ],
9999999999n, 9999999999n,
); );
@@ -423,15 +464,12 @@ describe("uniswap decoder", () => {
// The user pays the V4 step's WETH, which only its unread swap action // The user pays the V4 step's WETH, which only its unread swap action
// names. The V3 step's USDC and its amount were shown instead. // names. The V3 step's USDC and its amount were shown instead.
test.each(V4_SWAP_ACTION_SHAPES)( test.each(V4_SWAP_ACTION_SHAPES)(
"a V4 $action in the newer shape and SETTLE_ALL, then a V3 step, does not show the V3 step's input", "a V4 step of only a $action in the newer shape, then a V3 step, does not show the V3 step's input",
({ id, newer }) => { ({ id, newer }) => {
const data = buildExecute( const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x10, 0x00]), solidityPacked(["uint8", "uint8"], [0x10, 0x00]),
[ [
encodeV4Swap(new Uint8Array([id, V4_SETTLE_ALL_ID]), [ encodeV4Swap(new Uint8Array([id]), [newer]),
newer,
encodeV4SettleAll(WETH_ADDR, ONE_WETH),
]),
encodeV3SwapExactIn( encodeV3SwapExactIn(
USER_ADDR, USER_ADDR,
990000n, 990000n,
@@ -458,10 +496,7 @@ describe("uniswap decoder", () => {
const data = buildExecute( const data = buildExecute(
solidityPacked(["uint8", "uint8"], [0x10, 0x0c]), solidityPacked(["uint8", "uint8"], [0x10, 0x0c]),
[ [
encodeV4Swap(new Uint8Array([id, V4_TAKE_ALL_ID]), [ encodeV4Swap(new Uint8Array([id]), [newer]),
newer,
encodeV4TakeAll(USDC_ADDR, 990000n),
]),
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH, same encoding encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH, same encoding
], ],
9999999999n, 9999999999n,
@@ -1124,6 +1159,190 @@ 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
// chained single-pool exact-in swap actions, USDT to WETH and then WETH to
// USDC. The user pays USDT and receives USDC, but the screen showed the
// first action's WETH as Token Out and its minimum as Min. received.
test.each([
{
ending: "SETTLE_ALL and TAKE_ALL",
ids: [V4_SETTLE_ALL_ID, V4_TAKE_ALL_ID],
params: [
encodeV4SettleAll(USDT_ADDR, 1000000000n),
encodeV4TakeAll(USDC_ADDR, 990000000n),
],
},
{
ending: "SETTLE and TAKE",
ids: [V4_SETTLE_ID, V4_TAKE_ID],
params: [encodeV4Settle(USDT_ADDR), encodeV4Take(USDC_ADDR)],
},
{
// Only the swap actions name the currencies.
ending: "nothing",
ids: [],
params: [],
},
])(
"two chained V4 swap actions followed by $ending show the first one's input and the last one's output",
({ ids, params }) => {
const data = buildExecute(
"0x10",
[
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
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,
);
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, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Min. received").value).toBe("990.0000 USDC");
},
);
// 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();
});
// 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