fix: the approval screen names the address a swap's output is paid to (closes #594)
The swap decoder read no recipient, so a swap paying its output to another address still showed Min. received. It now collects the address that every swap step, SWEEP, WRAP_ETH, UNWRAP_WETH, V4 TAKE and TAKE_ALL pays, across the whole transaction, leaving out payments to the router itself. The minimum reads Min. received only when some step pays the sender, none pays another address, and no step that may pay an address went unread: a recipient that does not decode, or a command or V4 action the decoder does not read. Otherwise it reads Min. paid out, with a Paid to line for each other address and one saying when who is paid is unknown. The approval and transaction detail screens pass the sender in. Model: opus-5-5
This commit is contained in:
@@ -68,10 +68,13 @@ function swapData(amountIn, amountOutMin) {
|
||||
}
|
||||
|
||||
function swapDetail(amountIn, amountOutMin, label) {
|
||||
// Sent by the address the swap pays, so the output is the user's.
|
||||
const decoded = uniswap.decode(
|
||||
swapData(amountIn, amountOutMin),
|
||||
ROUTER,
|
||||
"0x1",
|
||||
{},
|
||||
RECIPIENT,
|
||||
);
|
||||
return decoded.details.find((d) => d.label === label);
|
||||
}
|
||||
|
||||
+128
-49
@@ -332,7 +332,13 @@ describe("uniswap decoder", () => {
|
||||
});
|
||||
|
||||
test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => {
|
||||
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
FIRST_SWAP_CALLDATA,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
||||
@@ -356,8 +362,10 @@ describe("uniswap decoder", () => {
|
||||
expect(result.details.find((d) => d.label === "Amount").value).toBe(
|
||||
"Unlimited",
|
||||
);
|
||||
// This fixture's TAKE parameters are one byte long, so who it pays
|
||||
// cannot be read, and the minimum is not said to be received.
|
||||
expect(
|
||||
result.details.find((d) => d.label === "Min. received").value,
|
||||
result.details.find((d) => d.label === "Min. paid out").value,
|
||||
).toBe("0.0002 ETH");
|
||||
|
||||
// Real mainnet calldata is in the deployed shape, so its V4 step is
|
||||
@@ -373,6 +381,8 @@ describe("uniswap decoder", () => {
|
||||
v4SwapCalldata(id, deployed),
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe("V4 Swap");
|
||||
},
|
||||
@@ -389,6 +399,8 @@ describe("uniswap decoder", () => {
|
||||
v4SwapCalldata(id, newer),
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Amount")).toBeUndefined();
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
@@ -419,7 +431,13 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
@@ -440,7 +458,13 @@ describe("uniswap decoder", () => {
|
||||
[encodeV4Swap(new Uint8Array([id]), [newer])],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
@@ -470,7 +494,13 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Amount").value).toBe("2.0000 USDT");
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
@@ -498,7 +528,13 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Token In").value).toBe(
|
||||
"Unknown (could not be decoded)",
|
||||
@@ -520,7 +556,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
"Unknown (could not be decoded)",
|
||||
);
|
||||
@@ -540,7 +576,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||
|
||||
@@ -567,7 +603,7 @@ describe("uniswap decoder", () => {
|
||||
1767225600n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Swap USDC → WETH");
|
||||
expect(detail(result, "Token In").address).toBe(USDC_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
|
||||
@@ -599,7 +635,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Swap ETH → USDC");
|
||||
|
||||
const amount = detail(result, "Amount");
|
||||
@@ -628,7 +664,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Swap USDC → ETH");
|
||||
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
@@ -671,7 +707,13 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Token Out").address).toBe(tokenOut);
|
||||
expect(detail(result, "Min. received").value).toBe(minReceived);
|
||||
},
|
||||
@@ -692,7 +734,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
|
||||
@@ -725,7 +767,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
||||
});
|
||||
@@ -744,7 +786,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
|
||||
});
|
||||
@@ -769,7 +811,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
"Unknown (not named in the calldata)",
|
||||
);
|
||||
@@ -792,7 +834,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||
});
|
||||
@@ -804,7 +846,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const tokenIn = result.details.find((d) => d.label === "Token In");
|
||||
@@ -827,12 +869,13 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
// UNWRAP_WETH means output is native ETH
|
||||
expect(result.name).toBe("Swap USDT \u2192 ETH");
|
||||
});
|
||||
|
||||
// No step pays anyone, so the minimum is not said to be received.
|
||||
test("decodes BALANCE_CHECK_ERC20 for min output", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x0b, 0x0e]),
|
||||
@@ -843,10 +886,10 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const minOut = result.details.find((d) => d.label === "Min. received");
|
||||
const minOut = result.details.find((d) => d.label === "Min. paid out");
|
||||
expect(minOut).toBeDefined();
|
||||
expect(minOut.value).toContain("2.0000");
|
||||
expect(minOut.value).toContain("USDT");
|
||||
@@ -863,7 +906,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// The V4 swap action's parameters are empty, so that step is said to
|
||||
@@ -943,7 +986,13 @@ describe("uniswap decoder", () => {
|
||||
[subPlan],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"Execute Sub-Plan (its steps are not shown)",
|
||||
@@ -1031,7 +1080,13 @@ describe("uniswap decoder", () => {
|
||||
"$beside an EXECUTE_SUB_PLAN step does not name either side",
|
||||
({ commands, inputs, steps }) => {
|
||||
const data = buildExecute(commands, inputs, 9999999999n);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe(steps);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Token In").value).toBe(
|
||||
@@ -1046,7 +1101,13 @@ describe("uniswap decoder", () => {
|
||||
);
|
||||
|
||||
test("shows the deadline as a UTC date and time", () => {
|
||||
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
FIRST_SWAP_CALLDATA,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
|
||||
});
|
||||
|
||||
@@ -1064,7 +1125,7 @@ describe("uniswap decoder", () => {
|
||||
2n ** 256n - 1n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Swap USDT \u2192 WETH");
|
||||
expect(detail(result, "Deadline").value).toBe(
|
||||
@@ -1079,7 +1140,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
const amount = result.details.find((d) => d.label === "Amount");
|
||||
@@ -1154,7 +1215,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
|
||||
// After fix: correctly identifies both tokens from V4 sub-actions
|
||||
@@ -1188,7 +1249,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
// The amount and the token it is counted in come from the same hop.
|
||||
@@ -1228,7 +1289,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
@@ -1273,7 +1334,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||
@@ -1302,7 +1363,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||
@@ -1339,7 +1400,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token In").value).toContain("WETH");
|
||||
@@ -1356,9 +1417,12 @@ describe("uniswap decoder", () => {
|
||||
// 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.
|
||||
// `minLabel` is the minimum's line: with no TAKE or TAKE_ALL, no step
|
||||
// pays the user, so it is not said to be received.
|
||||
const V4_STEP_ENDINGS = [
|
||||
{
|
||||
ending: "SETTLE_ALL and TAKE_ALL",
|
||||
minLabel: "Min. received",
|
||||
ids: [V4_SETTLE_ALL_ID, V4_TAKE_ALL_ID],
|
||||
params: [
|
||||
encodeV4SettleAll(USDT_ADDR, 1000000000n),
|
||||
@@ -1367,11 +1431,13 @@ describe("uniswap decoder", () => {
|
||||
},
|
||||
{
|
||||
ending: "SETTLE and TAKE",
|
||||
minLabel: "Min. received",
|
||||
ids: [V4_SETTLE_ID, V4_TAKE_ID],
|
||||
params: [encodeV4Settle(USDT_ADDR), encodeV4Take(USDC_ADDR)],
|
||||
},
|
||||
{
|
||||
ending: "CLOSE_CURRENCY",
|
||||
minLabel: "Min. paid out",
|
||||
ids: [
|
||||
V4_CLOSE_CURRENCY_ID,
|
||||
V4_CLOSE_CURRENCY_ID,
|
||||
@@ -1520,7 +1586,7 @@ describe("uniswap decoder", () => {
|
||||
),
|
||||
)(
|
||||
"a V4 step of $step, then $ending, reads Swap USDT → USDC",
|
||||
({ swaps, ids, params, amount, min }) => {
|
||||
({ swaps, ids, params, amount, min, minLabel }) => {
|
||||
const data = buildExecute(
|
||||
"0x10",
|
||||
[
|
||||
@@ -1532,12 +1598,18 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_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);
|
||||
expect(detail(result, minLabel)?.value).toBe(min);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1584,7 +1656,13 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
@@ -1643,7 +1721,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Token In").address).toBe(
|
||||
USDT_ADDR.toLowerCase(),
|
||||
@@ -1699,7 +1777,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Token In").value).toBe(
|
||||
"Unknown (not named in the calldata)",
|
||||
@@ -1722,7 +1800,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
"Unknown (not named in the calldata)",
|
||||
);
|
||||
@@ -1756,7 +1834,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_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);
|
||||
@@ -1780,7 +1858,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token In").value).toContain("USDT");
|
||||
@@ -1811,7 +1889,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toContain("WETH");
|
||||
@@ -1832,7 +1910,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toContain("USDC");
|
||||
@@ -1840,7 +1918,8 @@ describe("uniswap decoder", () => {
|
||||
});
|
||||
|
||||
// With no minimum stated before it, a zero balance check is what sets the
|
||||
// output side, and it guarantees nothing.
|
||||
// output side, and it guarantees nothing. No step pays anyone, so the
|
||||
// minimum is not said to be received.
|
||||
test("a zero balance check with no earlier minimum states no minimum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x0b, 0x0e]),
|
||||
@@ -1851,11 +1930,11 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token Out").value).toContain("USDT");
|
||||
expect(detail(result, "Min. received").value).toBe(
|
||||
expect(detail(result, "Min. paid out").value).toBe(
|
||||
"None (no minimum guaranteed)",
|
||||
);
|
||||
});
|
||||
@@ -1871,7 +1950,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
|
||||
@@ -1891,7 +1970,7 @@ describe("uniswap decoder", () => {
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
// The address a swap's output is paid to
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/594).
|
||||
//
|
||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH and V4 TAKE names the
|
||||
// address it pays, and TAKE_ALL pays the sender. The decoder read none of
|
||||
// them: a swap whose output went to another address still read "Min.
|
||||
// received", as if the user received it. The router reads address(1) as the
|
||||
// sender and address(2) as the router itself, which holds what it is paid
|
||||
// until a later step pays it out.
|
||||
//
|
||||
// The screens are driven against a minimal DOM stub in the shape
|
||||
// tests/uniswapRouterAddress.test.js uses.
|
||||
|
||||
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
||||
|
||||
jest.mock("../src/shared/log", () => ({
|
||||
log: {
|
||||
debugf: () => {},
|
||||
infof: () => {},
|
||||
warnf: () => {},
|
||||
errorf: () => {},
|
||||
},
|
||||
debugFetch: async (url) => ({
|
||||
ok: true,
|
||||
json: async () => mockExplorerAnswer(url),
|
||||
}),
|
||||
setRuntimeDebug: () => {},
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
const uniswap = require("../src/shared/uniswap");
|
||||
const { state } = require("../src/shared/state");
|
||||
const approval = require("../src/popup/views/approval");
|
||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||
|
||||
// UniversalRouterV2 on Ethereum Mainnet.
|
||||
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||
// UniversalRouterV2_1_2 on Ethereum Mainnet, whose mask reads 0x07 and 0x40.
|
||||
const ROUTER_V2_1_2 = "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85";
|
||||
const USER = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||
const OTHER = "0x7777777777777777777777777777777777777777";
|
||||
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
||||
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||
const ZERO = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
const USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||
const ONE_WETH = 1000000000000000000n;
|
||||
const USDT_2000 = 2000000000n;
|
||||
const USDC_1990 = 1990000000n;
|
||||
|
||||
// The Paid to line's value when no address can be named.
|
||||
const UNDECODED = "Unknown (could not be decoded)";
|
||||
const UNNAMED = "Unknown (not named in the calldata)";
|
||||
|
||||
const coder = AbiCoder.defaultAbiCoder();
|
||||
const routerIface = new Interface([
|
||||
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
||||
]);
|
||||
|
||||
// execute() of `steps`, each [command, input].
|
||||
function execute(...steps) {
|
||||
const commands = steps.map(([command]) => command);
|
||||
return routerIface.encodeFunctionData("execute", [
|
||||
solidityPacked(
|
||||
commands.map(() => "uint8"),
|
||||
commands,
|
||||
),
|
||||
steps.map(([, input]) => input),
|
||||
9999999999n,
|
||||
]);
|
||||
}
|
||||
|
||||
// V2_SWAP_EXACT_IN (0x08) and V2_SWAP_EXACT_OUT (0x09) share one layout.
|
||||
function v2Swap(command, recipient, amount, limit, path) {
|
||||
return [
|
||||
command,
|
||||
coder.encode(
|
||||
["address", "uint256", "uint256", "address[]", "bool"],
|
||||
[recipient, amount, limit, path, true],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function v3SwapExactIn(recipient, amountIn, amountOutMin, tokenIn, tokenOut) {
|
||||
return [
|
||||
0x00,
|
||||
coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[
|
||||
recipient,
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
solidityPacked(
|
||||
["address", "uint24", "address"],
|
||||
[tokenIn, 3000, tokenOut],
|
||||
),
|
||||
true,
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function sweep(token, recipient) {
|
||||
return [
|
||||
0x04,
|
||||
coder.encode(["address", "address", "uint256"], [token, recipient, 0n]),
|
||||
];
|
||||
}
|
||||
|
||||
function wrapEth(recipient, amount) {
|
||||
return [0x0b, coder.encode(["address", "uint256"], [recipient, amount])];
|
||||
}
|
||||
|
||||
function unwrapWeth(recipient) {
|
||||
return [0x0c, coder.encode(["address", "uint256"], [recipient, 0n])];
|
||||
}
|
||||
|
||||
function balanceCheck(token, minBalance) {
|
||||
return [
|
||||
0x0e,
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[USER, token, minBalance],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// `step`, a [command or action, input] pair, with the top byte of the input's
|
||||
// word `index` set. The decoder cannot read that word as an address.
|
||||
function withDirtyWord([id, input], index) {
|
||||
const at = 2 + index * 64;
|
||||
return [id, input.slice(0, at) + "ff" + input.slice(at + 2)];
|
||||
}
|
||||
|
||||
// The V4 actions a step below is built from.
|
||||
const SWAP_USDT_FOR_USDC = [
|
||||
0x06, // SWAP_EXACT_IN_SINGLE
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
[[[USDT, USDC, 100, 1, ZERO], true, USDT_2000, USDC_1990, "0x"]],
|
||||
),
|
||||
];
|
||||
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||
const SETTLE_USDT = [
|
||||
0x0b,
|
||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
||||
];
|
||||
function take(currency, recipient) {
|
||||
return [
|
||||
0x0e,
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[currency, recipient, 0n],
|
||||
),
|
||||
];
|
||||
}
|
||||
function takeAll(currency) {
|
||||
return [0x0f, coder.encode(["address", "uint256"], [currency, 0n])];
|
||||
}
|
||||
|
||||
// A V4_SWAP step of `actions`, each [action, params].
|
||||
function v4Swap(...actions) {
|
||||
return [
|
||||
0x10,
|
||||
coder.encode(
|
||||
["bytes", "bytes[]"],
|
||||
[
|
||||
solidityPacked(
|
||||
actions.map(() => "uint8"),
|
||||
actions.map(([action]) => action),
|
||||
),
|
||||
actions.map(([, params]) => params),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function decode(data, sender = USER, router = ROUTER) {
|
||||
return uniswap.decode(data, router, "0x1", {}, sender);
|
||||
}
|
||||
|
||||
function labels(result) {
|
||||
return result.details.map((d) => d.label);
|
||||
}
|
||||
|
||||
function detail(result, label) {
|
||||
return result.details.find((d) => d.label === label);
|
||||
}
|
||||
|
||||
function paidTo(result) {
|
||||
return result.details
|
||||
.filter((d) => d.label === "Paid to")
|
||||
.map((d) => d.value);
|
||||
}
|
||||
|
||||
// A swap paid to the user, the lines it has always had.
|
||||
const PAID_TO_USER = [
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
"Token Out",
|
||||
"Min. received",
|
||||
"Steps",
|
||||
"Deadline",
|
||||
];
|
||||
const PAID_ELSEWHERE = [
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
"Token Out",
|
||||
"Paid to",
|
||||
"Min. paid out",
|
||||
"Steps",
|
||||
"Deadline",
|
||||
];
|
||||
|
||||
// A swap of 2,000 USDT for at least 1 WETH, paid to `recipient`.
|
||||
const swapToWeth = (recipient) =>
|
||||
v3SwapExactIn(recipient, USDT_2000, ONE_WETH, USDT, WETH);
|
||||
|
||||
beforeEach(() => {
|
||||
state.trackedTokens = [];
|
||||
state.wallets = [];
|
||||
});
|
||||
|
||||
describe("each swap command's recipient", () => {
|
||||
test.each([
|
||||
{
|
||||
kind: "V2_SWAP_EXACT_IN",
|
||||
data: (r) =>
|
||||
execute(v2Swap(0x08, r, USDT_2000, ONE_WETH, [USDT, WETH])),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "V2_SWAP_EXACT_OUT",
|
||||
data: (r) =>
|
||||
execute(v2Swap(0x09, r, ONE_WETH, USDT_2000, [USDT, WETH])),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "V3_SWAP_EXACT_IN",
|
||||
data: (r) => execute(swapToWeth(r)),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "V4_SWAP with TAKE",
|
||||
data: (r) =>
|
||||
execute(v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, take(USDC, r))),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
])(
|
||||
"$kind: paid to the user it reads as before; paid to another address it names that address",
|
||||
({ data, min }) => {
|
||||
const viaPlaceholder = decode(data(MSG_SENDER));
|
||||
expect(labels(viaPlaceholder)).toEqual(PAID_TO_USER);
|
||||
expect(detail(viaPlaceholder, "Min. received").value).toBe(min);
|
||||
// The sender's own address, in any case, reads the same.
|
||||
expect(decode(data(USER), USER.toLowerCase()).details).toEqual(
|
||||
viaPlaceholder.details,
|
||||
);
|
||||
|
||||
const toOther = decode(data(OTHER));
|
||||
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
expect(detail(toOther, "Min. paid out").value).toBe(min);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("an output the router holds", () => {
|
||||
const toRouter = v3SwapExactIn(
|
||||
ADDRESS_THIS,
|
||||
USDT_2000,
|
||||
USDC_1990,
|
||||
USDT,
|
||||
USDC,
|
||||
);
|
||||
|
||||
test("SWEEP pays it to the SWEEP's recipient", () => {
|
||||
const direct = decode(
|
||||
execute(
|
||||
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||
),
|
||||
);
|
||||
const toUser = decode(execute(toRouter, sweep(USDC, MSG_SENDER)));
|
||||
expect(labels(toUser)).toEqual(PAID_TO_USER);
|
||||
const withoutSteps = (r) =>
|
||||
r.details.filter((d) => d.label !== "Steps");
|
||||
expect(withoutSteps(toUser)).toEqual(withoutSteps(direct));
|
||||
|
||||
const toOther = decode(execute(toRouter, sweep(USDC, OTHER)));
|
||||
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
expect(detail(toOther, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||
});
|
||||
|
||||
test("with nothing paying it out, no one is named as paid", () => {
|
||||
const result = decode(execute(toRouter));
|
||||
expect(labels(result)).toEqual(PAID_ELSEWHERE);
|
||||
expect(paidTo(result)).toEqual([UNNAMED]);
|
||||
});
|
||||
|
||||
test("the router's own address as a recipient is read as address(2) is", () => {
|
||||
const toRouterAddress = v3SwapExactIn(
|
||||
ROUTER,
|
||||
USDT_2000,
|
||||
USDC_1990,
|
||||
USDT,
|
||||
USDC,
|
||||
);
|
||||
expect(paidTo(decode(execute(toRouterAddress)))).toEqual([UNNAMED]);
|
||||
expect(
|
||||
labels(decode(execute(toRouterAddress, sweep(USDC, MSG_SENDER)))),
|
||||
).toEqual(PAID_TO_USER);
|
||||
});
|
||||
|
||||
test("UNWRAP_WETH pays its WETH, as ETH, to the UNWRAP_WETH's recipient", () => {
|
||||
const toWeth = swapToWeth(ADDRESS_THIS);
|
||||
|
||||
const toUser = decode(execute(toWeth, unwrapWeth(MSG_SENDER)));
|
||||
expect(labels(toUser)).toEqual(PAID_TO_USER);
|
||||
expect(detail(toUser, "Token Out").value).toBe("ETH");
|
||||
expect(detail(toUser, "Min. received").value).toBe("1.0000 ETH");
|
||||
|
||||
const toOther = decode(execute(toWeth, unwrapWeth(OTHER)));
|
||||
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
expect(detail(toOther, "Min. paid out").value).toBe("1.0000 ETH");
|
||||
});
|
||||
|
||||
test("WRAP_ETH pays its WETH to the WRAP_ETH's recipient", () => {
|
||||
const toUser = decode(
|
||||
execute(wrapEth(ADDRESS_THIS, ONE_WETH), unwrapWeth(MSG_SENDER)),
|
||||
);
|
||||
expect(detail(toUser, "Token Out").value).toBe("ETH");
|
||||
expect(paidTo(toUser)).toEqual([]);
|
||||
|
||||
const toOther = decode(
|
||||
execute(wrapEth(OTHER, ONE_WETH), unwrapWeth(MSG_SENDER)),
|
||||
);
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
});
|
||||
|
||||
test("a V4 TAKE to the router, then SWEEP, pays the SWEEP's recipient", () => {
|
||||
const step = v4Swap(
|
||||
SWAP_USDT_FOR_USDC,
|
||||
SETTLE_USDT,
|
||||
take(USDC, ADDRESS_THIS),
|
||||
);
|
||||
|
||||
const toUser = decode(execute(step, sweep(USDC, MSG_SENDER)));
|
||||
expect(labels(toUser)).toEqual(PAID_TO_USER);
|
||||
|
||||
const toOther = decode(execute(step, sweep(USDC, OTHER)));
|
||||
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("every step's payee counts, whichever step sets the output", () => {
|
||||
test("TAKE_ALL pays the user; a TAKE to another address beside it is named", () => {
|
||||
const toUser = decode(
|
||||
execute(v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, takeAll(USDC))),
|
||||
);
|
||||
expect(labels(toUser)).toEqual(PAID_TO_USER);
|
||||
|
||||
// TAKE with an amount of zero takes the whole open credit, so
|
||||
// TAKE_ALL after it pays the user nothing.
|
||||
const split = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_USDC,
|
||||
SETTLE_USDT,
|
||||
take(USDC, OTHER),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(paidTo(split)).toEqual([OTHER]);
|
||||
expect(detail(split, "Min. received")).toBeUndefined();
|
||||
expect(detail(split, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||
});
|
||||
|
||||
test("a swap paid to another address, then a V4 step that only takes the same token", () => {
|
||||
const emptyStep = v4Swap(takeAll(WETH));
|
||||
|
||||
const result = decode(execute(swapToWeth(OTHER), emptyStep));
|
||||
expect(paidTo(result)).toEqual([OTHER]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
|
||||
const checked = decode(
|
||||
execute(swapToWeth(OTHER), emptyStep, balanceCheck(WETH, ONE_WETH)),
|
||||
);
|
||||
expect(paidTo(checked)).toEqual([OTHER]);
|
||||
expect(detail(checked, "Min. received")).toBeUndefined();
|
||||
expect(detail(checked, "Min. paid out").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
test("a swap paid to the router, a V4 step that only takes, then SWEEP to another address", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v3SwapExactIn(ADDRESS_THIS, USDT_2000, USDC_1990, USDT, USDC),
|
||||
v4Swap(takeAll(USDC)),
|
||||
sweep(USDC, OTHER),
|
||||
),
|
||||
);
|
||||
expect(paidTo(result)).toEqual([OTHER]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a swap paid to another address, then a small swap paid to the user", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(OTHER),
|
||||
v2Swap(0x08, MSG_SENDER, 1000000n, 1n, [USDT, WETH]),
|
||||
),
|
||||
);
|
||||
expect(paidTo(result)).toEqual([OTHER]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeDefined();
|
||||
});
|
||||
|
||||
test("one V4 step: a TAKE of what it buys to another address, then TAKE_ALL of another currency", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_USDC,
|
||||
SETTLE_USDT,
|
||||
take(USDC, OTHER),
|
||||
takeAll(USDT),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(paidTo(result)).toEqual([OTHER]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("a payout that cannot be read is never said to be received", () => {
|
||||
test("a V4 TAKE whose recipient word has its top byte set", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_USDC,
|
||||
SETTLE_USDT,
|
||||
withDirtyWord(take(USDC, OTHER), 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(paidTo(result)).toEqual([UNDECODED]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
kind: "V2_SWAP_EXACT_IN",
|
||||
step: v2Swap(0x08, OTHER, 1n, 1n, [USDT, WETH]),
|
||||
},
|
||||
{ kind: "V3_SWAP_EXACT_IN", step: swapToWeth(OTHER) },
|
||||
{ kind: "SWEEP", step: sweep(WETH, OTHER), word: 1 },
|
||||
{ kind: "WRAP_ETH", step: wrapEth(OTHER, 1n) },
|
||||
{ kind: "UNWRAP_WETH", step: unwrapWeth(OTHER) },
|
||||
])(
|
||||
"a $kind whose recipient word has its top byte set",
|
||||
({ step, word = 0 }) => {
|
||||
const result = decode(
|
||||
execute(swapToWeth(MSG_SENDER), withDirtyWord(step, word)),
|
||||
);
|
||||
expect(paidTo(result)).toEqual([UNDECODED]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeDefined();
|
||||
},
|
||||
);
|
||||
|
||||
test("an output set only by BALANCE_CHECK_ERC20", () => {
|
||||
const result = decode(execute(balanceCheck(WETH, ONE_WETH)));
|
||||
expect(paidTo(result)).toEqual([UNNAMED]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/599 is to read these. Until
|
||||
// then each is a payout this decoder does not read.
|
||||
const transferShape = (command) => [
|
||||
command,
|
||||
coder.encode(["address", "address", "uint256"], [WETH, OTHER, 1n]),
|
||||
];
|
||||
test.each([
|
||||
{ kind: "PERMIT2_TRANSFER_FROM", step: transferShape(0x02) },
|
||||
{ kind: "TRANSFER", step: transferShape(0x05) },
|
||||
{ kind: "PAY_PORTION", step: transferShape(0x06) },
|
||||
{ kind: "PAY_PORTION_FULL_PRECISION", step: transferShape(0x07) },
|
||||
{
|
||||
kind: "PERMIT2_TRANSFER_FROM_BATCH",
|
||||
step: [
|
||||
0x0d,
|
||||
coder.encode(
|
||||
["tuple(address,address,uint160,address)[]"],
|
||||
[[[USER, OTHER, 1n, WETH]]],
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "V3_SWAP_EXACT_OUT",
|
||||
step: [0x01, swapToWeth(OTHER)[1]],
|
||||
},
|
||||
{
|
||||
kind: "EXECUTE_SUB_PLAN",
|
||||
step: [
|
||||
0x21,
|
||||
coder.encode(
|
||||
["bytes", "bytes[]"],
|
||||
["0x05", [transferShape(0x05)[1]]],
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "ACROSS_V4_DEPOSIT_V3",
|
||||
step: [0x40, coder.encode(["address"], [OTHER])],
|
||||
},
|
||||
{
|
||||
kind: "V4 TAKE_PORTION",
|
||||
step: v4Swap([
|
||||
0x10,
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[WETH, OTHER, 10000n],
|
||||
),
|
||||
]),
|
||||
},
|
||||
])("a swap paid to the user, then $kind", ({ step }) => {
|
||||
const result = decode(
|
||||
execute(swapToWeth(MSG_SENDER), step),
|
||||
USER,
|
||||
ROUTER_V2_1_2,
|
||||
);
|
||||
expect(paidTo(result)).toEqual([UNDECODED]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("paths whose recipients were untested", () => {
|
||||
test("BALANCE_CHECK_ERC20 after a swap leaves who is paid as the swap says", () => {
|
||||
const check = balanceCheck(WETH, ONE_WETH);
|
||||
|
||||
const toUser = decode(execute(swapToWeth(MSG_SENDER), check));
|
||||
expect(paidTo(toUser)).toEqual([]);
|
||||
expect(detail(toUser, "Min. received").value).toBe("1.0000 WETH");
|
||||
|
||||
const toOther = decode(execute(swapToWeth(OTHER), check));
|
||||
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||
expect(detail(toOther, "Min. paid out").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
test("a V4 step whose swap action cannot be read still has its TAKE read", () => {
|
||||
const step = (r) => v4Swap(UNREADABLE_SWAP, SETTLE_USDT, take(USDC, r));
|
||||
|
||||
const toUser = decode(execute(step(MSG_SENDER)));
|
||||
expect(detail(toUser, "Steps").value).toBe(
|
||||
"V4 Swap (could not be decoded)",
|
||||
);
|
||||
expect(paidTo(toUser)).toEqual([]);
|
||||
|
||||
expect(paidTo(decode(execute(step(OTHER))))).toEqual([OTHER]);
|
||||
});
|
||||
});
|
||||
|
||||
// The DOM stub both screens are drawn into.
|
||||
function makeElement(id) {
|
||||
const classes = new Set();
|
||||
const el = {
|
||||
id,
|
||||
textContent: "",
|
||||
value: "",
|
||||
innerHTML: "",
|
||||
style: {},
|
||||
dataset: {},
|
||||
classList: {
|
||||
add: (...names) => names.forEach((n) => classes.add(n)),
|
||||
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
||||
contains: (n) => classes.has(n),
|
||||
toggle: () => false,
|
||||
},
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
appendChild: () => {},
|
||||
};
|
||||
Object.defineProperty(el, "parentElement", {
|
||||
get: () => node(id + "-parent"),
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeDocument() {
|
||||
const els = new Map();
|
||||
return {
|
||||
getElementById(id) {
|
||||
if (id === "debug-banner") return null;
|
||||
if (!els.has(id)) els.set(id, makeElement(id));
|
||||
return els.get(id);
|
||||
},
|
||||
createElement: () => makeElement("created"),
|
||||
body: { prepend: () => {} },
|
||||
};
|
||||
}
|
||||
|
||||
function node(id) {
|
||||
return globalThis.document.getElementById(id);
|
||||
}
|
||||
|
||||
// A swap of 2,000 USDT for at least 1 WETH, paid to `recipient`.
|
||||
const swapPaidTo = (recipient) => execute(swapToWeth(recipient));
|
||||
|
||||
// Open the approval screen for a transaction from USER sending `data` to the
|
||||
// router.
|
||||
async function showApproval(data) {
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" }, close: () => {} };
|
||||
globalThis.chrome.runtime = {
|
||||
connect: () => ({ postMessage: () => {} }),
|
||||
sendMessage: (_msg, reply) =>
|
||||
reply({
|
||||
type: "tx",
|
||||
origin: "https://dapp.example",
|
||||
isPhishingDomain: false,
|
||||
approvedFrom: USER,
|
||||
approvedTx: {
|
||||
type: 2,
|
||||
from: USER,
|
||||
chainId: "0x1",
|
||||
nonce: "0x7",
|
||||
gasLimit: "0x30d40",
|
||||
maxPriorityFeePerGas: "0x3b9aca00",
|
||||
maxFeePerGas: "0x77359400",
|
||||
to: ROUTER,
|
||||
value: "0x0",
|
||||
data,
|
||||
accessList: [],
|
||||
},
|
||||
}),
|
||||
};
|
||||
state.networkId = "mainnet";
|
||||
state.currentView = null;
|
||||
await approval.show(1);
|
||||
return node("approve-tx-decoded-details").innerHTML;
|
||||
}
|
||||
|
||||
// The transaction the explorer reports: from USER, sending `mockSentData` to
|
||||
// the router.
|
||||
const TX_HASH = "0x" + "5".repeat(64);
|
||||
let mockSentData = null;
|
||||
|
||||
function mockExplorerAnswer(url) {
|
||||
if (url.endsWith("/transactions/" + TX_HASH)) {
|
||||
return {
|
||||
from: { hash: USER },
|
||||
to: { hash: ROUTER },
|
||||
raw_input: mockSentData,
|
||||
method: "execute",
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Open the transaction detail screen for that transaction, sending `data`.
|
||||
async function showDetail(data) {
|
||||
mockSentData = data;
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = "mainnet";
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
transactionDetail.show({
|
||||
hash: TX_HASH,
|
||||
from: USER,
|
||||
to: ROUTER,
|
||||
value: "",
|
||||
symbol: "ETH",
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "contract",
|
||||
directionLabel: "Swap",
|
||||
method: "execute",
|
||||
chainId: "0x1",
|
||||
});
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
return node("tx-detail-calldata-details").innerHTML;
|
||||
}
|
||||
|
||||
describe("the screens judge the output against the transaction's sender", () => {
|
||||
test("the approval screen, against its From address", async () => {
|
||||
const toUser = await showApproval(swapPaidTo(USER));
|
||||
expect(toUser).toContain("Min. received");
|
||||
expect(toUser).not.toContain("Paid to");
|
||||
|
||||
const toOther = await showApproval(swapPaidTo(OTHER));
|
||||
expect(toOther).toContain("Paid to");
|
||||
expect(toOther).toContain(OTHER);
|
||||
expect(toOther).not.toContain("Min. received");
|
||||
});
|
||||
|
||||
test("the transaction detail screen, against the sender the explorer reports", async () => {
|
||||
const toUser = await showDetail(swapPaidTo(USER));
|
||||
expect(toUser).toContain("Min. received");
|
||||
expect(toUser).not.toContain("Paid to");
|
||||
|
||||
const toOther = await showDetail(swapPaidTo(OTHER));
|
||||
expect(toOther).toContain("Paid to");
|
||||
expect(toOther).not.toContain("Min. received");
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,14 @@ function unwrapData() {
|
||||
}
|
||||
|
||||
function detail(data, label, sources) {
|
||||
const decoded = uniswap.decode(data, ROUTER, "0x1", sources || {});
|
||||
// Sent by the address the swap pays, so the output is the user's.
|
||||
const decoded = uniswap.decode(
|
||||
data,
|
||||
ROUTER,
|
||||
"0x1",
|
||||
sources || {},
|
||||
RECIPIENT,
|
||||
);
|
||||
return decoded.details.find((d) => d.label === label);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ function detail(data, label) {
|
||||
describe("an execute() whose input currency never decoded", () => {
|
||||
// A V4_SWAP whose sub-action params do not decode — an action encoding
|
||||
// this decoder does not know — leaves every V4 token null. The
|
||||
// BALANCE_CHECK still names the output, so the screen has a Min. received
|
||||
// BALANCE_CHECK still names the output, so the screen has a minimum
|
||||
// figure and a Token Out, and previously claimed the user was paying ETH
|
||||
// for them.
|
||||
const data = () =>
|
||||
@@ -132,7 +132,8 @@ describe("an execute() that names only an output token", () => {
|
||||
|
||||
test("still states the output it did establish", () => {
|
||||
expect(detail(data(), "Token Out").value).toContain("USDT");
|
||||
expect(detail(data(), "Min. received").value).toBe("2.0000 USDT");
|
||||
// No step pays anyone, so the minimum is not said to be received.
|
||||
expect(detail(data(), "Min. paid out").value).toBe("2.0000 USDT");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ function v3ExactIn(tokenIn, tokenOut, amountIn, amountOutMin) {
|
||||
}
|
||||
|
||||
function detail(data, label) {
|
||||
const decoded = uniswap.decode(data, ROUTER, "0x1", {});
|
||||
const decoded = uniswap.decode(data, ROUTER, "0x1", {}, USER);
|
||||
expect(decoded).not.toBeNull();
|
||||
return decoded.details.find((d) => d.label === label);
|
||||
}
|
||||
@@ -134,7 +134,9 @@ describe("a V4 step that states a Min. received but no output currency", () => {
|
||||
});
|
||||
|
||||
test("refuses to scale Min. received rather than assuming 18", () => {
|
||||
expect(detail(data(), "Min. received").value).toBe(
|
||||
// With no TAKE, no step pays the user either, so the line is not
|
||||
// labelled received.
|
||||
expect(detail(data(), "Min. paid out").value).toBe(
|
||||
unknownDecimalsAmount(MIN_OUT),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,8 @@ function walletsHolding(token, decimals) {
|
||||
// decodeCalldata() rather than uniswap.decode() directly, because the scale
|
||||
// sources the screen supplies are part of what is under test.
|
||||
function swapDetail(data, label) {
|
||||
const decoded = decodeCalldata(data, ROUTER, "0x1");
|
||||
// Sent by the address the swap pays, so the output is the user's.
|
||||
const decoded = decodeCalldata(data, ROUTER, "0x1", RECIPIENT);
|
||||
return decoded.details.find((d) => d.label === label);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user