fix: any token a swap leaves in the router keeps it from reading as received (closes #606)
The decoder checked only the swap's output for being left in the router. It now records every token a step pays the router, the WETH of a WRAP_ETH and the rest of a V4 SETTLE taken to the router among them, and any token still there after the last step gets a Paid to line naming it, so the minimum reads Min. paid out. Steps that spend all the router holds of a token, and swap steps from the router that together spend what a WRAP_ETH stated, count as spending it. A SETTLE of OPEN_DELTA from the router counts only in the plain shape of Uniswap's own routes. Model: opus-5-5
This commit is contained in:
@@ -739,7 +739,9 @@ describe("uniswap decoder", () => {
|
||||
});
|
||||
|
||||
// Paid in ETH, with an exact-out step, the last swap step buys WETH, so
|
||||
// UNWRAP_WETH makes the output ETH.
|
||||
// UNWRAP_WETH makes the output ETH. That step is paid by the user, so the
|
||||
// USDC the exact-out step bought stays in the router, and the minimum is
|
||||
// not received (https://git.eeqj.de/sneak/AutistMask/issues/606).
|
||||
test("an ETH-paid swap whose last step buys WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(
|
||||
@@ -767,7 +769,7 @@ describe("uniswap decoder", () => {
|
||||
|
||||
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");
|
||||
expect(detail(result, "Min. paid out").value).toBe("0.4000 ETH");
|
||||
});
|
||||
|
||||
// Sepolia's WETH is a different contract; UNWRAP_WETH makes it ETH too.
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
// on the steps that pay it
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/602), the command each router
|
||||
// release runs for a command type
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), and the address whose
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
||||
// balance a BALANCE_CHECK_ERC20 checks
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600).
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600), and any token a swap
|
||||
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606).
|
||||
//
|
||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
||||
@@ -546,6 +547,486 @@ describe("an output left in the router is not received", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/606: any token a step pays the
|
||||
// router stays there unless a later step pays out or spends all of it, not
|
||||
// only the swap's output. A V4 SETTLE can take more of the user's tokens than
|
||||
// the step's swap spends, a WRAP_ETH wraps the user's ETH into the router, and
|
||||
// a swap can leave there the token it bought on the way to the output.
|
||||
describe("any token left in the router is not received", () => {
|
||||
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||
const USDT_5000 = 5000000000n;
|
||||
|
||||
// V4 SETTLE of `amount` of `currency` (zero: what the step's swap
|
||||
// spends), paid by the user, or by the router when `payerIsUser` is false.
|
||||
function settle(currency, amount, payerIsUser) {
|
||||
return [
|
||||
0x0b,
|
||||
coder.encode(
|
||||
["address", "uint256", "bool"],
|
||||
[currency, amount, payerIsUser],
|
||||
),
|
||||
];
|
||||
}
|
||||
// V4 SWAP_EXACT_IN_SINGLE of `amountIn` of `currencyIn` (zero: the open
|
||||
// delta) for at least `amountOutMin` of `currencyOut`.
|
||||
function swapExactInSingle(
|
||||
currencyIn,
|
||||
currencyOut,
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
) {
|
||||
return [
|
||||
0x06,
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
[
|
||||
[
|
||||
[currencyIn, currencyOut, 500, 10, ZERO],
|
||||
true,
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
"0x",
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
// V4 SWAP_EXACT_IN of `amountIn` of `currencyIn` through one pool for
|
||||
// each currency of `path` in turn, for at least `amountOutMin` of the
|
||||
// last. An empty path swaps nothing.
|
||||
function swapExactIn(currencyIn, path, amountIn, amountOutMin) {
|
||||
return [
|
||||
0x07,
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
[
|
||||
[
|
||||
currencyIn,
|
||||
path.map((c) => [c, 500, 10, ZERO, "0x"]),
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
// V4 SETTLE_ALL of `currency`, paid by the user.
|
||||
function settleAll(currency, maxAmount) {
|
||||
return [
|
||||
0x0c,
|
||||
coder.encode(["address", "uint256"], [currency, maxAmount]),
|
||||
];
|
||||
}
|
||||
// The line giving a token left in the router.
|
||||
const tokenLine = (token, symbol) => ({
|
||||
label: "Token moved into the router",
|
||||
value: symbol + " (" + token + ")",
|
||||
address: token,
|
||||
isToken: true,
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4 SETTLE of 5,000 USDT for a swap of 2,000, the rest taken to the router by TAKE",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
settle(USDT, USDT_5000, true),
|
||||
SWAP_USDT_FOR_USDC,
|
||||
take(USDT, ADDRESS_THIS),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(USDT, "USDT"),
|
||||
},
|
||||
{
|
||||
kind: "a V4 SETTLE of 5,000 USDT for a swap of 2,000, the rest taken to the router by TAKE_PORTION",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
settle(USDT, USDT_5000, true),
|
||||
SWAP_USDT_FOR_USDC,
|
||||
takeAll(USDC),
|
||||
takePortion(USDT, ADDRESS_THIS, 10000n),
|
||||
),
|
||||
),
|
||||
left: tokenLine(USDT, "USDT"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router that nothing spends",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
ONE_WETH / 2n,
|
||||
USDC_1990,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
||||
// still owes of its currency when it runs.
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 0.5 WETH with two SETTLEs of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH / 2n, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 0.75 WETH with a SETTLE of 0.25 WETH and one of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(
|
||||
WETH,
|
||||
USDC,
|
||||
(ONE_WETH * 3n) / 4n,
|
||||
USDC_1990,
|
||||
),
|
||||
settle(WETH, ONE_WETH / 4n, false),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step that also buys WETH back, with a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
swapExactInSingle(USDC, WETH, 1000000n, 1n),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 SETTLE of OPEN_DELTA from the router before the swap, which the user settles",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
settle(WETH, 0n, false),
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, true),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
|
||||
// the decoder cannot read, so the SETTLE from the router pays nothing.
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE_ALL that cannot be read and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
withDirtyWord(settleAll(WETH, ONE_WETH), 0),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE by the user that cannot be read and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
withDirtyWord(settle(WETH, ONE_WETH, true), 0),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step with an exact-in of 1 WETH less 1,000 wei and an empty path, a swap of 1,000 wei, and a SETTLE of OPEN_DELTA from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactIn(WETH, [], ONE_WETH - 1000n, 0n),
|
||||
swapExactInSingle(WETH, USDC, 1000n, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of all of it settled from the router, with an action the decoder does not read",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
[0x14, coder.encode(["address"], [WETH])],
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, a TRANSFER of 1 wei of it to the WETH/USDC pair, then a V2 swap of what the pair holds",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
transfer(0x05, WETH, PAIR, 1n),
|
||||
v2Swap(0x08, MSG_SENDER, 0n, USDC_1990, [WETH, USDC], false),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, then a swap of 1 wei of it from the router",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
v2Swap(0x08, MSG_SENDER, 1n, USDC_1990, [WETH, USDC], false),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, an UNWRAP_WETH into the router, then another swap",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
unwrapWeth(ADDRESS_THIS),
|
||||
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||
),
|
||||
left: { label: "Token moved into the router", value: "ETH" },
|
||||
},
|
||||
])("$kind", ({ data, left }) => {
|
||||
const result = decode(data);
|
||||
expect(labels(result)).toEqual([
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
"Token Out",
|
||||
"Paid to",
|
||||
"Token moved into the router",
|
||||
"Min. paid out",
|
||||
"Steps",
|
||||
"Deadline",
|
||||
]);
|
||||
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||
expect(detail(result, "Token moved into the router")).toEqual(left);
|
||||
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||
});
|
||||
|
||||
// Uniswap's own routes: a swap paid in ETH wraps into the router what its
|
||||
// swap steps then spend from it, and a route through several pools pays
|
||||
// each token it buys on the way to the router and spends all of it in the
|
||||
// next step (CONTRACT_BALANCE), turning ETH into WETH or back where the
|
||||
// pools need it.
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4 SETTLE of what its swap spends",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
settle(USDT, USDT_2000, true),
|
||||
SWAP_USDT_FOR_USDC,
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a V4 SETTLE of more than its swap spends, the rest taken to the user",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
settle(USDT, USDT_5000, true),
|
||||
SWAP_USDT_FOR_USDC,
|
||||
take(USDT, MSG_SENDER),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router, then a swap of all of it from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
ONE_WETH,
|
||||
USDC_1990,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router, then two swaps from the router that together spend all of it",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
(ONE_WETH * 3n) / 5n,
|
||||
1n,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
v2Swap(
|
||||
0x08,
|
||||
MSG_SENDER,
|
||||
(ONE_WETH * 2n) / 5n,
|
||||
USDC_1990,
|
||||
[WETH, USDC],
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router, then a V4 swap of all of it settled from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH into the router, then a V4 swap of all of it through two pools, settled from the router",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v4Swap(
|
||||
swapExactIn(WETH, [USDT, USDC], ONE_WETH, USDC_1990),
|
||||
settle(WETH, 0n, false),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of all the router's ETH, then a swap of all its WETH",
|
||||
data: execute(
|
||||
wrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
CONTRACT_BALANCE,
|
||||
USDC_1990,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, then a V2 swap of all of it",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
v2Swap(
|
||||
0x08,
|
||||
MSG_SENDER,
|
||||
CONTRACT_BALANCE,
|
||||
USDC_1990,
|
||||
[WETH, USDC],
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, then a V4 swap settling all of it from the router",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
v4Swap(
|
||||
settle(WETH, CONTRACT_BALANCE, false),
|
||||
swapExactInSingle(WETH, USDC, 0n, USDC_1990),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap taking USDC to the router, then a V3 swap of all of it",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_USDC,
|
||||
SETTLE_USDT,
|
||||
take(USDC, ADDRESS_THIS),
|
||||
),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
CONTRACT_BALANCE,
|
||||
ONE_WETH,
|
||||
USDC,
|
||||
WETH,
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap taking ETH to the router, a WRAP_ETH of all of it, then a V3 swap of all the WETH",
|
||||
data: execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_ETH,
|
||||
SETTLE_USDT,
|
||||
take(ZERO, ADDRESS_THIS),
|
||||
),
|
||||
wrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
CONTRACT_BALANCE,
|
||||
USDC_1990,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a swap paying WETH to the router, an UNWRAP_WETH into the router, then a V4 swap settling all the ETH from the router",
|
||||
data: execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
unwrapWeth(ADDRESS_THIS),
|
||||
v4Swap(
|
||||
settle(ZERO, CONTRACT_BALANCE, false),
|
||||
swapExactInSingle(ZERO, USDC, 0n, USDC_1990),
|
||||
takeAll(USDC),
|
||||
),
|
||||
),
|
||||
min: "1990.0000 USDC",
|
||||
},
|
||||
])("$kind reads as a swap paid to the user", ({ data, min }) => {
|
||||
const result = decode(data);
|
||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||
expect(detail(result, "Min. received").value).toBe(min);
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/599: TRANSFER, PAY_PORTION,
|
||||
// PAY_PORTION_FULL_PRECISION and V4 TAKE_PORTION pay a token to an address,
|
||||
// and PERMIT2_TRANSFER_FROM moves the user's own tokens to one.
|
||||
|
||||
Reference in New Issue
Block a user