fix: a V4 step of chained swap actions shows what the user receives (closes #563)
A V4 step's input side is what SETTLE or SETTLE_ALL names, else the one currency its swap actions spend and never buy; its output side is what TAKE or TAKE_ALL names, else the one they buy and never spend. Amount sums the amountIn of the actions spending the input side, shown only when each states one; Min. received likewise. An unread swap action leaves the sides to SETTLE, SETTLE_ALL, TAKE and TAKE_ALL, with no figure. A step with a swap action always sets the output side, sets the input side only if no earlier step did, and leaves a side its rules cannot name unknown, with no figure. SWAP_EXACT_OUT spends its first path entry. Model: opus-5-5
This commit was merged in pull request #566.
This commit is contained in:
@@ -1168,11 +1168,13 @@ read:
|
|||||||
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
|
- `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
|
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
|
whole open delta", so the calldata states no quantity. The line shows the
|
||||||
amount of one step that names an input token or amount: the last
|
amount of the step that sets the input side: the last `PERMIT2_PERMIT` step if
|
||||||
`PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in,
|
there is one, otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH`
|
||||||
V2 exact-out, `WRAP_ETH` or V4 swap step. A V2 exact-out step gives its
|
or V4 swap step that names an input token or amount. A V4 step with a swap
|
||||||
`amountInMax`, and a V4 swap step the `amountIn` of its first readable
|
action sets the input side when no earlier step set it, even when it names
|
||||||
exact-in action.
|
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.
|
||||||
- `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
|
||||||
@@ -1194,16 +1196,38 @@ 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
|
||||||
whose swap action could not be read still sets each side as a swap step does,
|
with a swap action always sets the output side, and sets the input side only
|
||||||
and a side its `SETTLE` or `TAKE` did not name is then unknown: no other step's
|
when no earlier step set it. It finds each side by three rules:
|
||||||
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
|
1. Its input side is the currency its `SETTLE` or `SETTLE_ALL` names, or else
|
||||||
the output side; a WETH `Min. received` figure then reads in ETH. Otherwise
|
the one currency that some swap action in the step spends and none buys. Its
|
||||||
`Token Out` and `Min. received` keep the output side's own token and figure,
|
output side is the currency its `TAKE` or `TAKE_ALL` names, or else the one
|
||||||
whether the swap was paid in ETH or in a token: a V2 exact-out swap that buys
|
currency that some swap action buys and none spends. If there is not exactly
|
||||||
USDC and then unwraps the WETH it did not spend shows USDC. The token permission
|
one such currency, that side is unknown, and the step sets it as unknown with
|
||||||
warning on the signature screen has its own amount wording, including `Unknown`;
|
no figure. A step that buys WETH with USDT and USDC with that WETH, with
|
||||||
the SignApproval section below describes it.
|
nothing naming its sides, reads `Swap USDT → USDC`.
|
||||||
|
2. Its `Amount` is the `amountIn` of the swap action that spends the input
|
||||||
|
currency. If several exact-in actions spend it, each with a stated amount, it
|
||||||
|
is their sum; in any other case the step gives no `Amount`. The decoder reads
|
||||||
|
no amount from an exact-out action, and an open delta is not a stated amount.
|
||||||
|
Its `Min. received` is found the same way, from the `amountOutMin` of the
|
||||||
|
actions that buy the output currency.
|
||||||
|
3. If any swap action in the step could not be read, only `SETTLE`,
|
||||||
|
`SETTLE_ALL`, `TAKE` and `TAKE_ALL` name its sides, and the step gives no
|
||||||
|
`Amount` or `Min. received`. A side they did not name is unknown, and the
|
||||||
|
step sets it as unknown with no figure.
|
||||||
|
|
||||||
|
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||||
|
`TAKE` or `TAKE_ALL` names.
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
+222
-131
@@ -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,133 @@ function decodeExactly(types, data) {
|
|||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
// The router works through the path from its last entry back to its
|
||||||
|
// first, starting from currencyOut, so the first entry is what the
|
||||||
|
// action spends.
|
||||||
|
const path = s[0][1];
|
||||||
|
return {
|
||||||
|
tokenIn: path.length > 0 ? path[0][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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
// We extract token addresses from SETTLE (input) and TAKE (output) sub-actions,
|
// action gives the currency it spends and the currency it buys (see
|
||||||
// and swap amounts from the swap sub-actions.
|
// decodeV4SwapAction()). The step's sides then follow three rules:
|
||||||
|
//
|
||||||
|
// 1. Token In is the currency SETTLE or SETTLE_ALL names; otherwise the one
|
||||||
|
// currency that some swap action spends and none buys. Token Out is the
|
||||||
|
// currency TAKE or TAKE_ALL names; otherwise the one currency that some
|
||||||
|
// swap action buys and none spends. If there is not exactly one such
|
||||||
|
// currency, that side is unknown.
|
||||||
|
// 2. Amount is the amountIn of the swap action that spends Token In. If
|
||||||
|
// several exact-in actions spend it, each with a stated amount, it is
|
||||||
|
// their sum; in any other case there is none. Min. received is the same
|
||||||
|
// for amountOutMin and the actions that buy Token Out.
|
||||||
|
// 3. If any swap action in the step cannot be read, only SETTLE, SETTLE_ALL,
|
||||||
|
// TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
|
||||||
|
// received.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// `hasSwapAction` is true when the step has a swap action, read or not.
|
||||||
|
// decode() then always sets the output side from this step, and sets the
|
||||||
|
// input side from it only when no earlier step set it. A side it sets that
|
||||||
|
// these rules leave unknown is set unknown, with no figure. A step with no
|
||||||
|
// swap action sets only the sides SETTLE, SETTLE_ALL, TAKE and TAKE_ALL
|
||||||
|
// name.
|
||||||
function decodeV4Swap(input) {
|
function decodeV4Swap(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||||
@@ -330,11 +455,12 @@ function decodeV4Swap(input) {
|
|||||||
|
|
||||||
let settleToken = null;
|
let settleToken = null;
|
||||||
let takeToken = null;
|
let takeToken = null;
|
||||||
let amountIn = null;
|
// The step's swap actions that could be read, as decodeV4SwapAction()
|
||||||
let amountOutMin = null;
|
// reads them.
|
||||||
|
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 +473,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 +484,20 @@ 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
|
try {
|
||||||
if (actionId === V4_SWAP_EXACT_IN) {
|
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
||||||
// ExactInputParams: (address currencyIn,
|
} catch {
|
||||||
// tuple(address,uint24,int24,address,bytes)[] path,
|
swapUndecoded = true;
|
||||||
// uint128 amountIn, uint128 amountOutMin)
|
|
||||||
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 (
|
|
||||||
actionId === V4_SWAP_EXACT_OUT ||
|
|
||||||
actionId === V4_SWAP_EXACT_OUT_SINGLE
|
|
||||||
) {
|
|
||||||
if (actionId === V4_SWAP_EXACT_OUT) {
|
|
||||||
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 {
|
|
||||||
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];
|
|
||||||
} catch {
|
|
||||||
swapUndecoded = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -462,12 +505,54 @@ function decodeV4Swap(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rule 3.
|
||||||
|
if (swapUndecoded) {
|
||||||
|
return {
|
||||||
|
tokenIn: settleToken,
|
||||||
|
tokenOut: takeToken,
|
||||||
|
amountIn: null,
|
||||||
|
amountOutMin: null,
|
||||||
|
swapUndecoded,
|
||||||
|
hasSwapAction: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
tokenIn: settleToken,
|
tokenIn,
|
||||||
tokenOut: takeToken,
|
tokenOut,
|
||||||
amountIn,
|
amountIn,
|
||||||
amountOutMin,
|
amountOutMin,
|
||||||
swapUndecoded,
|
swapUndecoded,
|
||||||
|
hasSwapAction: swaps.length > 0,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -491,6 +576,7 @@ function decode(data, toAddress, sources) {
|
|||||||
let inputEstablished = false;
|
let inputEstablished = false;
|
||||||
let outputToken = null;
|
let outputToken = null;
|
||||||
let minOutput = null;
|
let minOutput = null;
|
||||||
|
let outputEstablished = false;
|
||||||
let hasUnwrapWeth = false;
|
let hasUnwrapWeth = false;
|
||||||
let hasV2ExactOut = false;
|
let hasV2ExactOut = false;
|
||||||
let swapUndecoded = false;
|
let swapUndecoded = false;
|
||||||
@@ -510,8 +596,11 @@ function decode(data, toAddress, sources) {
|
|||||||
//
|
//
|
||||||
// The input side is fixed by the first hop that states either half, the
|
// The input side is fixed by the first hop that states either half, the
|
||||||
// output side by the last, because the final leg is what the user
|
// output side by the last, because the final leg is what the user
|
||||||
// receives. A half the establishing hop did not state stays null and
|
// receives. A V4 step with a swap action sets a side even when it
|
||||||
// the line says so, rather than being filled in from a different hop.
|
// 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.
|
||||||
const setInput = (token, amount) => {
|
const setInput = (token, amount) => {
|
||||||
inputToken = present(token) ? token : null;
|
inputToken = present(token) ? token : null;
|
||||||
inputAmount = present(amount) ? amount : null;
|
inputAmount = present(amount) ? amount : null;
|
||||||
@@ -526,6 +615,7 @@ function decode(data, toAddress, sources) {
|
|||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
outputToken = present(token) ? token : null;
|
outputToken = present(token) ? token : null;
|
||||||
minOutput = present(amount) ? amount : null;
|
minOutput = present(amount) ? amount : null;
|
||||||
|
outputEstablished = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < commandsBytes.length; i++) {
|
for (let i = 0; i < commandsBytes.length; i++) {
|
||||||
@@ -597,30 +687,33 @@ 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.
|
||||||
// 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.
|
|
||||||
swapUndecoded = true;
|
swapUndecoded = true;
|
||||||
commandNames[i] += " (could not be decoded)";
|
commandNames[i] += " (could not be decoded)";
|
||||||
|
}
|
||||||
|
if (v4 && v4.hasSwapAction) {
|
||||||
|
// A step with a swap action always sets the output
|
||||||
|
// side, as the last swap step wins, and sets the
|
||||||
|
// input side only when no earlier step set it. Each
|
||||||
|
// side it sets is set as a pair, even when the step
|
||||||
|
// states neither half, so a side decodeV4Swap() left
|
||||||
|
// unknown is set unknown with no figure. A token
|
||||||
|
// stated without a figure, or a figure without a
|
||||||
|
// token, likewise never sits beside an earlier step's
|
||||||
|
// other half.
|
||||||
if (!inputEstablished) {
|
if (!inputEstablished) {
|
||||||
setInput(v4.tokenIn, v4.amountIn);
|
setInput(v4.tokenIn, v4.amountIn);
|
||||||
}
|
}
|
||||||
outputToken = v4.tokenOut;
|
outputToken = v4.tokenOut;
|
||||||
minOutput = v4.amountOutMin;
|
minOutput = v4.amountOutMin;
|
||||||
|
outputEstablished = true;
|
||||||
} else if (v4) {
|
} else if (v4) {
|
||||||
|
// A step with no swap action sets only the sides its
|
||||||
|
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||||
|
// setInputOnce() and setOutput() leave alone a side
|
||||||
|
// whose two halves are both unstated.
|
||||||
setInputOnce(v4.tokenIn, v4.amountIn);
|
setInputOnce(v4.tokenIn, v4.amountIn);
|
||||||
// Always update output: last swap step wins. A step
|
|
||||||
// that carries the Min. received figure but decoded no
|
|
||||||
// output currency makes the output *undetermined* — it
|
|
||||||
// is neither ETH nor whatever an earlier step named,
|
|
||||||
// and that figure is no longer counted in that token.
|
|
||||||
// Equally, a step that names an output currency but no
|
|
||||||
// minimum leaves Min. received unstated rather than
|
|
||||||
// keeping an earlier step's figure beside the new
|
|
||||||
// token. setOutput() is both rules; a step that states
|
|
||||||
// neither half leaves the output alone.
|
|
||||||
setOutput(v4.tokenOut, v4.amountOutMin);
|
setOutput(v4.tokenOut, v4.amountOutMin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,21 +727,19 @@ function decode(data, toAddress, sources) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve token info. A null token on either side means no step named
|
// Resolve token info. A null token on either side means no step named
|
||||||
// a currency for it, or a V4 step whose swap action went unread left
|
// a currency for it, or a V4 step with a swap action left it
|
||||||
// it undetermined; tokenInfo() refuses rather than calling it ETH.
|
// unknown; tokenInfo() refuses rather than calling it ETH.
|
||||||
// UNWRAP_WETH turns WETH into ETH, so it makes the output ETH when the
|
// UNWRAP_WETH turns WETH into ETH, so it makes the output ETH when the
|
||||||
// output side is WETH, or when no step set the output side. A step
|
// output side is WETH, or when no step set the output side. A V4 step
|
||||||
// whose swap action went unread did set it, to undetermined. Any other
|
// with a swap action did set it, even when it set it unknown. Any other
|
||||||
// output keeps its own token and figure: a swap that buys USDC and
|
// output keeps its own token and figure: a swap that buys USDC and
|
||||||
// then unwraps the WETH it did not spend receives USDC.
|
// then unwraps the WETH it did not spend receives USDC.
|
||||||
const outputIsWeth =
|
const outputIsWeth =
|
||||||
present(outputToken) &&
|
present(outputToken) &&
|
||||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||||
const outputUnset =
|
|
||||||
!present(outputToken) && !present(minOutput) && !swapUndecoded;
|
|
||||||
const inInfo = tokenInfo(inputToken, sources);
|
const inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo =
|
const outInfo =
|
||||||
hasUnwrapWeth && (outputIsWeth || outputUnset)
|
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
? { symbol: "ETH", decimals: 18, address: null }
|
||||||
: tokenInfo(outputToken, sources);
|
: tokenInfo(outputToken, sources);
|
||||||
|
|
||||||
|
|||||||
+474
-20
@@ -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) {
|
||||||
@@ -279,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);
|
||||||
}
|
}
|
||||||
@@ -358,10 +388,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 +410,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 +444,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 +453,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 +470,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 +502,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 +1165,419 @@ 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 — a V4 step of several
|
||||||
|
// swap actions. The screen showed the first action's output as Token Out.
|
||||||
|
// Every step below pays USDT and receives USDC, so it reads Swap USDT →
|
||||||
|
// USDC whichever kind of swap actions it holds and in whatever order they
|
||||||
|
// 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",
|
||||||
|
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)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ending: "CLOSE_CURRENCY",
|
||||||
|
ids: [
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
],
|
||||||
|
params: [
|
||||||
|
encodeV4CloseCurrency(USDT_ADDR),
|
||||||
|
encodeV4CloseCurrency(WETH_ADDR),
|
||||||
|
encodeV4CloseCurrency(USDC_ADDR),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// 1,000 USDT for at least 0.5 WETH
|
||||||
|
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(
|
||||||
|
"0x10",
|
||||||
|
[
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([...swaps.map((s) => s.id), ...ids]),
|
||||||
|
[...swaps.map((s) => s.param), ...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, "Token Out").address).toBe(USDC_ADDR);
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// A V4 step that spends WETH on USDC and on DAI. Its swap actions buy two
|
||||||
|
// currencies that none of them spends, and CLOSE_CURRENCY, which the
|
||||||
|
// decoder does not read, names no side, so its output side is unknown.
|
||||||
|
const WETH_FOR_USDC_AND_DAI = encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
// 0.25 WETH for at least 495 USDC
|
||||||
|
encodeV4ExactInSingle(
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
250000000000000000n,
|
||||||
|
495000000n,
|
||||||
|
),
|
||||||
|
// 0.25 WETH for at least 495 DAI
|
||||||
|
encodeV4ExactInSingle(
|
||||||
|
WETH_ADDR,
|
||||||
|
DAI_ADDR,
|
||||||
|
250000000000000000n,
|
||||||
|
495000000000000000000n,
|
||||||
|
),
|
||||||
|
encodeV4CloseCurrency(WETH_ADDR),
|
||||||
|
encodeV4CloseCurrency(USDC_ADDR),
|
||||||
|
encodeV4CloseCurrency(DAI_ADDR),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// The user ends with USDC and DAI, not the V3 step's WETH. That WETH and
|
||||||
|
// the V3 step's Min. received figure were shown instead.
|
||||||
|
test("a V3 step, then a V4 step whose swap actions buy two currencies, does not show the V3 step's output", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(
|
||||||
|
USER_ADDR,
|
||||||
|
1000000000n,
|
||||||
|
500000000000000000n,
|
||||||
|
[USDT_ADDR, WETH_ADDR],
|
||||||
|
),
|
||||||
|
WETH_FOR_USDC_AND_DAI,
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
expect(detail(result, "Token In").address).toBe(
|
||||||
|
USDT_ADDR.toLowerCase(),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||||
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
|
"Unknown (not named in the calldata)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The input side of the same gap: the V4 step's swap actions spend USDC
|
||||||
|
// and DAI, two currencies that none of them buys, and nothing names its
|
||||||
|
// input side. The V3 step's WETH and its amount were shown instead.
|
||||||
|
test("a V4 step whose swap actions spend two currencies, then a V3 step, does not show the V3 step's input", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x10, 0x00]),
|
||||||
|
[
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
V4_CLOSE_CURRENCY_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
// 500 USDC for at least 0.25 WETH
|
||||||
|
encodeV4ExactInSingle(
|
||||||
|
USDC_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
500000000n,
|
||||||
|
250000000000000000n,
|
||||||
|
),
|
||||||
|
// 500 DAI for at least 0.25 WETH
|
||||||
|
encodeV4ExactInSingle(
|
||||||
|
DAI_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
500000000000000000000n,
|
||||||
|
250000000000000000n,
|
||||||
|
),
|
||||||
|
encodeV4CloseCurrency(USDC_ADDR),
|
||||||
|
encodeV4CloseCurrency(DAI_ADDR),
|
||||||
|
encodeV4CloseCurrency(WETH_ADDR),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
encodeV3SwapExactIn(
|
||||||
|
USER_ADDR,
|
||||||
|
500000000000000000n,
|
||||||
|
990000000n,
|
||||||
|
[WETH_ADDR, USDT_ADDR],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
|
expect(result.name).toBe("Uniswap Swap");
|
||||||
|
expect(detail(result, "Token In").value).toBe(
|
||||||
|
"Unknown (not named in the calldata)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount")).toBeUndefined();
|
||||||
|
expect(detail(result, "Token Out").address).toBe(
|
||||||
|
USDT_ADDR.toLowerCase(),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("990.0000 USDT");
|
||||||
|
});
|
||||||
|
|
||||||
|
// UNWRAP_WETH makes Token Out ETH when no step set the output side. A V4
|
||||||
|
// step with a swap action did set it, to unknown here.
|
||||||
|
test("an UNWRAP_WETH after a V4 step whose swap actions buy two currencies does not make Token Out ETH", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x10, 0x0c]),
|
||||||
|
[
|
||||||
|
WETH_FOR_USDC_AND_DAI,
|
||||||
|
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH, same encoding
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
|
"Unknown (not named in the calldata)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The router works through an exact-out path from its last entry back to
|
||||||
|
// its first: the WETH entry buys the USDC, then the USDT entry buys that
|
||||||
|
// WETH. The user pays the first entry's USDT, not the last entry's WETH.
|
||||||
|
test("a two-hop V4 exact-out action shows its first path entry as Token In", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
"0x10",
|
||||||
|
[
|
||||||
|
encodeV4Swap(new Uint8Array([0x09]), [
|
||||||
|
// SWAP_EXACT_OUT: exactly 990 USDC for at most 1,000 USDT
|
||||||
|
coder.encode(
|
||||||
|
[`tuple(address,${PATH_KEY}[],uint128,uint128)`],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
USDC_ADDR,
|
||||||
|
[
|
||||||
|
[USDT_ADDR, 100, 1, ZERO_ADDR, "0x"],
|
||||||
|
[WETH_ADDR, 100, 1, ZERO_ADDR, "0x"],
|
||||||
|
],
|
||||||
|
990000000n,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user