fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
The swap's Amount line took only the first swap step's amount. Each later V2, V3 or V4 swap step the user pays for now adds what it spends of Token In to that line, and one that spends another token gets Moved from the sender, Token moved and Moved to lines of its own. Steps paid from the router add nothing, and a PERMIT2_PERMIT keeps its own amount. Model: opus-5-5
This commit is contained in:
@@ -1189,14 +1189,8 @@ number. They can read:
|
||||
`amountOut`, the exact amount it buys.
|
||||
- `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
|
||||
whole open delta", so the calldata states no quantity. The line shows the
|
||||
amount of the step that sets the input side: 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 that names an input token or amount. A V4 step with a swap
|
||||
action sets the input side when no earlier step set it, even when it names
|
||||
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.
|
||||
whole open delta", so the calldata states no quantity. Transaction Decoding
|
||||
below gives the steps whose amounts the line shows.
|
||||
- `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
|
||||
@@ -2374,10 +2368,11 @@ view would leave a wallet one click from deletion.
|
||||
- Site origin (bold, scheme and port included) + "wants to send a
|
||||
transaction"
|
||||
- Decoded action (if calldata is recognized): action name, token details,
|
||||
amounts, what each `PERMIT2_TRANSFER_FROM` moves and where to, steps,
|
||||
deadline, and unless a swap pays only the From address, a `Paid to` line
|
||||
for each other address it pays, or saying that a token stays in the router
|
||||
or who it pays is unknown (see Transaction Decoding)
|
||||
amounts, what each swap step the user pays for spends of a token other
|
||||
than `Token In`, what each `PERMIT2_TRANSFER_FROM` moves and where to,
|
||||
steps, deadline, and unless a swap pays only the From address, a `Paid to`
|
||||
line for each other address it pays, or saying that a token stays in the
|
||||
router or who it pays is unknown (see Transaction Decoding)
|
||||
- From: color dot + full address + etherscan link
|
||||
- Contract: color dot + full address + etherscan link, token symbol label if
|
||||
known; for a contract creation, the same sentence as on WaitTx
|
||||
@@ -3127,24 +3122,48 @@ Currently supported:
|
||||
name); and one more reads `Unknown (could not be decoded)` when a step that
|
||||
may pay an address went unread. When no step pays anyone, a single line reads
|
||||
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
||||
minimum reads `Min. paid out`. After the `Amount` line (after `Token In` when
|
||||
there is none), each `PERMIT2_TRANSFER_FROM` but one into the router, in step
|
||||
order, gives three lines: `Moved from the sender`, the amount it moves, for
|
||||
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
|
||||
`Token In` line does; and `Moved to`, which reads
|
||||
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
|
||||
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
||||
move into the router needs none of them, because its `Paid to` line and
|
||||
`Token moved into the router` line already give the amount, the token and
|
||||
where it goes, and so its amount is shown once. These lines change neither the
|
||||
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
|
||||
does not decode as an address, or when it is a command the decoder does not
|
||||
read: every command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
|
||||
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
|
||||
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
|
||||
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
|
||||
(`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and
|
||||
the NFT commands among them), or a type its release names no command for.
|
||||
minimum reads `Min. paid out`. The `Amount` line gives the amount of the step
|
||||
that sets the input side: 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 that names an input token or amount. A V4 step with a swap action sets
|
||||
the input side when no earlier step set it, even when it names neither (see
|
||||
Display Consistency). A V2 exact-out step gives its `amountInMax`, and a V4
|
||||
swap step the `amountIn` of the swap action that spends its input currency.
|
||||
Each later swap step that the user pays for also takes its amount, found in
|
||||
the same way, from the user's wallet: a V2 or V3 exact-in or V2 exact-out step
|
||||
whose `payerIsUser` is true, or a V4 step with a swap action whose input
|
||||
currency a `SETTLE` paid by the user (`payerIsUser` true) or a `SETTLE_ALL`
|
||||
names. When it spends `Token In`, its amount is added to the line's, unless a
|
||||
`PERMIT2_PERMIT` set the input side, whose amount the line keeps. So a V3
|
||||
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user,
|
||||
read `1000.0000 USDT`. When either amount is not a quantity (a V4 open delta,
|
||||
`ALREADY_PAID`, or an amount the decoder did not read), the total is not known
|
||||
and there is no `Amount` line. A step paid from the router spends what the
|
||||
router holds and adds nothing, and so does a V2 exact-in whose `amountIn` is
|
||||
`ALREADY_PAID`, which takes nothing from the user. A step the user pays for
|
||||
that spends a token other than `Token In` gives three lines after the `Amount`
|
||||
line (after `Token In` when there is none), in step order:
|
||||
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
|
||||
`Unknown (could not be decoded)` when the decoder read none; `Token moved`,
|
||||
giving the token's address as the `Token In` line does; and `Moved to`, which
|
||||
reads `The swap step that spends it`. Then each `PERMIT2_TRANSFER_FROM` but
|
||||
one into the router, in step order, gives three lines:
|
||||
`Moved from the sender`, the amount it moves, for example `1000000.0000 USDT`;
|
||||
`Token moved`, giving the token's address as the `Token In` line does; and
|
||||
`Moved to`, which reads `The V2 pair the next swap spends` for a hop and
|
||||
`The sender` for `address(1)` on every router but `UniversalRouterV1`, and
|
||||
otherwise gives the address. A move into the router needs none of them,
|
||||
because its `Paid to` line and `Token moved into the router` line already give
|
||||
the amount, the token and where it goes, and so its amount is shown once.
|
||||
These lines change neither the `Paid to` lines nor the minimum's label. A step
|
||||
goes unread when its recipient does not decode as an address, or when it is a
|
||||
command the decoder does not read: every command other than `PERMIT2_PERMIT`,
|
||||
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
|
||||
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
|
||||
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
|
||||
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
|
||||
`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
|
||||
them), or a type its release names no command for.
|
||||
|
||||
### Non-Goals Forever
|
||||
|
||||
|
||||
+114
-33
@@ -335,7 +335,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
||||
// commands this decoder does not read. That action names both of its
|
||||
// currencies, and those commands may name one, so saying the calldata did not
|
||||
// name it would be false. The Paid to line uses it too, when a step may pay an
|
||||
// address this decoder did not read.
|
||||
// address this decoder did not read, and so does the amount a swap step the
|
||||
// user pays for spends of another token, when this decoder read none.
|
||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||
|
||||
// The Paid to line for each token paid to the router, the swap's output or
|
||||
@@ -566,6 +567,7 @@ function decodeV2SwapExactIn(input) {
|
||||
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
||||
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
||||
// address[] path, bool payerIsUser)
|
||||
// With `payerIsUser` false the router pays the input from its own balance.
|
||||
function decodeV2SwapExactOut(input) {
|
||||
try {
|
||||
const d = coder.decode(
|
||||
@@ -578,6 +580,7 @@ function decodeV2SwapExactOut(input) {
|
||||
amountInMax: d[2],
|
||||
tokenIn: d[3][0],
|
||||
tokenOut: d[3][d[3].length - 1],
|
||||
payerIsUser: d[4],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -904,7 +907,9 @@ function totalAmount(amounts) {
|
||||
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
||||
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
||||
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
||||
// decoder cannot tell (see below).
|
||||
// decoder cannot tell (see below). `payerIsUser` is true when the user pays
|
||||
// the step's Token In: a SETTLE paid by the user (`payerIsUser` true) or a
|
||||
// SETTLE_ALL, which V4Router always has the user pay, names it.
|
||||
function decodeV4Swap(input) {
|
||||
try {
|
||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||
@@ -919,6 +924,8 @@ function decodeV4Swap(input) {
|
||||
const routerSettles = [];
|
||||
// The currency of every SETTLE and SETTLE_ALL.
|
||||
const settledCurrencies = [];
|
||||
// The currency of every SETTLE and SETTLE_ALL the user pays.
|
||||
const settledByUser = [];
|
||||
// The index of the step's last swap action, read or not, or -1.
|
||||
let lastSwapAction = -1;
|
||||
// Set when an action of the step could not be read.
|
||||
@@ -943,7 +950,9 @@ function decodeV4Swap(input) {
|
||||
);
|
||||
settleToken = s[0];
|
||||
settledCurrencies.push(s[0]);
|
||||
if (!s[2]) {
|
||||
if (s[2]) {
|
||||
settledByUser.push(s[0]);
|
||||
} else {
|
||||
routerSettles.push({
|
||||
currency: s[0],
|
||||
amount: s[1],
|
||||
@@ -955,6 +964,7 @@ function decodeV4Swap(input) {
|
||||
const s = coder.decode(["address", "uint256"], params[i]);
|
||||
settleToken = s[0];
|
||||
settledCurrencies.push(s[0]);
|
||||
settledByUser.push(s[0]);
|
||||
} else if (actionId === V4_TAKE) {
|
||||
// TAKE: (address currency, address recipient, uint256 amount)
|
||||
const t = coder.decode(
|
||||
@@ -1055,6 +1065,7 @@ function decodeV4Swap(input) {
|
||||
recipientUnread,
|
||||
swapUndecoded,
|
||||
hasSwapAction: true,
|
||||
payerIsUser: settledByUser.includes(settleToken),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1097,6 +1108,7 @@ function decodeV4Swap(input) {
|
||||
recipientUnread,
|
||||
swapUndecoded,
|
||||
hasSwapAction: swaps.length > 0,
|
||||
payerIsUser: present(tokenIn) && settledByUser.includes(tokenIn),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -1125,6 +1137,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
let inputToken = null;
|
||||
let inputAmount = null;
|
||||
let inputEstablished = false;
|
||||
let inputFromPermit = false;
|
||||
let outputToken = null;
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
@@ -1135,8 +1148,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const commandNames = [];
|
||||
|
||||
// THE INVARIANT: an amount and the token it is counted in always come
|
||||
// from the same hop. A figure is never rendered against a token that
|
||||
// did not supply it.
|
||||
// from the same hop, or from hops that all spend that token. A figure
|
||||
// is never rendered against a token that did not supply it.
|
||||
//
|
||||
// Both sides are therefore set as a PAIR — never field by field, and
|
||||
// never on truthiness. An address is never falsy once set but an
|
||||
@@ -1152,7 +1165,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// 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.
|
||||
// from a different hop. Each later swap step the user pays for adds
|
||||
// what it spends, in addUserPaid(), the one place the total is formed.
|
||||
const setInput = (token, amount) => {
|
||||
inputToken = present(token) ? token : null;
|
||||
inputAmount = present(amount) ? amount : null;
|
||||
@@ -1170,6 +1184,33 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputEstablished = true;
|
||||
};
|
||||
|
||||
// What each swap step the user pays for spends of a token other than
|
||||
// the input side's, as { token, amount }, in step order.
|
||||
const otherTokensSpent = [];
|
||||
// A swap step the user pays for (`payerIsUser` true), after the input
|
||||
// side is set, takes `amount` of `token` from the sender. Of the input
|
||||
// side's token it adds to the input side's amount, so the Amount line
|
||||
// is what the steps spend of it together; when either amount is not a
|
||||
// quantity, the total is not known and the amount is null. Of another
|
||||
// token it goes in `otherTokensSpent`. A V2 exact-in of ALREADY_PAID
|
||||
// takes nothing from the sender, and a permit's amount stays as the
|
||||
// permit states it.
|
||||
const addUserPaid = (token, amount) => {
|
||||
if (amount === ALREADY_PAID) return;
|
||||
const sameToken =
|
||||
present(inputToken) &&
|
||||
token.toLowerCase() === inputToken.toLowerCase();
|
||||
if (!sameToken) {
|
||||
otherTokensSpent.push({ token, amount });
|
||||
return;
|
||||
}
|
||||
if (inputFromPermit) return;
|
||||
inputAmount =
|
||||
typeof inputAmount === "bigint" && typeof amount === "bigint"
|
||||
? inputAmount + amount
|
||||
: null;
|
||||
};
|
||||
|
||||
// Who the steps pay, in whichever step and of whatever token.
|
||||
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
|
||||
// its own address, holds what it is paid until a later step pays it
|
||||
@@ -1309,7 +1350,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// A permit states both halves itself, so it may replace an
|
||||
// input side an earlier hop established without breaking
|
||||
// the invariant.
|
||||
if (p) setInput(p.token, p.amount);
|
||||
if (p) {
|
||||
setInput(p.token, p.amount);
|
||||
inputFromPermit = true;
|
||||
}
|
||||
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
||||
const p = decodePermit2TransferFrom(inputs[i]);
|
||||
if (!p) {
|
||||
@@ -1375,7 +1419,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||
const s = decodeV3SwapExactIn(inputs[i]);
|
||||
if (s) {
|
||||
setInputOnce(s.tokenIn, s.amountIn);
|
||||
if (!inputEstablished) {
|
||||
setInputOnce(s.tokenIn, s.amountIn);
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountIn);
|
||||
}
|
||||
// Always update output: in multi-step swaps (V3 → V4),
|
||||
// the last swap step determines the final output token
|
||||
// and minimum received amount.
|
||||
@@ -1392,7 +1440,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (command === "V2_SWAP_EXACT_IN") {
|
||||
const s = decodeV2SwapExactIn(inputs[i]);
|
||||
if (s) {
|
||||
setInputOnce(s.tokenIn, s.amountIn);
|
||||
if (!inputEstablished) {
|
||||
setInputOnce(s.tokenIn, s.amountIn);
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountIn);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOutMin);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
@@ -1408,7 +1460,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
hasV2ExactOut = true;
|
||||
const s = decodeV2SwapExactOut(inputs[i]);
|
||||
if (s) {
|
||||
setInputOnce(s.tokenIn, s.amountInMax);
|
||||
if (!inputEstablished) {
|
||||
setInputOnce(s.tokenIn, s.amountInMax);
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountInMax);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOut);
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
@@ -1550,6 +1606,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// other half.
|
||||
if (!inputEstablished) {
|
||||
setInput(v4.tokenIn, v4.amountIn);
|
||||
} else if (v4.payerIsUser) {
|
||||
addUserPaid(v4.tokenIn, v4.amountIn);
|
||||
}
|
||||
outputToken = v4.tokenOut;
|
||||
minOutput = v4.amountOutMin;
|
||||
@@ -1672,37 +1730,43 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
});
|
||||
}
|
||||
|
||||
if (present(inputAmount)) {
|
||||
// Three amounts need no scale to describe and are named rather
|
||||
// than formatted: V4's open delta and V2's already-paid zero,
|
||||
// neither of which is a quantity at all (see OPEN_DELTA and
|
||||
// ALREADY_PAID), and an unbounded permit. Those two tests come
|
||||
// first — the sentinels are not bigints and cannot be compared
|
||||
// with one.
|
||||
let amount;
|
||||
if (inputAmount === OPEN_DELTA) {
|
||||
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||
} else if (inputAmount === ALREADY_PAID) {
|
||||
amount = {
|
||||
// The text of an amount the swap steps spend, for the Amount line and
|
||||
// the lines for other tokens below it. Three amounts need no scale to
|
||||
// describe and are named rather than formatted: V4's open delta and
|
||||
// V2's already-paid zero, neither of which is a quantity at all (see
|
||||
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
||||
// tests come first — the sentinels are not bigints and cannot be
|
||||
// compared with one.
|
||||
const spentText = (amount, info) => {
|
||||
if (amount === OPEN_DELTA) {
|
||||
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||
}
|
||||
if (amount === ALREADY_PAID) {
|
||||
return {
|
||||
raw: ALREADY_PAID_AMOUNT,
|
||||
display: ALREADY_PAID_AMOUNT,
|
||||
};
|
||||
} else if (inputAmount >= MAX_UINT160) {
|
||||
amount = { raw: "Unlimited", display: "Unlimited" };
|
||||
} else if (hasV2ExactOut) {
|
||||
}
|
||||
if (amount >= MAX_UINT160) {
|
||||
return { raw: "Unlimited", display: "Unlimited" };
|
||||
}
|
||||
if (hasV2ExactOut) {
|
||||
// A V2 exact-out swap spends at most this figure, whichever
|
||||
// step set the line (its amountInMax, the WRAP_ETH of a swap
|
||||
// paid in ETH, a permit), so it is said to be a maximum, in
|
||||
// `raw` too: the wait, success and error screens show `raw` as
|
||||
// the transaction's amount.
|
||||
const most = amountText(inputAmount, inInfo);
|
||||
amount = {
|
||||
// steps gave it (an amountInMax, the WRAP_ETH of a swap paid
|
||||
// in ETH, a permit, or several steps' total), so it is said to
|
||||
// be a maximum, in `raw` too: the wait, success and error
|
||||
// screens show `raw` as the transaction's amount.
|
||||
const most = amountText(amount, info);
|
||||
return {
|
||||
raw: "Up to " + most.raw,
|
||||
display: "Up to " + most.display,
|
||||
};
|
||||
} else {
|
||||
amount = amountText(inputAmount, inInfo);
|
||||
}
|
||||
return amountText(amount, info);
|
||||
};
|
||||
|
||||
if (present(inputAmount)) {
|
||||
const amount = spentText(inputAmount, inInfo);
|
||||
details.push({
|
||||
label: "Amount",
|
||||
value: amount.display,
|
||||
@@ -1710,6 +1774,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
});
|
||||
}
|
||||
|
||||
// What each swap step the user pays for spends of another token, in
|
||||
// the shape of the lines below for a PERMIT2_TRANSFER_FROM. An amount
|
||||
// this decoder did not read is said so.
|
||||
for (const s of otherTokensSpent) {
|
||||
details.push({
|
||||
label: "Moved from the sender",
|
||||
value: present(s.amount)
|
||||
? spentText(s.amount, tokenInfo(s.token, sources)).display
|
||||
: UNDECODED_CURRENCY,
|
||||
});
|
||||
details.push(tokenMovedLine("Token moved", s.token, sources));
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value: "The swap step that spends it",
|
||||
});
|
||||
}
|
||||
|
||||
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
|
||||
// where it goes, an address on a line of its own.
|
||||
for (const m of movedFromSender) {
|
||||
|
||||
@@ -1289,6 +1289,194 @@ describe("uniswap decoder", () => {
|
||||
expect(detail(result, "Amount").value).not.toContain("500000000000");
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/625 — a route split across
|
||||
// two swap steps the user pays for, with no PERMIT2_PERMIT, takes both
|
||||
// steps' amounts from the wallet. On next at 671b4d3 the Amount line read
|
||||
// only the first step's 600.0000 USDT.
|
||||
describe("a swap split across steps the user pays for", () => {
|
||||
const USDT_600 = 600000000n;
|
||||
const USDT_400 = 400000000n;
|
||||
|
||||
// The lines from Amount on, as [label, value].
|
||||
function linesFromAmount(result, count) {
|
||||
const lines = result.details.map((d) => [d.label, d.value]);
|
||||
const at = lines.findIndex(([label]) => label === "Amount");
|
||||
return lines.slice(at, at + count);
|
||||
}
|
||||
|
||||
test("a split across a V3 and a V2 step the user pays for shows the total", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token In").address.toLowerCase()).toBe(
|
||||
USDT_ADDR.toLowerCase(),
|
||||
);
|
||||
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||
expect(detail(result, "Amount").rawValue).toBe("1000.0000");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a split across a V3 step and a V4 step the user pays for shows the total", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SETTLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_TAKE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4Settle(USDT_ADDR), // paid by the user
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
USDT_400,
|
||||
390000000n,
|
||||
),
|
||||
encodeV4Take(USDC_ADDR),
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a second step the user pays for that spends another token shows that token's amount", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(
|
||||
USER_ADDR,
|
||||
400000000000000000000n, // 400 DAI
|
||||
390000000n,
|
||||
[DAI_ADDR, USDC_ADDR],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(linesFromAmount(result, 4)).toEqual([
|
||||
["Amount", "600.0000 USDT"],
|
||||
["Moved from the sender", "400.0000 DAI"],
|
||||
["Token moved", "DAI (" + DAI_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
]);
|
||||
});
|
||||
|
||||
// A PERMIT2_PERMIT that sets the input side keeps its own amount: the
|
||||
// steps it lets the router spend do not add to it.
|
||||
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
|
||||
[
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), // 5000 USDT
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("5000.0000 USDT");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
// A V2 exact-out step makes every amount the swap steps spend a maximum,
|
||||
// the total included.
|
||||
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x09]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactOut(USER_ADDR, 390000000n, USDT_400, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
||||
// the shape the issue measured: a V4 step that states a minimum of zero
|
||||
// and names no output currency.
|
||||
|
||||
@@ -671,6 +671,7 @@ describe("any token left in the router is not received", () => {
|
||||
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "2000.0000 USDT",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
|
||||
@@ -697,6 +698,7 @@ describe("any token left in the router is not received", () => {
|
||||
v2Swap(0x08, MSG_SENDER, ONE_WETH, USDC_1990, [WETH, USDC]),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V3 swap of 1 WETH that the user pays for",
|
||||
@@ -705,6 +707,7 @@ describe("any token left in the router is not received", () => {
|
||||
v3SwapExactIn(MSG_SENDER, ONE_WETH, USDC_1990, WETH, USDC),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE of 1 WETH by the user",
|
||||
@@ -717,6 +720,7 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH from the router",
|
||||
@@ -792,6 +796,7 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 WETH",
|
||||
},
|
||||
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
|
||||
// the decoder cannot read, so the SETTLE from the router pays nothing.
|
||||
@@ -873,12 +878,17 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
left: { label: "Token moved into the router", value: "ETH" },
|
||||
},
|
||||
])("$kind", ({ data, left }) => {
|
||||
])("$kind", ({ data, left, spent }) => {
|
||||
const result = decode(data);
|
||||
expect(labels(result)).toEqual([
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
// A later swap step the user pays for spends `spent` of a token
|
||||
// other than Token In (https://git.eeqj.de/sneak/AutistMask/issues/625).
|
||||
...(spent
|
||||
? ["Moved from the sender", "Token moved", "Moved to"]
|
||||
: []),
|
||||
"Token Out",
|
||||
"Paid to",
|
||||
"Token moved into the router",
|
||||
@@ -886,6 +896,9 @@ describe("any token left in the router is not received", () => {
|
||||
"Steps",
|
||||
"Deadline",
|
||||
]);
|
||||
if (spent) {
|
||||
expect(detail(result, "Moved from the sender").value).toBe(spent);
|
||||
}
|
||||
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||
expect(detail(result, "Token moved into the router")).toEqual(left);
|
||||
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||
|
||||
Reference in New Issue
Block a user