Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc42063d2 |
@@ -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 the swap action that
|
||||
spends its input currency, as described below.
|
||||
`amountInMax`, and a V4 swap step the `amountIn` of the first of its swap
|
||||
actions as they are lined up below, or none when that action is exact-out,
|
||||
could not be read or does not spend the step's input currency.
|
||||
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's
|
||||
`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
|
||||
@@ -1187,28 +1188,29 @@ 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's
|
||||
sides follow three rules:
|
||||
|
||||
1. Its input side is the currency its `SETTLE` or `SETTLE_ALL` names, or else
|
||||
the one currency that some swap action in the step spends and none buys. Its
|
||||
output side is the currency its `TAKE` or `TAKE_ALL` names, or else the one
|
||||
currency that some swap action buys and none spends. If there is not exactly
|
||||
one such currency, that side is unknown. A step that buys WETH with USDT and
|
||||
USDC with that WETH, with nothing naming its sides, reads `Swap USDT → USDC`.
|
||||
2. `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 there is no `Amount` line. The decoder reads no amount
|
||||
from an exact-out action, and an open delta is not a stated amount.
|
||||
`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 there is no `Amount`
|
||||
or `Min. received` line. Such a step still sets each side as a swap step
|
||||
does, and no other step's token or figure is shown for a side they did not
|
||||
name.
|
||||
|
||||
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
|
||||
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, lined up as the exact-out actions in reverse
|
||||
order, then the exact-in actions in the order they appear: the input side from
|
||||
the first action in that line, the output side from the last. The router runs
|
||||
exact-in actions from the input end and a chain of exact-out actions from the
|
||||
output end, so a step whose swap actions are all of one kind is lined up from
|
||||
what the user pays to what the user receives. A step that mixes the two kinds
|
||||
need not be: one that buys WETH with USDT exact-in and then USDC with that WETH
|
||||
exact-out is lined up with the USDC purchase first, so with no `SETTLE`,
|
||||
`SETTLE_ALL`, `TAKE` or `TAKE_ALL` it reads `Swap WETH → WETH`. `Amount` is the
|
||||
first action's `amountIn`, shown only when that action spends the step's input
|
||||
currency, and `Min. received` the last action's `amountOutMin`, shown only when
|
||||
that action buys the step's output currency, so no figure is shown against a
|
||||
currency it is not counted in. The decoder reads no amount from an exact-out
|
||||
action. That mixed step, ending in `SETTLE_ALL` USDT and `TAKE_ALL` USDC, reads
|
||||
`Swap USDT → USDC` with neither figure, since its first action in the line is
|
||||
exact-out and its last buys WETH. A swap action that could not be read keeps its
|
||||
place in the line 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
|
||||
|
||||
+56
-85
@@ -320,6 +320,15 @@ function decodeExactly(types, data) {
|
||||
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.
|
||||
@@ -402,44 +411,25 @@ function decodeV4SwapAction(actionId, param) {
|
||||
};
|
||||
}
|
||||
|
||||
// The currencies in `list` that `other` does not hold, each listed once.
|
||||
function currenciesNotIn(list, other) {
|
||||
return [...new Set(list.filter((c) => !other.includes(c)))];
|
||||
}
|
||||
|
||||
// The amount that the swap actions on one side of a V4 step state together:
|
||||
// the one action's own amount, or the sum when there are several and each
|
||||
// states a quantity. Exact-out actions state none here, and the open delta is
|
||||
// not a quantity. Otherwise null.
|
||||
function totalAmount(amounts) {
|
||||
if (amounts.length === 1) return amounts[0];
|
||||
if (amounts.length === 0) return null;
|
||||
if (!amounts.every((a) => typeof a === "bigint")) return null;
|
||||
return amounts.reduce((sum, a) => sum + a, 0n);
|
||||
}
|
||||
|
||||
// Decode V4_SWAP (command 0x10) input bytes.
|
||||
// The input is ABI-encoded as (bytes actions, bytes[] params). Each swap
|
||||
// action gives the currency it spends and the currency it buys (see
|
||||
// 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.
|
||||
// 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, lined up as the exact-out actions in
|
||||
// reverse order, then the exact-in actions in the order they appear. The
|
||||
// input comes from the first action in that line and the output from the
|
||||
// last. The router runs exact-in actions from the input end and a chain of
|
||||
// exact-out actions from the output end, so a step whose swap actions are all
|
||||
// of one kind is lined up from what the user pays to what the user receives.
|
||||
// A step that mixes the two kinds need not be: one that buys WETH with USDT
|
||||
// exact-in and then USDC with that WETH exact-out is lined up with the USDC
|
||||
// purchase first, so with no SETTLE, SETTLE_ALL, TAKE or TAKE_ALL both its
|
||||
// sides are WETH. Amount is the first action's amountIn, kept only when that
|
||||
// action spends the step's input currency, and Min. received the last
|
||||
// action's amountOutMin, kept only when that action buys the step's output
|
||||
// currency, so a figure is never shown against a currency other than the one
|
||||
// it is counted in. A swap action that could not be read keeps its place in
|
||||
// the line and states nothing.
|
||||
function decodeV4Swap(input) {
|
||||
try {
|
||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||
@@ -448,8 +438,8 @@ function decodeV4Swap(input) {
|
||||
|
||||
let settleToken = null;
|
||||
let takeToken = null;
|
||||
// The step's swap actions that could be read, as decodeV4SwapAction()
|
||||
// reads them.
|
||||
// The step's swap actions, as decodeV4SwapAction() reads them, lined
|
||||
// up as the comment above says.
|
||||
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,
|
||||
@@ -487,58 +477,39 @@ 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;
|
||||
}
|
||||
// An exact-out action goes in front of the swap actions
|
||||
// before it, an exact-in one after them.
|
||||
if (
|
||||
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
||||
actionId === V4_SWAP_EXACT_OUT
|
||||
) {
|
||||
swaps.unshift(swap);
|
||||
} else {
|
||||
swaps.push(swap);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip sub-actions we can't decode
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 3.
|
||||
if (swapUndecoded) {
|
||||
return {
|
||||
tokenIn: settleToken,
|
||||
tokenOut: takeToken,
|
||||
amountIn: null,
|
||||
amountOutMin: null,
|
||||
swapUndecoded,
|
||||
};
|
||||
}
|
||||
|
||||
const spent = swaps.map((s) => s.tokenIn);
|
||||
const bought = swaps.map((s) => s.tokenOut);
|
||||
|
||||
// Rules 1 and 2 for Token In.
|
||||
let tokenIn = null;
|
||||
let amountIn = null;
|
||||
const spentOnly = currenciesNotIn(spent, bought);
|
||||
if (present(settleToken) || spentOnly.length === 1) {
|
||||
tokenIn = present(settleToken) ? settleToken : spentOnly[0];
|
||||
amountIn = totalAmount(
|
||||
swaps
|
||||
.filter((s) => s.tokenIn === tokenIn)
|
||||
.map((s) => s.amountIn),
|
||||
);
|
||||
}
|
||||
|
||||
// Rules 1 and 2 for Token Out.
|
||||
let tokenOut = null;
|
||||
let amountOutMin = null;
|
||||
const boughtOnly = currenciesNotIn(bought, spent);
|
||||
if (present(takeToken) || boughtOnly.length === 1) {
|
||||
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
|
||||
amountOutMin = totalAmount(
|
||||
swaps
|
||||
.filter((s) => s.tokenOut === tokenOut)
|
||||
.map((s) => s.amountOutMin),
|
||||
);
|
||||
}
|
||||
|
||||
return { tokenIn, tokenOut, amountIn, amountOutMin, swapUndecoded };
|
||||
const first = swaps.length > 0 ? swaps[0] : NO_SWAP_READ;
|
||||
const last = swaps.length > 0 ? swaps[swaps.length - 1] : NO_SWAP_READ;
|
||||
const tokenIn = present(settleToken) ? settleToken : first.tokenIn;
|
||||
const tokenOut = present(takeToken) ? takeToken : last.tokenOut;
|
||||
return {
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn: first.tokenIn === tokenIn ? first.amountIn : null,
|
||||
amountOutMin: last.tokenOut === tokenOut ? last.amountOutMin : null,
|
||||
swapUndecoded,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -669,10 +640,10 @@ function decode(data, toAddress, sources) {
|
||||
if (v4 && v4.swapUndecoded) {
|
||||
// A swap action of this step went unread, so the step
|
||||
// is marked and still sets both sides as a swap step
|
||||
// does, with no figure. A side its SETTLE, SETTLE_ALL,
|
||||
// TAKE or TAKE_ALL did not name is set undetermined,
|
||||
// not left to an earlier or later step's token and
|
||||
// figure.
|
||||
// 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) {
|
||||
|
||||
+202
-217
@@ -303,12 +303,6 @@ function encodeV4TakeAll(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) {
|
||||
return result.details.find((d) => d.label === label);
|
||||
}
|
||||
@@ -1165,14 +1159,11 @@ describe("uniswap decoder", () => {
|
||||
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 = [
|
||||
// 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],
|
||||
@@ -1187,212 +1178,39 @@ describe("uniswap decoder", () => {
|
||||
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)",
|
||||
// Only the swap actions name the currencies.
|
||||
ending: "nothing",
|
||||
ids: [],
|
||||
params: [],
|
||||
},
|
||||
])(
|
||||
"a V4 step of a readable swap action and an unread one, then $ending, shows no figure",
|
||||
({ ids, params, tokenIn, tokenOut }) => {
|
||||
"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([
|
||||
USDT_FOR_WETH_IN.id,
|
||||
V4_SWAP_ACTION_SHAPES[0].id,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
...ids,
|
||||
]),
|
||||
[
|
||||
USDT_FOR_WETH_IN.param,
|
||||
V4_SWAP_ACTION_SHAPES[0].newer,
|
||||
// 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,
|
||||
],
|
||||
),
|
||||
@@ -1401,13 +1219,11 @@ describe("uniswap decoder", () => {
|
||||
);
|
||||
|
||||
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();
|
||||
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");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1445,6 +1261,175 @@ describe("uniswap decoder", () => {
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
});
|
||||
|
||||
// The router runs a chain of exact-out actions from the output end: the
|
||||
// first buys the USDC the user receives, owing WETH, and the second buys
|
||||
// that WETH (the open delta) with the USDT the user pays.
|
||||
test("two chained V4 exact-out actions show the last one's input and the first one's output", () => {
|
||||
const data = buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||
]),
|
||||
[
|
||||
// exactly 990 USDC for at most 0.5 WETH
|
||||
encodeV4ExactOutSingle(
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
990000000n,
|
||||
500000000000000000n,
|
||||
),
|
||||
// exactly that WETH (the open delta) for at most
|
||||
// 1,000 USDT
|
||||
encodeV4ExactOutSingle(
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
0n,
|
||||
1000000000n,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
expect(detail(result, "Amount")).toBeUndefined();
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
|
||||
// An exact-in action buys WETH with USDT and an exact-out action then buys
|
||||
// USDC with that WETH. The exact-out action is lined up first, so the last
|
||||
// action in the line is the exact-in one, and its minimum is counted in
|
||||
// WETH, not in the USDC the user receives. Shown as Min. received, it would
|
||||
// read "400000000000.0000 USDC".
|
||||
test("a V4 exact-in action, then an exact-out one that spends its output, shows no WETH figure as Min. received", () => {
|
||||
const data = buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
0x13, // CLEAR_OR_TAKE, which the decoder does not read
|
||||
]),
|
||||
[
|
||||
// 1,000 USDT for at least 0.4 WETH
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
1000000000n,
|
||||
400000000000000000n,
|
||||
),
|
||||
// exactly 990 USDC for at most 0.5 WETH
|
||||
encodeV4ExactOutSingle(
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
990000000n,
|
||||
500000000000000000n,
|
||||
),
|
||||
encodeV4SettleAll(USDT_ADDR, 1000000000n),
|
||||
encodeV4TakeAll(USDC_ADDR, 990000000n),
|
||||
// the WETH left over: (address currency, uint256 amountMax)
|
||||
coder.encode(
|
||||
["address", "uint256"],
|
||||
[WETH_ADDR, 100000000000000000n],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
expect(detail(result, "Amount")).toBeUndefined();
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
|
||||
// A swap action that could not be read still stands first or last in its
|
||||
// step, so the side and figure it would have given are unknown, while
|
||||
// those from the readable action are shown. The unread action is
|
||||
// V4_SWAP_ACTION_SHAPES[0].newer, a SWAP_EXACT_IN_SINGLE of WETH to USDC.
|
||||
test("a readable V4 swap action, then an unread one, shows the first one's input and no output", () => {
|
||||
const data = buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
1000000000n,
|
||||
400000000000000000n,
|
||||
),
|
||||
V4_SWAP_ACTION_SHAPES[0].newer,
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
|
||||
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
"Unknown (could not be decoded)",
|
||||
);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("an unread V4 swap action, then a readable one, shows the last one's output and no input", () => {
|
||||
const data = buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
]),
|
||||
[
|
||||
V4_SWAP_ACTION_SHAPES[0].newer,
|
||||
encodeV4ExactInSingle(
|
||||
USDC_ADDR,
|
||||
DAI_ADDR,
|
||||
0n,
|
||||
990000000000000000000n,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
expect(detail(result, "Token In").value).toBe(
|
||||
"Unknown (could not be decoded)",
|
||||
);
|
||||
expect(detail(result, "Amount")).toBeUndefined();
|
||||
expect(detail(result, "Token Out").address).toBe(DAI_ADDR);
|
||||
expect(detail(result, "Min. received").value).toBe("990.0000 DAI");
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/415 — the router's V2
|
||||
// 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