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. A swap step or V4 SETTLE that spends all the router holds of a token, a WRAP_ETH of all its ETH, 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 as its step's one SETTLE of that token, after every swap action, with none buying it. Model: opus-5-5
This commit is contained in:
@@ -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,392 @@ 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",
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
// 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"),
|
||||
},
|
||||
{
|
||||
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 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