fix: a step the router lets fail is marked and sets no minimum (closes #602)
Each Universal Router release now carries the command types its dispatch() can return a failure for instead of reverting, read from its source; only a step of one of those with the allow-revert flag (0x80) set can fail while the transaction goes on. Such a step is listed with "(allowed to fail)". A minimum such a step states guarantees nothing, so it is read as zero: a BALANCE_CHECK_ERC20 then keeps an earlier step's minimum, and 0x10 on UniversalRouterV1 and UniversalRouterV1_2_V2Support, an NFT call there still read as a V4 swap, states no minimum. Every swap and every payment of ETH or an ERC-20 token succeeds or reverts the whole transaction, so the flag on them has no effect. Model: opus-5-5
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// The address a swap's output is paid to
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/594 and
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/599).
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/599), and the allow-revert flag
|
||||
// on the steps that pay it
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/602).
|
||||
//
|
||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
||||
@@ -149,6 +151,20 @@ function transfer(command, token, recipient, amount) {
|
||||
];
|
||||
}
|
||||
|
||||
// PERMIT2_PERMIT (0x0a) of `amount` of `token`, for the router to spend.
|
||||
function permit2Permit(token, amount) {
|
||||
return [
|
||||
0x0a,
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
|
||||
"bytes",
|
||||
],
|
||||
[[[token, amount, 0, 0], ROUTER, 0n], "0x"],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function permit2TransferFrom(token, recipient, amount) {
|
||||
return [
|
||||
0x02,
|
||||
@@ -1362,6 +1378,342 @@ describe("paths whose recipients were untested", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/602: the top bit of a command
|
||||
// byte (0x80) is the allow-revert flag. The router lets a step with it fail
|
||||
// and goes on to the next step only for some commands, none of them a swap or
|
||||
// a payment; on every other command the flag has no effect.
|
||||
describe("the allow-revert flag", () => {
|
||||
// UniversalRouterV1_2_V2Support on Ethereum Mainnet.
|
||||
const ROUTER_V1_2 = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
|
||||
// UniversalRouterV2_1_1 on Ethereum Mainnet.
|
||||
const ROUTER_V2_1_1 = "0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA";
|
||||
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||
|
||||
// `step` with the allow-revert flag set on its command byte.
|
||||
const flagged = ([command, input]) => [command | 0x80, input];
|
||||
// `steps` with the flag set on the one at `index`.
|
||||
const flagStep = (steps, index) =>
|
||||
steps.map((s, i) => (i === index ? flagged(s) : s));
|
||||
|
||||
const usdcToRouter = v3SwapExactIn(
|
||||
ADDRESS_THIS,
|
||||
USDT_2000,
|
||||
USDC_1990,
|
||||
USDT,
|
||||
USDC,
|
||||
);
|
||||
const wethToPair = v3SwapExactIn(PAIR, USDT_2000, 0n, USDT, WETH);
|
||||
const pairSpends = v2Swap(
|
||||
0x08,
|
||||
MSG_SENDER,
|
||||
0n,
|
||||
USDC_1990,
|
||||
[WETH, USDC],
|
||||
false,
|
||||
);
|
||||
|
||||
// The flag is set on the step at `index`: a swap, a payment, or the V2
|
||||
// swap that a payment to its pair hops into.
|
||||
test.each([
|
||||
{
|
||||
kind: "V2_SWAP_EXACT_IN",
|
||||
steps: [
|
||||
v2Swap(0x08, MSG_SENDER, USDT_2000, ONE_WETH, [USDT, WETH]),
|
||||
],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "V2_SWAP_EXACT_OUT",
|
||||
steps: [
|
||||
v2Swap(0x09, MSG_SENDER, ONE_WETH, USDT_2000, [USDT, WETH]),
|
||||
],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "V3_SWAP_EXACT_IN",
|
||||
steps: [swapToWeth(MSG_SENDER)],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "V4_SWAP",
|
||||
steps: [v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, takeAll(USDC))],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "SWEEP",
|
||||
steps: [usdcToRouter, sweep(USDC, MSG_SENDER)],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "TRANSFER",
|
||||
steps: [
|
||||
usdcToRouter,
|
||||
transfer(0x05, USDC, MSG_SENDER, CONTRACT_BALANCE),
|
||||
],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "PAY_PORTION",
|
||||
steps: [usdcToRouter, transfer(0x06, USDC, MSG_SENDER, 10000n)],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "PAY_PORTION_FULL_PRECISION",
|
||||
steps: [usdcToRouter, transfer(0x07, USDC, MSG_SENDER, 10n ** 18n)],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "UNWRAP_WETH",
|
||||
steps: [swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER)],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "WRAP_ETH",
|
||||
steps: [
|
||||
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
ONE_WETH,
|
||||
USDC_1990,
|
||||
WETH,
|
||||
USDC,
|
||||
false,
|
||||
),
|
||||
],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "ACROSS_V4_DEPOSIT_V3",
|
||||
steps: [
|
||||
usdcToRouter,
|
||||
acrossDeposit(USER, USER, USDC, CONTRACT_BALANCE),
|
||||
],
|
||||
index: 1,
|
||||
},
|
||||
{
|
||||
kind: "PERMIT2_TRANSFER_FROM to the pair the next V2 swap spends",
|
||||
steps: [permit2TransferFrom(WETH, PAIR, ONE_WETH), pairSpends],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "a swap paid to the pair the next V2 swap spends",
|
||||
steps: [wethToPair, pairSpends],
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
kind: "the V2 swap after a swap paid to its pair",
|
||||
steps: [wethToPair, pairSpends],
|
||||
index: 1,
|
||||
},
|
||||
])(
|
||||
"$kind reads the same with the flag as without it",
|
||||
({ steps, index }) => {
|
||||
const plain = decode(execute(...steps), USER, ROUTER_V2_1_2);
|
||||
const withFlag = decode(
|
||||
execute(...flagStep(steps, index)),
|
||||
USER,
|
||||
ROUTER_V2_1_2,
|
||||
);
|
||||
expect(withFlag).toEqual(plain);
|
||||
expect(detail(withFlag, "Steps").value).not.toContain(
|
||||
"allowed to fail",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
{ router: "UniversalRouterV1_2_V2Support", address: ROUTER_V1_2 },
|
||||
{ router: "UniversalRouterV2", address: ROUTER },
|
||||
{ router: "UniversalRouterV2_1_2", address: ROUTER_V2_1_2 },
|
||||
])(
|
||||
"on $router, a BALANCE_CHECK_ERC20 with the flag is allowed to fail and does not replace the swap's minimum",
|
||||
({ address }) => {
|
||||
const check = balanceCheck(WETH, 2n * ONE_WETH);
|
||||
|
||||
const plain = decode(
|
||||
execute(swapToWeth(MSG_SENDER), check),
|
||||
USER,
|
||||
address,
|
||||
);
|
||||
expect(detail(plain, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → Balance Check",
|
||||
);
|
||||
expect(detail(plain, "Min. received").value).toBe("2.0000 WETH");
|
||||
|
||||
const withFlag = decode(
|
||||
execute(swapToWeth(MSG_SENDER), flagged(check)),
|
||||
USER,
|
||||
address,
|
||||
);
|
||||
expect(detail(withFlag, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → Balance Check (allowed to fail)",
|
||||
);
|
||||
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
|
||||
},
|
||||
);
|
||||
|
||||
test("a BALANCE_CHECK_ERC20 with the flag, and no minimum before it, states none", () => {
|
||||
const check = balanceCheck(WETH, ONE_WETH);
|
||||
expect(detail(decode(execute(check)), "Min. paid out").value).toBe(
|
||||
"1.0000 WETH",
|
||||
);
|
||||
|
||||
const withFlag = decode(execute(flagged(check)));
|
||||
expect(detail(withFlag, "Token Out").address).toBe(WETH);
|
||||
expect(detail(withFlag, "Min. paid out").value).toBe(
|
||||
"None (no minimum guaranteed)",
|
||||
);
|
||||
});
|
||||
|
||||
// On these two routers 0x10 is an NFT market call the router lets fail,
|
||||
// still read as a V4 swap
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592).
|
||||
test.each([
|
||||
{ router: "UniversalRouterV1", address: ROUTER_V1 },
|
||||
{ router: "UniversalRouterV1_2_V2Support", address: ROUTER_V1_2 },
|
||||
])(
|
||||
"on $router, a V4_SWAP with the flag is allowed to fail and states no minimum",
|
||||
({ address }) => {
|
||||
const step = v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, takeAll(USDC));
|
||||
|
||||
const plain = decode(execute(step), USER, address);
|
||||
expect(detail(plain, "Steps").value).toBe("V4 Swap");
|
||||
expect(detail(plain, "Min. received").value).toBe("1990.0000 USDC");
|
||||
|
||||
const withFlag = decode(execute(flagged(step)), USER, address);
|
||||
expect(detail(withFlag, "Steps").value).toBe(
|
||||
"V4 Swap (allowed to fail)",
|
||||
);
|
||||
expect(detail(withFlag, "Token Out").address).toBe(USDC);
|
||||
expect(detail(withFlag, "Min. received").value).toBe(
|
||||
"None (no minimum guaranteed)",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Only the routers from UniversalRouterV2 on let a PERMIT2_PERMIT fail.
|
||||
test.each([
|
||||
{ router: "UniversalRouterV1", address: ROUTER_V1, mark: "" },
|
||||
{
|
||||
router: "UniversalRouterV1_2_V2Support",
|
||||
address: ROUTER_V1_2,
|
||||
mark: "",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2",
|
||||
address: ROUTER,
|
||||
mark: " (allowed to fail)",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2_1_2",
|
||||
address: ROUTER_V2_1_2,
|
||||
mark: " (allowed to fail)",
|
||||
},
|
||||
])(
|
||||
"on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark', and the swap reads as without the flag",
|
||||
({ address, mark }) => {
|
||||
const steps = [
|
||||
permit2Permit(USDT, USDT_2000),
|
||||
swapToWeth(MSG_SENDER),
|
||||
];
|
||||
const plain = decode(execute(...steps), USER, address);
|
||||
const withFlag = decode(
|
||||
execute(...flagStep(steps, 0)),
|
||||
USER,
|
||||
address,
|
||||
);
|
||||
expect(detail(withFlag, "Steps").value).toBe(
|
||||
"Permit2 Permit" + mark + " → V3 Swap (Exact In)",
|
||||
);
|
||||
const withoutSteps = (r) =>
|
||||
r.details.filter((d) => d.label !== "Steps");
|
||||
expect(withoutSteps(withFlag)).toEqual(withoutSteps(plain));
|
||||
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
|
||||
},
|
||||
);
|
||||
|
||||
test("a step the decoder does not read is marked too", () => {
|
||||
const positionManagerCall = [0x12, "0x"];
|
||||
const plain = decode(
|
||||
execute(swapToWeth(MSG_SENDER), positionManagerCall),
|
||||
);
|
||||
expect(detail(plain, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → V3 Position Mgr Call",
|
||||
);
|
||||
|
||||
const withFlag = decode(
|
||||
execute(swapToWeth(MSG_SENDER), flagged(positionManagerCall)),
|
||||
);
|
||||
expect(detail(withFlag, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → V3 Position Mgr Call (allowed to fail)",
|
||||
);
|
||||
expect(paidTo(withFlag)).toEqual([UNDECODED]);
|
||||
});
|
||||
|
||||
// A command from each router's list, by its name in that release's
|
||||
// Commands.sol.
|
||||
test.each([
|
||||
{
|
||||
router: "UniversalRouterV1",
|
||||
address: ROUTER_V1,
|
||||
command: "FOUNDATION",
|
||||
id: 0x1c,
|
||||
name: "Command 0x1c",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV1_2_V2Support",
|
||||
address: ROUTER_V1_2,
|
||||
command: "ELEMENT_MARKET",
|
||||
id: 0x1e,
|
||||
name: "Command 0x1e",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2",
|
||||
address: ROUTER,
|
||||
command: "PERMIT2_PERMIT_BATCH",
|
||||
id: 0x03,
|
||||
name: "Permit2 Permit Batch",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2_1_1",
|
||||
address: ROUTER_V2_1_1,
|
||||
command: "V4_INITIALIZE_POOL",
|
||||
id: 0x13,
|
||||
name: "V4 Initialize Pool",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2_1_2",
|
||||
address: ROUTER_V2_1_2,
|
||||
command: "V4_POSITION_MANAGER_CALL",
|
||||
id: 0x14,
|
||||
name: "V4 Position Mgr Call",
|
||||
},
|
||||
])(
|
||||
"on $router, $command with the flag is listed as '$name (allowed to fail)'",
|
||||
({ address, id, name }) => {
|
||||
const step = [id, "0x"];
|
||||
const plain = decode(
|
||||
execute(swapToWeth(MSG_SENDER), step),
|
||||
USER,
|
||||
address,
|
||||
);
|
||||
expect(detail(plain, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → " + name,
|
||||
);
|
||||
|
||||
const withFlag = decode(
|
||||
execute(swapToWeth(MSG_SENDER), flagged(step)),
|
||||
USER,
|
||||
address,
|
||||
);
|
||||
expect(detail(withFlag, "Steps").value).toBe(
|
||||
"V3 Swap (Exact In) → " + name + " (allowed to fail)",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// The DOM stub both screens are drawn into.
|
||||
function makeElement(id) {
|
||||
const classes = new Set();
|
||||
|
||||
Reference in New Issue
Block a user