Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4f2c549f5 |
@@ -1164,8 +1164,9 @@ read:
|
||||
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,
|
||||
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
|
||||
exact-in action.
|
||||
`amountInMax`, and a V4 swap step the `amountIn` of the swap action that
|
||||
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
|
||||
`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
|
||||
@@ -1186,17 +1187,29 @@ read:
|
||||
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
|
||||
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
|
||||
whose swap action could not be read still sets each side as a swap step does,
|
||||
and a side its `SETTLE` or `TAKE` did not name is then unknown: no other step's
|
||||
token or figure is shown for it. 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 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
|
||||
side the currency its `TAKE` or `TAKE_ALL` names. A side none of them names
|
||||
comes from the step's 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, so each one goes after the actions before
|
||||
it, whose output it spends. It runs exact-out actions from the output end, so
|
||||
the first one buys what the user receives and each later one goes in front of
|
||||
the actions before it, buying what they spend. The input side and `Amount` come
|
||||
from the first action in that order, and the output side and `Min. received`
|
||||
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.
|
||||
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
||||
|
||||
+27
-13
@@ -412,10 +412,15 @@ function decodeV4SwapAction(actionId, param) {
|
||||
// The input is ABI-encoded as (bytes actions, bytes[] params).
|
||||
// SETTLE or SETTLE_ALL names the step's input currency, and TAKE or TAKE_ALL
|
||||
// its output currency, wherever in the step they stand. A side neither one
|
||||
// names is read from the swap actions. In a chain of swap actions each one
|
||||
// spends what the one before it bought, so the input is what the first swap
|
||||
// action spends and the output is what the last one buys. Amount comes from
|
||||
// the first swap action and Min. received from the last.
|
||||
// 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) {
|
||||
try {
|
||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||
@@ -424,8 +429,8 @@ function decodeV4Swap(input) {
|
||||
|
||||
let settleToken = null;
|
||||
let takeToken = null;
|
||||
// The step's swap actions in order, as decodeV4SwapAction() reads
|
||||
// them.
|
||||
// The step's swap actions, as decodeV4SwapAction() reads them, from
|
||||
// what the user pays to what the user receives.
|
||||
const swaps = [];
|
||||
// Set when a swap action's parameters are not in the deployed shape
|
||||
// (see decodeExactly()), so nothing was read from that action. SETTLE,
|
||||
@@ -463,11 +468,19 @@ function decodeV4Swap(input) {
|
||||
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
||||
actionId === V4_SWAP_EXACT_OUT
|
||||
) {
|
||||
let swap = NO_SWAP_READ;
|
||||
try {
|
||||
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
||||
swap = decodeV4SwapAction(actionId, params[i]);
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
swaps.push(NO_SWAP_READ);
|
||||
}
|
||||
if (
|
||||
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
||||
actionId === V4_SWAP_EXACT_OUT
|
||||
) {
|
||||
swaps.unshift(swap);
|
||||
} else {
|
||||
swaps.push(swap);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -612,11 +625,12 @@ function decode(data, toAddress, sources) {
|
||||
if (cmdId === 0x10) {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
if (v4 && v4.swapUndecoded) {
|
||||
// The unread swap action named this step's currencies
|
||||
// and amounts, so the step is marked and still sets
|
||||
// both sides as a swap step does. A side its SETTLE or
|
||||
// TAKE did not name is set undetermined, not left to
|
||||
// an earlier or later step's token and figure.
|
||||
// A swap action of this step went unread, so the step
|
||||
// is marked and still sets both sides as a swap step
|
||||
// does. A currency or figure that neither SETTLE,
|
||||
// SETTLE_ALL, TAKE, TAKE_ALL nor a readable swap
|
||||
// action gave decodeV4Swap() is set undetermined, not
|
||||
// left to an earlier or later step's token and figure.
|
||||
swapUndecoded = true;
|
||||
commandNames[i] += " (could not be decoded)";
|
||||
if (!inputEstablished) {
|
||||
|
||||
@@ -107,6 +107,7 @@ function encodeV4Swap(actions, params) {
|
||||
// V4 inner action IDs, as src/shared/uniswap.js names them.
|
||||
const V4_SWAP_EXACT_IN = 0x07;
|
||||
const V4_SWAP_EXACT_IN_SINGLE_ID = 0x06;
|
||||
const V4_SWAP_EXACT_OUT_SINGLE_ID = 0x08;
|
||||
const V4_SETTLE_ID = 0x0b;
|
||||
const V4_TAKE_ID = 0x0e;
|
||||
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
|
||||
// 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.
|
||||
@@ -1201,6 +1227,122 @@ describe("uniswap decoder", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// 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
|
||||
// exact-in reads an amountIn of zero as universal-router
|
||||
// Constants.ALREADY_PAID: an earlier step sent the tokens to the pair, and
|
||||
|
||||
Reference in New Issue
Block a user