fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum (closes #283)
decode() had no arm for Universal Router command 0x09, so the approval screen showed no token or amount for it. It now takes the path's first token and amountInMax as the input side, the last token and amountOut as the output side. With such a step, the Amount figure reads "Up to <amount>" whichever step set it, on the approval, wait, success and error screens, unless it reads "Unlimited", as an unbounded PERMIT2_PERMIT does, 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 no step set it; otherwise the output keeps its own token and figure. decodeV2SwapExactOut() loses its eslint-disable comment. Model: opus-5-5
This commit is contained in:
@@ -962,16 +962,25 @@ read:
|
|||||||
- `Unlimited`: on the ERC-20 `Amount` line, an `approve` of the `uint256`
|
- `Unlimited`: on the ERC-20 `Amount` line, an `approve` of the `uint256`
|
||||||
maximum, an unbounded allowance. On the swap's `Amount` line, any amount at or
|
maximum, an unbounded allowance. On the swap's `Amount` line, any amount at or
|
||||||
above the `uint160` maximum, whichever step set the line: a `PERMIT2_PERMIT`
|
above the `uint160` maximum, whichever step set the line: a `PERMIT2_PERMIT`
|
||||||
amount at that maximum, which is an unbounded permit, or a V2 or V3 exact-in
|
amount at that maximum, which is an unbounded permit, or a V2 or V3 exact-in,
|
||||||
or `WRAP_ETH` amount that large, which is not an allowance. The router's
|
V2 exact-out or `WRAP_ETH` amount that large, which is not an allowance. The
|
||||||
whole-balance value, `CONTRACT_BALANCE` (`2^255`), is one such amount.
|
router's whole-balance value, `CONTRACT_BALANCE` (`2^255`), is one such
|
||||||
|
amount.
|
||||||
|
- `Up to <amount>`: the swap's `Amount` line, when the transaction has a V2
|
||||||
|
exact-out step, whichever step set the line, including the `WRAP_ETH` of a
|
||||||
|
swap paid in ETH and a `PERMIT2_PERMIT`. The swap spends at most that figure,
|
||||||
|
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.
|
||||||
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
|
- `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
|
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
|
whole open delta", so the calldata states no quantity. The line shows the
|
||||||
amount of one step that names an input token or amount: the last
|
amount of one step that names an input token or amount: the last
|
||||||
`PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in,
|
`PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in,
|
||||||
`WRAP_ETH` or V4 swap step, a V4 swap step giving the `amountIn` of its first
|
V2 exact-out, `WRAP_ETH` or V4 swap step. A V2 exact-out step gives its
|
||||||
readable exact-in action.
|
`amountInMax`, and a V4 swap step the `amountIn` of its first readable
|
||||||
|
exact-in action.
|
||||||
- `None (no minimum guaranteed)`: the swap's `Min. received` line, when the
|
- `None (no minimum guaranteed)`: the swap's `Min. received` line, when the
|
||||||
minimum it shows is zero, whether a V2, V3 or V4 swap's minimum or a
|
minimum it shows is zero, whether a V2, V3 or V4 swap's minimum or a
|
||||||
`BALANCE_CHECK_ERC20` step's `minBalance`. Before
|
`BALANCE_CHECK_ERC20` step's `minBalance`. Before
|
||||||
@@ -980,9 +989,14 @@ read:
|
|||||||
|
|
||||||
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
|
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
|
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
|
that side. An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side
|
||||||
amount wording, including `Unknown`; the SignApproval section below describes
|
is WETH, on mainnet or Sepolia, or when no step set the output side; a WETH
|
||||||
it.
|
`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
|
#### Partial USD totals
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-04: A Uniswap V2 exact-out swap (Universal Router command `0x09`) is
|
||||||
|
decoded on the approval screen
|
||||||
|
([#283](https://git.eeqj.de/sneak/AutistMask/issues/283)). `decode()` in
|
||||||
|
`src/shared/uniswap.js` had no arm for it, so the screen named the step and
|
||||||
|
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,
|
||||||
|
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: A transaction with no `to` says "This transaction creates a new
|
- 2026-10-04: A transaction with no `to` says "This transaction creates a new
|
||||||
contract. It has no recipient." on its recipient line and in its transaction
|
contract. It has no recipient." on its recipient line and in its transaction
|
||||||
history row ([#250](https://git.eeqj.de/sneak/AutistMask/issues/250)). The
|
history row ([#250](https://git.eeqj.de/sneak/AutistMask/issues/250)). The
|
||||||
|
|||||||
+41
-10
@@ -100,6 +100,13 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
|
|||||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||||
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
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
|
// `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
|
// 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
|
// ERC-20 line, and a scale guessed there is what showed a 1,000 USDT swap as
|
||||||
@@ -198,11 +205,6 @@ function decodeV2SwapExactIn(input) {
|
|||||||
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
||||||
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
||||||
// address[] path, bool payerIsUser)
|
// address[] path, bool payerIsUser)
|
||||||
//
|
|
||||||
// Nothing calls this: decode() has no 0x09 arm, so a V2 exact-out swap gets
|
|
||||||
// its command name and no token or amount detail. Kept for the fix, which is
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/283.
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function decodeV2SwapExactOut(input) {
|
function decodeV2SwapExactOut(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(
|
const d = coder.decode(
|
||||||
@@ -447,6 +449,7 @@ function decode(data, toAddress, sources) {
|
|||||||
let outputToken = null;
|
let outputToken = null;
|
||||||
let minOutput = null;
|
let minOutput = null;
|
||||||
let hasUnwrapWeth = false;
|
let hasUnwrapWeth = false;
|
||||||
|
let hasV2ExactOut = false;
|
||||||
const commandNames = [];
|
const commandNames = [];
|
||||||
|
|
||||||
// THE INVARIANT: an amount and the token it is counted in always come
|
// THE INVARIANT: an amount and the token it is counted in always come
|
||||||
@@ -521,6 +524,16 @@ function decode(data, toAddress, sources) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmdId === 0x09) {
|
||||||
|
// Buys exactly amountOut and spends at most amountInMax.
|
||||||
|
hasV2ExactOut = true;
|
||||||
|
const s = decodeV2SwapExactOut(inputs[i]);
|
||||||
|
if (s) {
|
||||||
|
setInputOnce(s.tokenIn, s.amountInMax);
|
||||||
|
setOutput(s.tokenOut, s.amountOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cmdId === 0x0b) {
|
if (cmdId === 0x0b) {
|
||||||
const w = decodeWrapEth(inputs[i]);
|
const w = decodeWrapEth(inputs[i]);
|
||||||
if (w) {
|
if (w) {
|
||||||
@@ -559,12 +572,19 @@ function decode(data, toAddress, sources) {
|
|||||||
|
|
||||||
// Resolve token info. A null token on either side means the calldata
|
// Resolve token info. A null token on either side means the calldata
|
||||||
// named no currency for it; tokenInfo() refuses rather than calling it
|
// 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
|
// ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||||
// decode, and it is answered here rather than left to that rule.
|
// 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 inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo = hasUnwrapWeth
|
const outInfo =
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
hasUnwrapWeth && (outputIsWeth || outputUnset)
|
||||||
: tokenInfo(outputToken, sources);
|
? { symbol: "ETH", decimals: 18, address: null }
|
||||||
|
: tokenInfo(outputToken, sources);
|
||||||
|
|
||||||
const inSymbol = inInfo.symbol;
|
const inSymbol = inInfo.symbol;
|
||||||
const outSymbol = outInfo.symbol;
|
const outSymbol = outInfo.symbol;
|
||||||
@@ -613,6 +633,17 @@ function decode(data, toAddress, sources) {
|
|||||||
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||||
} else if (inputAmount >= MAX_UINT160) {
|
} else if (inputAmount >= MAX_UINT160) {
|
||||||
amount = { raw: "Unlimited", display: "Unlimited" };
|
amount = { raw: "Unlimited", display: "Unlimited" };
|
||||||
|
} else 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 = {
|
||||||
|
raw: "Up to " + most.raw,
|
||||||
|
display: "Up to " + most.display,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
amount = amountText(inputAmount, inInfo);
|
amount = amountText(inputAmount, inInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
|||||||
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||||
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||||
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||||
|
const DAI_ADDR = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
||||||
|
const SEPOLIA_WETH_ADDR = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||||
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
// AutistMask's first-ever swap, 2026-02-27.
|
// AutistMask's first-ever swap, 2026-02-27.
|
||||||
@@ -75,6 +77,14 @@ function encodeV2SwapExactIn(recipient, amountIn, amountOutMin, pathAddrs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: encode a V2_SWAP_EXACT_OUT input (command 0x09)
|
||||||
|
function encodeV2SwapExactOut(recipient, amountOut, amountInMax, pathAddrs) {
|
||||||
|
return coder.encode(
|
||||||
|
["address", "uint256", "uint256", "address[]", "bool"],
|
||||||
|
[recipient, amountOut, amountInMax, pathAddrs, true],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: encode a V3_SWAP_EXACT_IN input (command 0x00)
|
// Helper: encode a V3_SWAP_EXACT_IN input (command 0x00)
|
||||||
function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) {
|
function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) {
|
||||||
// V3 path: token(20) + fee(3) + token(20) ...
|
// V3 path: token(20) + fee(3) + token(20) ...
|
||||||
@@ -223,6 +233,233 @@ describe("uniswap decoder", () => {
|
|||||||
expect(minOut.value).toContain("WETH");
|
expect(minOut.value).toContain("WETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Buy exactly 0.5 WETH for at most 1,500 USDC, paid by the user, sent to
|
||||||
|
// the caller (the router's MSG_SENDER recipient, address(1)).
|
||||||
|
test("decodes V2_SWAP_EXACT_OUT, stating the input amount as a maximum", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
"0x09", // V2_SWAP_EXACT_OUT
|
||||||
|
[
|
||||||
|
encodeV2SwapExactOut(
|
||||||
|
"0x0000000000000000000000000000000000000001",
|
||||||
|
500000000000000000n, // amountOut: 0.5 WETH
|
||||||
|
1500000000n, // amountInMax: 1,500 USDC (6 decimals)
|
||||||
|
[USDC_ADDR, WETH_ADDR],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
1767225600n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(data, ROUTER_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);
|
||||||
|
|
||||||
|
// The wait, success and error screens show rawValue as the amount, so
|
||||||
|
// it says "Up to" as well.
|
||||||
|
const amount = detail(result, "Amount");
|
||||||
|
expect(amount.value).toBe("Up to 1500.0000 USDC");
|
||||||
|
expect(amount.rawValue).toBe("Up to 1500.0000");
|
||||||
|
|
||||||
|
expect(detail(result, "Min. received").value).toBe("0.5000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buy exactly 1,500 USDC for at most 0.5 ETH: WRAP_ETH of the maximum, the
|
||||||
|
// swap, then UNWRAP_WETH of 0, which returns the ETH the swap did not spend.
|
||||||
|
test("a V2 exact-out swap paid in ETH shows the token it buys and a maximum", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8", "uint8"], [0x0b, 0x09, 0x0c]),
|
||||||
|
[
|
||||||
|
encodeWrapEth(ROUTER_ADDR, 500000000000000000n),
|
||||||
|
encodeV2SwapExactOut(
|
||||||
|
USER_ADDR,
|
||||||
|
1500000000n, // amountOut: 1,500 USDC
|
||||||
|
500000000000000000n, // amountInMax: 0.5 WETH
|
||||||
|
[WETH_ADDR, USDC_ADDR],
|
||||||
|
),
|
||||||
|
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH same encoding
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
|
expect(result.name).toBe("Swap ETH → USDC");
|
||||||
|
|
||||||
|
const amount = detail(result, "Amount");
|
||||||
|
expect(amount.value).toBe("Up to 0.5000 ETH");
|
||||||
|
expect(amount.rawValue).toBe("Up to 0.5000");
|
||||||
|
|
||||||
|
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1500.0000 USDC");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buy exactly 0.5 ETH for at most 1,500 USDC under a permit: the swap buys
|
||||||
|
// WETH and UNWRAP_WETH turns it into ETH.
|
||||||
|
test("a V2 exact-out swap that buys ETH shows ETH and the permit as a maximum", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x09, 0x0c]),
|
||||||
|
[
|
||||||
|
encodePermit2(USDC_ADDR, 1500000000n, ROUTER_ADDR),
|
||||||
|
encodeV2SwapExactOut(
|
||||||
|
ROUTER_ADDR,
|
||||||
|
500000000000000000n, // amountOut: 0.5 WETH
|
||||||
|
1500000000n, // amountInMax: 1,500 USDC
|
||||||
|
[USDC_ADDR, WETH_ADDR],
|
||||||
|
),
|
||||||
|
encodeWrapEth(USER_ADDR, 500000000000000000n), // UNWRAP_WETH
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(data, ROUTER_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");
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
// A step that states a Min. received figure but names no output token has
|
||||||
|
// set the output side, so UNWRAP_WETH does not make it ETH: nothing says
|
||||||
|
// the figure is counted in WETH.
|
||||||
|
test("a step with a minimum but no output token, then UNWRAP_WETH, names no token", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x10, 0x0c]),
|
||||||
|
[
|
||||||
|
encodeV4Swap(new Uint8Array([V4_SWAP_EXACT_IN]), [
|
||||||
|
encodeV4ExactIn(
|
||||||
|
USDC_ADDR,
|
||||||
|
[], // no path: this step names no output currency
|
||||||
|
1000000000n, // 1,000 USDC
|
||||||
|
400000000000000000n, // amountOutMin
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||||
|
expect(detail(result, "Token Out").value).toBe(
|
||||||
|
"Unknown (not named in the calldata)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Min. received").value).toBe(
|
||||||
|
"400000000000000000 base units (decimals unknown)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("decodes V3_SWAP_EXACT_IN with known tokens", () => {
|
test("decodes V3_SWAP_EXACT_IN with known tokens", () => {
|
||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
"0x00", // V3_SWAP_EXACT_IN
|
"0x00", // V3_SWAP_EXACT_IN
|
||||||
|
|||||||
Reference in New Issue
Block a user