fix: the swap decoder shows the ETH of a WRAP_ETH that does not set the input side (closes #721) #725
@@ -1365,9 +1365,14 @@ when both tokens are known. The lines are:
|
||||
the sender that the `Amount` line does not include. The same three lines show
|
||||
what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH
|
||||
to a recipient other than the router. When its amount gets these lines, its
|
||||
`Moved to` line names that recipient, or reads `The sender`. ETH that the
|
||||
sender pays a V4 step can get these three lines when the router may hold ETH
|
||||
from an earlier step. When the decoder knows the amount, its
|
||||
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
||||
after the step that sets `Token In` takes the amount it states from the
|
||||
sender. When it pays anyone but the router, that amount gets these lines. When
|
||||
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
||||
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states
|
||||
no amount, so it gets no lines. ETH that the sender pays a V4 step or a
|
||||
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
||||
hold ETH from an earlier step. When the decoder knows the amount, its
|
||||
`Moved from the sender` line reads `Up to` and the amount.
|
||||
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
||||
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
|
||||
|
||||
+30
-7
@@ -1368,7 +1368,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// After the loop each joins the Amount line or gets lines of its own
|
||||
// (see permitStands). A take with `atMost` set may take less than
|
||||
// `amount`: it never joins the Amount line, and its line reads
|
||||
// "Up to" and the amount.
|
||||
// "Up to" and the amount. A take with a `recipient`, the ETH of a
|
||||
// later WRAP_ETH that pays anyone but the router, never joins the
|
||||
// Amount line either: its Moved to line names that recipient.
|
||||
const userPaid = [];
|
||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
||||
// of `token` from the sender, or at most that when `atMost` is set. A
|
||||
@@ -1380,8 +1382,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
};
|
||||
// A step may take from the sender what this decoder did not read: a
|
||||
// V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it
|
||||
// cannot read; a V2 or V3 swap step or PERMIT2_TRANSFER_FROM whose
|
||||
// input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
|
||||
// cannot read; a V2 or V3 swap step, PERMIT2_TRANSFER_FROM or WRAP_ETH
|
||||
// whose input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
|
||||
// EXECUTE_SUB_PLAN; and a step it fails on. It is recorded with a null
|
||||
// token and amount: one line says so, there is no Amount line, whose
|
||||
// figure may then be too low, and the amount of a permit after it
|
||||
@@ -1907,6 +1909,25 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!isRouter(w.recipient)) {
|
||||
wrapEthRecipient = w.recipient;
|
||||
}
|
||||
} else if (
|
||||
w.amount > 0n &&
|
||||
w.amount !== CONTRACT_BALANCE
|
||||
) {
|
||||
// The ETH of a stated amount comes from the
|
||||
// transaction's value. ETH an earlier step left in
|
||||
// the router may pay part of it, and the sender
|
||||
// then pays at most that amount. CONTRACT_BALANCE
|
||||
// states no amount to take.
|
||||
userPaid.push({
|
||||
token: "0x0000000000000000000000000000000000000000",
|
||||
amount: w.amount,
|
||||
atMost: heldByRouter.has(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
),
|
||||
recipient: isRouter(w.recipient)
|
||||
? null
|
||||
: w.recipient,
|
||||
});
|
||||
}
|
||||
addPayee(w.recipient);
|
||||
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||
@@ -1931,6 +1952,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
}
|
||||
} else if (command === "V4_SWAP") {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
@@ -2203,8 +2225,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// added to it while both are quantities. A take that cannot be added,
|
||||
// being of another token, coming after an amount that is not a
|
||||
// quantity (a V4 open delta, ALREADY_PAID, or none stated), or having
|
||||
// `atMost` set, gets lines of its own. One whose amount is not known
|
||||
// also leaves the total unknown, with no figure.
|
||||
// `atMost` or a `recipient` set, gets lines of its own. One whose
|
||||
// amount is not known also leaves the total unknown, with no figure.
|
||||
//
|
||||
// A PERMIT2_PERMIT names Token In only when its amount stands as the
|
||||
// Amount figure in place of the total, which it does only in the
|
||||
@@ -2245,7 +2267,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// { token, amount, atMost, recipient }. A null token is a step this
|
||||
// decoder did not read (see spentUnread()), or a token it does not
|
||||
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
|
||||
// pays elsewhere has a recipient (see wrapEthRecipient).
|
||||
// pays elsewhere has a recipient (see wrapEthRecipient and userPaid).
|
||||
const spentFromSender = [];
|
||||
let total = null;
|
||||
// The takes from the sender that `total` adds up, each a quantity, in
|
||||
@@ -2292,7 +2314,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
ofTokenIn &&
|
||||
typeof total === "bigint" &&
|
||||
typeof t.amount === "bigint" &&
|
||||
!t.atMost
|
||||
!t.atMost &&
|
||||
!present(t.recipient)
|
||||
) {
|
||||
total += t.amount;
|
||||
inTotal.push(t);
|
||||
|
||||
@@ -2593,6 +2593,9 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "1.0000 ETH"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", OTHER_ADDR],
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
@@ -2622,6 +2625,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
@@ -2753,6 +2757,172 @@ describe("uniswap decoder", () => {
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/721 — a WRAP_ETH that
|
||||
// does not set the input side takes the amount it states from the
|
||||
// sender. On next no line gave that ETH. The rows marked "reads as on
|
||||
// next" state no amount to take from the sender. Uniswap's own routes
|
||||
// wrap CONTRACT_BALANCE after their first step, as the last two rows
|
||||
// do.
|
||||
const CONTRACT_BALANCE = 2n ** 255n;
|
||||
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_TO_OTHER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to address(1)",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(MSG_SENDER, ONE_ETH)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The sender"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to address(2)",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to the router by its own address",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x0b, 0x0b],
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_TO_OTHER],
|
||||
lines: [["Amount", "1.0000 ETH"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "two WRAP_ETHs of 1 ETH into the router, which the Amount line adds up",
|
||||
commands: [0x0b, 0x0b],
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_INTO_ROUTER],
|
||||
lines: [["Amount", "2.0000 ETH"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x00, 0x0c, 0x0b],
|
||||
inputs: [
|
||||
encodeV3SwapExactIn(ADDRESS_THIS, USDT_600, 1n, [
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
]),
|
||||
UNWRAP_INTO_ROUTER,
|
||||
WRAP_TO_OTHER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", OTHER_ADDR],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a WRAP_ETH of 1 ETH into the router",
|
||||
commands: [0x0b, 0x0c, 0x0b],
|
||||
inputs: [
|
||||
WRAP_INTO_ROUTER,
|
||||
UNWRAP_INTO_ROUTER,
|
||||
WRAP_INTO_ROUTER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "1.0000 ETH"],
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x0a, 0x00, 0x0b],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||
V3_600,
|
||||
WRAP_TO_OTHER,
|
||||
],
|
||||
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH to another address, then an EXECUTE_SUB_PLAN",
|
||||
commands: [0x00, 0x0b, 0x21],
|
||||
inputs: [V3_600, WRAP_TO_OTHER, SUB_PLAN],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
...wrapLines(OTHER_ADDR),
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH whose input this decoder cannot read",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, "0x1234"],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of nothing to another address, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap of 600 USDT that takes ETH into the router, a WRAP_ETH of CONTRACT_BALANCE into the router, then a V3 exact-in of it, reads as on next",
|
||||
commands: [0x10, 0x0b, 0x00],
|
||||
inputs: [
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
ZERO_ADDR,
|
||||
USDT_600,
|
||||
1n,
|
||||
),
|
||||
settle(USDT_ADDR, 0n, true),
|
||||
take(ZERO_ADDR, ADDRESS_THIS, 0n),
|
||||
],
|
||||
),
|
||||
encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||
coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[
|
||||
USER_ADDR,
|
||||
CONTRACT_BALANCE,
|
||||
1n,
|
||||
encodeV3Path([WETH_ADDR, USDC_ADDR]),
|
||||
false,
|
||||
],
|
||||
),
|
||||
],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
])("$kind", ({ commands, inputs, lines }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
|
||||
// USDT for USDC.
|
||||
const v3ExactOut = (amountInMax) =>
|
||||
|
||||
@@ -370,7 +370,8 @@ const PAID_ELSEWHERE = [
|
||||
"Steps",
|
||||
"Deadline",
|
||||
];
|
||||
// `lines` with the lines of one PERMIT2_TRANSFER_FROM after Amount.
|
||||
// `lines` with the lines of one PERMIT2_TRANSFER_FROM or later WRAP_ETH after
|
||||
// Amount.
|
||||
const withMove = (lines) =>
|
||||
lines.flatMap((l) =>
|
||||
l === "Amount"
|
||||
@@ -985,7 +986,7 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
);
|
||||
|
||||
// The SWEEP pays the user the swap's WETH, so the UNWRAP_WETH unwraps the
|
||||
// WRAP_ETH's.
|
||||
// WRAP_ETH's. The WRAP_ETH's 1 ETH gets lines of its own.
|
||||
test("a SWEEP of the swap's WETH to the user, then a WRAP_ETH into the router, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
@@ -996,7 +997,10 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(labels(result)).toEqual(NO_MINIMUM_TO_USER);
|
||||
expect(labels(result)).toEqual(withMove(NO_MINIMUM_TO_USER));
|
||||
expect(detail(result, "Moved from the sender").value).toBe(
|
||||
"1.0000 ETH",
|
||||
);
|
||||
});
|
||||
|
||||
test("an UNWRAP_WETH into the router, then a WRAP_ETH of all its ETH to the user, reads as WETH with no minimum", () => {
|
||||
@@ -1097,7 +1101,9 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// The user receives 0.5 WETH and the rest of the swap's output as ETH.
|
||||
// The user receives 0.5 WETH and the rest of the swap's output as ETH. The
|
||||
// router holds ETH when the WRAP_ETH runs, so the sender pays at most its
|
||||
// 0.5 ETH.
|
||||
test("an UNWRAP_WETH into the router, then a WRAP_ETH of 0.5 ETH to the user, reads as WETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
@@ -1108,7 +1114,11 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(labels(result)).toEqual(NO_MINIMUM_TO_USER);
|
||||
expect(labels(result)).toEqual(withMove(NO_MINIMUM_TO_USER));
|
||||
expect(detail(result, "Moved from the sender").value).toBe(
|
||||
"Up to 0.5000 ETH",
|
||||
);
|
||||
expect(detail(result, "Moved to").value).toBe("The sender");
|
||||
});
|
||||
|
||||
// The UNWRAP_WETH pays the user the swap's WETH as ETH; the WRAP_ETH wraps
|
||||
@@ -1348,6 +1358,7 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 ETH",
|
||||
},
|
||||
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
||||
// still owes of its currency when it runs.
|
||||
@@ -1516,8 +1527,8 @@ describe("any token left in the router is not received", () => {
|
||||
"Moved from the sender",
|
||||
]
|
||||
: ["Amount"]),
|
||||
// A later swap step the user pays for spends `spent` of a token
|
||||
// other than Token In.
|
||||
// A later swap step or WRAP_ETH the user pays for spends `spent`
|
||||
// of a token other than Token In.
|
||||
...(spent
|
||||
? ["Moved from the sender", "Token moved", "Moved to"]
|
||||
: []),
|
||||
|
||||
Reference in New Issue
Block a user