Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3f8363e0 |
@@ -972,9 +972,7 @@ read:
|
||||
not necessarily all of it; the wait, success and error screens show it with
|
||||
the same words. `Unlimited` and `All available (V4 open delta)` keep their
|
||||
wording. When a V2 exact-out step sets `Min. received`, that line shows its
|
||||
`amountOut`, the exact amount it buys. In a V2 exact-out swap paid in ETH, the
|
||||
`UNWRAP_WETH` step returns the ETH not spent, so `Token Out` is the token the
|
||||
swap buys, not ETH.
|
||||
`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
|
||||
@@ -991,9 +989,14 @@ read:
|
||||
|
||||
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
|
||||
reads `Unknown (not named in the calldata)` when the decoder found no token for
|
||||
that side. The token permission warning on the signature screen has its own
|
||||
amount wording, including `Unknown`; the SignApproval section below describes
|
||||
it.
|
||||
that side. An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side
|
||||
is WETH, on mainnet or Sepolia, or when no step set the output side; a WETH
|
||||
`Min. received` figure then reads in ETH. Otherwise `Token Out` and
|
||||
`Min. received` keep the output side's own token and figure, whether the swap
|
||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
||||
the signature screen has its own amount wording, including `Unknown`; the
|
||||
SignApproval section below describes it.
|
||||
|
||||
#### Partial USD totals
|
||||
|
||||
|
||||
@@ -52,9 +52,13 @@ but the review is broader than any of them.
|
||||
showed no token or amount. The input side is the path's first token with
|
||||
`amountInMax`, the output side the last token with `amountOut`. When the
|
||||
transaction has such a step, the `Amount` figure reads `Up to <amount>`,
|
||||
whichever step set it, there and on the wait, success and error screens. In
|
||||
such a swap paid in ETH, the `UNWRAP_WETH` step that returns the ETH not spent
|
||||
does not make `Token Out` ETH. V3 exact-out (`0x01`) is still not decoded.
|
||||
whichever step set it, there and on the wait, success and error screens,
|
||||
except where it reads `Unlimited` (an unbounded `PERMIT2_PERMIT`, or any
|
||||
amount at or above the `uint160` maximum) or `All available (V4 open delta)`.
|
||||
In every swap, `UNWRAP_WETH` makes `Token Out` ETH only when the output side
|
||||
is WETH, on mainnet or Sepolia, or when no step set the output side; otherwise
|
||||
`Token Out` and `Min. received` keep the output side's own token and figure.
|
||||
V3 exact-out (`0x01`) is still not decoded.
|
||||
|
||||
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a
|
||||
token balance or a network fee below 0.000001 as zero
|
||||
|
||||
+16
-5
@@ -100,6 +100,13 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
|
||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
||||
|
||||
// WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia.
|
||||
// decode() is not told the network, so it takes either.
|
||||
const WETH_ADDRESSES = [
|
||||
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
||||
];
|
||||
|
||||
// `decimals` is null when nothing knows this token's scale. It is not
|
||||
// defaulted to 18: the swap lines land on the same approval screen as the
|
||||
// ERC-20 line, and a scale guessed there is what showed a 1,000 USDT swap as
|
||||
@@ -565,13 +572,17 @@ function decode(data, toAddress, sources) {
|
||||
|
||||
// Resolve token info. A null token on either side means the calldata
|
||||
// named no currency for it; tokenInfo() refuses rather than calling it
|
||||
// ETH. UNWRAP_WETH is the one output that is ETH without a currency to
|
||||
// decode, and it is answered here rather than left to that rule.
|
||||
// Except in a V2 exact-out swap paid in ETH: there UNWRAP_WETH returns
|
||||
// the ETH the swap did not spend, and the output is the token it buys.
|
||||
// ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||
// when the output side is WETH, or when no step set the output side.
|
||||
// Any other output keeps its own token and figure: a swap that buys
|
||||
// USDC and then unwraps the WETH it did not spend receives USDC.
|
||||
const outputIsWeth =
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||
const outputUnset = !present(outputToken) && !present(minOutput);
|
||||
const inInfo = tokenInfo(inputToken, sources);
|
||||
const outInfo =
|
||||
hasUnwrapWeth && !(hasV2ExactOut && inInfo.symbol === "ETH")
|
||||
hasUnwrapWeth && (outputIsWeth || outputUnset)
|
||||
? { symbol: "ETH", decimals: 18, address: null }
|
||||
: tokenInfo(outputToken, sources);
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
const DAI_ADDR = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
||||
const SEPOLIA_WETH_ADDR = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||
|
||||
// AutistMask's first-ever swap, 2026-02-27.
|
||||
@@ -315,6 +317,120 @@ describe("uniswap decoder", () => {
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
||||
});
|
||||
|
||||
// Paid in a token, an UNWRAP_WETH of 0 after a swap that buys something
|
||||
// other than WETH leaves the output side as it is: Token Out and Min.
|
||||
// received are the token bought and its figure, not ETH.
|
||||
test.each([
|
||||
{
|
||||
paidIn: "WETH",
|
||||
tokenIn: WETH_ADDR,
|
||||
amountInMax: 500000000000000000n, // 0.5 WETH
|
||||
tokenOut: USDC_ADDR,
|
||||
amountOut: 1300000000n, // 1,300 USDC
|
||||
minReceived: "1300.0000 USDC",
|
||||
},
|
||||
{
|
||||
paidIn: "USDC",
|
||||
tokenIn: USDC_ADDR,
|
||||
amountInMax: 8000000n, // 8 USDC
|
||||
tokenOut: DAI_ADDR,
|
||||
amountOut: 7000000000000000000n, // 7 DAI
|
||||
minReceived: "7.0000 DAI",
|
||||
},
|
||||
])(
|
||||
"a V2 exact-out swap paid in $paidIn, then UNWRAP_WETH, shows the token it buys",
|
||||
({ tokenIn, amountInMax, tokenOut, amountOut, minReceived }) => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x09, 0x0c]),
|
||||
[
|
||||
encodePermit2(tokenIn, amountInMax, ROUTER_ADDR),
|
||||
encodeV2SwapExactOut(USER_ADDR, amountOut, amountInMax, [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(tokenOut);
|
||||
expect(detail(result, "Min. received").value).toBe(minReceived);
|
||||
},
|
||||
);
|
||||
|
||||
// An exact-in swap is held to the same rule: buying USDC, then UNWRAP_WETH,
|
||||
// receives USDC.
|
||||
test("an exact-in swap to a token other than WETH, then UNWRAP_WETH, shows that token", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 2000000n, 1900000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_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");
|
||||
});
|
||||
|
||||
// Paid in ETH, with an exact-out step, the last swap step buys WETH, so
|
||||
// UNWRAP_WETH makes the output ETH.
|
||||
test("an ETH-paid swap whose last step buys WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(
|
||||
["uint8", "uint8", "uint8", "uint8"],
|
||||
[0x0b, 0x09, 0x08, 0x0c],
|
||||
),
|
||||
[
|
||||
encodeWrapEth(ROUTER_ADDR, 500000000000000000n),
|
||||
encodeV2SwapExactOut(
|
||||
ROUTER_ADDR,
|
||||
1500000000n, // amountOut: 1,500 USDC
|
||||
500000000000000000n, // amountInMax: 0.5 WETH
|
||||
[WETH_ADDR, USDC_ADDR],
|
||||
),
|
||||
encodeV2SwapExactIn(
|
||||
ROUTER_ADDR,
|
||||
1500000000n, // amountIn: 1,500 USDC
|
||||
400000000000000000n, // amountOutMin: 0.4 WETH
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
||||
});
|
||||
|
||||
// Sepolia's WETH is a different contract; UNWRAP_WETH makes it ETH too.
|
||||
test("a swap to Sepolia WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
||||
USDC_ADDR,
|
||||
SEPOLIA_WETH_ADDR,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
|
||||
});
|
||||
|
||||
test("decodes V3_SWAP_EXACT_IN with known tokens", () => {
|
||||
const data = buildExecute(
|
||||
"0x00", // V3_SWAP_EXACT_IN
|
||||
|
||||
Reference in New Issue
Block a user