`tokenInfo(null)` answered `{symbol: "ETH", decimals: 18}`, so a swap whose
calldata never named an input currency rendered `Token In: ETH (native)` and
titled itself `Swap ETH -> X`. The approval screen asserted the user was paying
native ETH when nothing had established it.
Null is not how native ETH arrives. v4-core declares `type Currency is address`
and wraps `address(0)` for it; a user-defined value type over `address` carries
the plain `address` ABI encoding, so a native-ETH currency reaches every decode
site here as the truthy string
`0x0000000000000000000000000000000000000000`, and WRAP_ETH sets that same
explicit zero address. A null token means undetermined, on the input side as on
the output side.
The collapse is removed from `tokenInfo()` itself rather than guarded at each
call site, so both sides of the screen answer the same condition the same way:
null refuses, the zero address is still ETH at 18 decimals. `Token In` gains
the refusal branch `Unknown (not named in the calldata)`, now a shared constant
with the `Token Out` branch it must match.
183 lines
7.1 KiB
JavaScript
183 lines
7.1 KiB
JavaScript
// What the dApp approval screen says the input token of a swap is when the
|
|
// calldata did not name one.
|
|
//
|
|
// Issue #357, the twin on the input side of
|
|
// https://git.eeqj.de/sneak/AutistMask/issues/353: `tokenInfo(null)` answered
|
|
// `{symbol: "ETH", decimals: 18}`, so a null `inputToken` rendered as
|
|
// `Token In: ETH (native)` and titled the swap `Swap ETH -> X`. An
|
|
// undetermined input was therefore asserted to the user as native ETH — the
|
|
// same class as https://git.eeqj.de/sneak/AutistMask/issues/340 and
|
|
// https://git.eeqj.de/sneak/AutistMask/issues/306, naming the wrong asset
|
|
// rather than merely mis-scaling it.
|
|
//
|
|
// The determination this file pins is the one #353 established, checked here
|
|
// on the input side: null is NOT how native ETH arrives. v4-core declares
|
|
// `type Currency is address` and wraps `address(0)` for native ETH, and a
|
|
// user-defined value type over `address` carries the plain `address` ABI
|
|
// encoding, so a native-ETH currency reaches the decoder as the truthy string
|
|
// "0x0000000000000000000000000000000000000000". WRAP_ETH sets that same
|
|
// explicit zero address. Both halves are asserted below: the zero address
|
|
// stays ETH, and null refuses.
|
|
|
|
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
|
const uniswap = require("../src/shared/uniswap");
|
|
|
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
|
const USER = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
// Both in the bundled list: USDT at 6 decimals, USDC at 6.
|
|
const USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
|
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
|
|
// The same wording the output side refuses with — one screen, one vocabulary.
|
|
const REFUSAL = "Unknown (not named in the calldata)";
|
|
|
|
const ONE_ETH = 1000000000000000000n;
|
|
|
|
const V4_SWAP_EXACT_IN = 0x07;
|
|
const V4_SETTLE = 0x0b;
|
|
const V4_TAKE = 0x0e;
|
|
|
|
const coder = AbiCoder.defaultAbiCoder();
|
|
const routerIface = new Interface([
|
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
|
]);
|
|
|
|
function execute(commands, inputs) {
|
|
return routerIface.encodeFunctionData("execute", [
|
|
commands,
|
|
inputs,
|
|
9999999999n,
|
|
]);
|
|
}
|
|
|
|
function v4Input(actions, params) {
|
|
return coder.encode(
|
|
["bytes", "bytes[]"],
|
|
[new Uint8Array(actions), params],
|
|
);
|
|
}
|
|
|
|
// IV4Router.ExactInputParams as the decoder reads it:
|
|
// (Currency currencyIn, PathKey[] path, uint128 amountIn, uint128 minOut).
|
|
function exactInParams(currencyIn, path, amountIn, amountOutMin) {
|
|
return coder.encode(
|
|
[
|
|
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
|
],
|
|
[[currencyIn, path, amountIn, amountOutMin]],
|
|
);
|
|
}
|
|
|
|
// BALANCE_CHECK_ERC20 (command 0x0e): (address owner, address token,
|
|
// uint256 minBalance). It names the output token and nothing about the input.
|
|
function balanceCheck(token, minBalance) {
|
|
return coder.encode(
|
|
["address", "address", "uint256"],
|
|
[USER, token, minBalance],
|
|
);
|
|
}
|
|
|
|
function detail(data, label) {
|
|
const decoded = uniswap.decode(data, ROUTER, {});
|
|
expect(decoded).not.toBeNull();
|
|
return decoded.details.find((d) => d.label === 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
|
|
// figure and a Token Out, and previously claimed the user was paying ETH
|
|
// for them.
|
|
const data = () =>
|
|
execute(solidityPacked(["uint8", "uint8"], [0x10, 0x0e]), [
|
|
coder.encode(["bytes", "bytes[]"], ["0x07", ["0x"]]),
|
|
balanceCheck(USDC, 2000000n),
|
|
]);
|
|
|
|
test("says the input token is unknown instead of naming ETH", () => {
|
|
expect(detail(data(), "Token In").value).toBe(REFUSAL);
|
|
});
|
|
|
|
test("keeps a Token In line, so the screen never omits what is paid", () => {
|
|
const tokenIn = detail(data(), "Token In");
|
|
expect(tokenIn).toBeDefined();
|
|
// A refusal is not a token: nothing to link to an explorer.
|
|
expect(tokenIn.address).toBeUndefined();
|
|
expect(tokenIn.isToken).toBeUndefined();
|
|
});
|
|
|
|
test("does not name ETH in the swap title either", () => {
|
|
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
|
|
});
|
|
});
|
|
|
|
describe("an execute() that names only an output token", () => {
|
|
// A lone BALANCE_CHECK_ERC20: nothing in it says what is being paid.
|
|
const data = () => execute("0x0e", [balanceCheck(USDT, 2000000n)]);
|
|
|
|
test("refuses the input rather than defaulting it to ETH", () => {
|
|
expect(detail(data(), "Token In").value).toBe(REFUSAL);
|
|
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap");
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
describe("native ETH in, which V4 spells as the zero address", () => {
|
|
test("a Currency of address(0) decodes to a truthy address string", () => {
|
|
// The fact the whole determination rests on: an absent input currency
|
|
// and a native-ETH one are distinguishable here, because the ABI
|
|
// decoder never yields null for an address word.
|
|
const [currency] = coder.decode(
|
|
["address", "uint256", "bool"],
|
|
coder.encode(["address", "uint256", "bool"], [ZERO, ONE_ETH, true]),
|
|
);
|
|
expect(currency).toBe(ZERO);
|
|
expect(Boolean(currency)).toBe(true);
|
|
});
|
|
|
|
test("a V4 SETTLE of the zero address is still ETH at 18 decimals", () => {
|
|
const data = execute("0x10", [
|
|
v4Input(
|
|
[V4_SETTLE, V4_SWAP_EXACT_IN, V4_TAKE],
|
|
[
|
|
coder.encode(
|
|
["address", "uint256", "bool"],
|
|
[ZERO, ONE_ETH, true],
|
|
),
|
|
exactInParams(
|
|
ZERO,
|
|
[[USDT, 500, 10, ZERO, "0x"]],
|
|
ONE_ETH,
|
|
2000000n,
|
|
),
|
|
coder.encode(
|
|
["address", "address", "uint256"],
|
|
[USDT, USER, 0n],
|
|
),
|
|
],
|
|
),
|
|
]);
|
|
|
|
expect(detail(data, "Token In").value).toBe("ETH (native)");
|
|
expect(detail(data, "Amount").value).toBe("1.0000 ETH");
|
|
expect(uniswap.decode(data, ROUTER, {}).name).toBe("Swap ETH → USDT");
|
|
});
|
|
|
|
test("WRAP_ETH is still ETH at 18 decimals", () => {
|
|
// WRAP_ETH names no currency of its own; the decoder supplies the
|
|
// explicit zero address for it, which is why it survives this change.
|
|
const data = execute("0x0b", [
|
|
coder.encode(["address", "uint256"], [ROUTER, ONE_ETH]),
|
|
]);
|
|
|
|
expect(detail(data, "Token In").value).toBe("ETH (native)");
|
|
expect(detail(data, "Amount").value).toBe("1.0000 ETH");
|
|
});
|
|
});
|