Compare commits
1
Commits
next
...
a93eba3f46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93eba3f46 |
@@ -962,16 +962,27 @@ 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. 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.
|
||||||
- `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
|
||||||
|
|||||||
@@ -45,6 +45,17 @@ 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. 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.
|
||||||
|
|
||||||
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance, a
|
- 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
|
token balance or a network fee below 0.000001 as zero
|
||||||
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances
|
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored balances
|
||||||
|
|||||||
+28
-8
@@ -198,11 +198,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 +442,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 +517,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) {
|
||||||
@@ -561,10 +567,13 @@ function decode(data, toAddress, sources) {
|
|||||||
// 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 is the one output that is ETH without a currency to
|
||||||
// decode, and it is answered here rather than left to that rule.
|
// 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.
|
||||||
const inInfo = tokenInfo(inputToken, sources);
|
const inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo = hasUnwrapWeth
|
const outInfo =
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
hasUnwrapWeth && !(hasV2ExactOut && inInfo.symbol === "ETH")
|
||||||
: 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 +622,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,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 +231,90 @@ 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");
|
||||||
|
});
|
||||||
|
|
||||||
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