Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70af00417c |
@@ -1198,7 +1198,11 @@ number. They can read:
|
||||
guarantees nothing: it sets `Token Out` and `Min. received` only when the
|
||||
output side holds no minimum, not even a zero one, at the point the check is
|
||||
reached, and otherwise leaves the current token and figure in place. A nonzero
|
||||
`minBalance` sets both lines, as a swap step does.
|
||||
`minBalance` sets both lines, as a swap step does. A `BALANCE_CHECK_ERC20`
|
||||
step that the router lets fail without reverting the transaction (see
|
||||
Transaction Decoding) guarantees nothing either, whatever its `minBalance`,
|
||||
and is read as one whose `minBalance` is zero. A minimum stated by a V4 swap
|
||||
step the router lets fail is likewise read as zero.
|
||||
|
||||
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
|
||||
@@ -2970,13 +2974,33 @@ Currently supported:
|
||||
address uses: `0x1f` for `UniversalRouterV1`, `0x3f` for
|
||||
`UniversalRouterV1_2_V2Support` and `UniversalRouterV2`, and `0x7f` for
|
||||
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`. The allow-revert flag
|
||||
(`0x80`) therefore does not change a step's name. Only the routers with the
|
||||
`0x7f` mask have `PAY_PORTION_FULL_PRECISION` (`0x07`), listed as
|
||||
`Pay Portion (Full Precision)`, and `ACROSS_V4_DEPOSIT_V3` (`0x40`), listed as
|
||||
`Across Bridge Deposit`; the other routers revert on `0x07`, and it is listed
|
||||
there as `Command 0x07`. The decoder does not read the commands inside an
|
||||
`EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that step stands they may spend
|
||||
the user's tokens or pay the user. That step is listed as
|
||||
(`0x80`) therefore does not change the command a step is read as. The router
|
||||
reverts the whole transaction when a step fails, unless the step's command
|
||||
byte has that flag set and its command is one the router lets fail, as each
|
||||
release's `contracts/base/Dispatcher.sol` shows: on `UniversalRouterV2`,
|
||||
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`, `PERMIT2_PERMIT`,
|
||||
`PERMIT2_PERMIT_BATCH`, `BALANCE_CHECK_ERC20`, `V3_POSITION_MANAGER_PERMIT`,
|
||||
`V3_POSITION_MANAGER_CALL`, `V4_INITIALIZE_POOL`, `V4_POSITION_MANAGER_CALL`
|
||||
and `EXECUTE_SUB_PLAN`; on `UniversalRouterV1_2_V2Support`,
|
||||
`BALANCE_CHECK_ERC20`, `EXECUTE_SUB_PLAN` and its NFT market calls and NFT
|
||||
owner checks; on `UniversalRouterV1`, its NFT market calls and NFT owner
|
||||
checks. Such a step may then fail while the transaction goes on to the next
|
||||
step, and it is listed with ` (allowed to fail)` after its name, for example
|
||||
`Balance Check (allowed to fail)`. On every other command, among them each V2,
|
||||
V3 and V4 swap and each payment of ETH or an ERC-20 token, the flag has no
|
||||
effect: the step succeeds or the whole transaction reverts, so the step reads
|
||||
as it does without the flag. A `BALANCE_CHECK_ERC20` the router lets fail
|
||||
guarantees nothing, so it is read as one whose `minBalance` is zero. On
|
||||
`UniversalRouterV1` and `UniversalRouterV1_2_V2Support`, `0x10` is an NFT
|
||||
market call the router lets fail, which the decoder still reads as a V4 swap
|
||||
([#592](https://git.eeqj.de/sneak/AutistMask/issues/592)); with the flag set,
|
||||
a minimum it states is read as zero: `None (no minimum guaranteed)`. Only the
|
||||
routers with the `0x7f` mask have `PAY_PORTION_FULL_PRECISION` (`0x07`),
|
||||
listed as `Pay Portion (Full Precision)`, and `ACROSS_V4_DEPOSIT_V3` (`0x40`),
|
||||
listed as `Across Bridge Deposit`; the other routers revert on `0x07`, and it
|
||||
is listed there as `Command 0x07`. The decoder does not read the commands
|
||||
inside an `EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that step stands they
|
||||
may spend the user's tokens or pay the user. That step is listed as
|
||||
`Execute Sub-Plan (its steps are not shown)`, and whatever the other steps
|
||||
state, the summary names no swap direction, `Token In` and `Token Out` read
|
||||
`Unknown (could not be decoded)`, and there is no `Amount` or `Min. received`
|
||||
|
||||
+105
-36
@@ -24,55 +24,104 @@ const ROUTER_IFACE = new Interface([
|
||||
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
||||
]);
|
||||
|
||||
// The Universal Router deployments on each network, by chain id, copied from
|
||||
// the deployment files Uniswap publishes, each address named by its key there.
|
||||
// The files also list `UnsupportedProtocol`, which is not a router.
|
||||
// The Universal Router releases deployed at the addresses below, each with two
|
||||
// things read from its source at the commit linked for it:
|
||||
//
|
||||
// Beside each address is the mask that router reads a command's type with:
|
||||
// its dispatcher runs `commandType & Commands.COMMAND_TYPE_MASK`, and the bits
|
||||
// above the mask are the allow-revert flag (0x80) or ignored. The mask grew
|
||||
// between releases. Each is `COMMAND_TYPE_MASK` in the release's
|
||||
// `contracts/libraries/Commands.sol`, the same file as in the source verified
|
||||
// for that address:
|
||||
// - 0x1f, UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Commands.sol
|
||||
// - 0x3f, UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/blob/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts/libraries/Commands.sol
|
||||
// - 0x3f, UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/libraries/Commands.sol
|
||||
// - 0x7f, UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/blob/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts/libraries/Commands.sol
|
||||
// - 0x7f, UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/libraries/Commands.sol
|
||||
// - `commandTypeMask`, the mask it reads a command's type with: its dispatcher
|
||||
// runs `commandType & Commands.COMMAND_TYPE_MASK`, and the bits above the
|
||||
// mask are the allow-revert flag (0x80) or ignored. The mask grew between
|
||||
// releases. Each is `COMMAND_TYPE_MASK` in the release's
|
||||
// `contracts/libraries/Commands.sol`, the same file as in the source
|
||||
// verified for its addresses.
|
||||
// - `allowRevert`, the command types it lets fail without reverting the
|
||||
// transaction. Its `execute()`, in `contracts/UniversalRouter.sol`, runs each
|
||||
// step through `dispatch()`, in `contracts/base/Dispatcher.sol`, and reverts
|
||||
// the whole transaction when a step fails, unless the step's command byte
|
||||
// has the allow-revert flag set: then it goes on to the next step. Only for
|
||||
// these command types does `dispatch()` return a failure. Every other
|
||||
// command, among them each V2, V3 and V4 swap and each payment of ETH or an
|
||||
// ERC-20 token, succeeds or reverts the whole transaction itself, so the
|
||||
// flag has no effect on it.
|
||||
//
|
||||
// UniversalRouterV1: https://github.com/Uniswap/universal-router/tree/cda697da221e24ca143bc4f55023694a2a16796f/contracts
|
||||
const ROUTER_V1 = {
|
||||
commandTypeMask: 0x1f,
|
||||
// Its NFT market calls (0x10 to 0x14, 0x18 to 0x1c) and NFT owner checks
|
||||
// (0x15, 0x16).
|
||||
allowRevert: [
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
],
|
||||
};
|
||||
// UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/tree/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts
|
||||
const ROUTER_V1_2 = {
|
||||
commandTypeMask: 0x3f,
|
||||
// BALANCE_CHECK_ERC20 (0x0e), its NFT market calls (0x10 to 0x13, 0x18 to
|
||||
// 0x1c, 0x1e, 0x20), its NFT owner checks (0x15, 0x16) and
|
||||
// EXECUTE_SUB_PLAN (0x21).
|
||||
allowRevert: [
|
||||
0x0e, 0x10, 0x11, 0x12, 0x13, 0x15, 0x16, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1e, 0x20, 0x21,
|
||||
],
|
||||
};
|
||||
// UniversalRouterV2: https://github.com/Uniswap/universal-router/tree/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts
|
||||
const ROUTER_V2 = {
|
||||
commandTypeMask: 0x3f,
|
||||
// PERMIT2_PERMIT_BATCH (0x03), PERMIT2_PERMIT (0x0a), BALANCE_CHECK_ERC20
|
||||
// (0x0e), V3_POSITION_MANAGER_PERMIT, V3_POSITION_MANAGER_CALL,
|
||||
// V4_INITIALIZE_POOL and V4_POSITION_MANAGER_CALL (0x11 to 0x14), and
|
||||
// EXECUTE_SUB_PLAN (0x21).
|
||||
allowRevert: [0x03, 0x0a, 0x0e, 0x11, 0x12, 0x13, 0x14, 0x21],
|
||||
};
|
||||
// UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/tree/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts
|
||||
// UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/tree/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts
|
||||
// The three files are the same in both releases.
|
||||
const ROUTER_V2_1 = {
|
||||
commandTypeMask: 0x7f,
|
||||
// The same as UniversalRouterV2's.
|
||||
allowRevert: ROUTER_V2.allowRevert,
|
||||
};
|
||||
|
||||
// The Universal Router deployments on each network, by chain id, copied from
|
||||
// the deployment files Uniswap publishes, each address named by its key there
|
||||
// and given with the release it runs. The files also list
|
||||
// `UnsupportedProtocol`, which is not a router.
|
||||
const ROUTER_ADDRESSES = new Map([
|
||||
[
|
||||
NETWORKS.mainnet.chainId,
|
||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
|
||||
[
|
||||
["0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", 0x1f], // UniversalRouterV1
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||
["0x66a9893cc07d91d95644aedd05d03f95e1dba8af", 0x3f], // UniversalRouterV2
|
||||
["0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", 0x7f], // UniversalRouterV2_1_1
|
||||
["0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", 0x7f], // UniversalRouterV2_1_2
|
||||
["0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", ROUTER_V1], // UniversalRouterV1
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", ROUTER_V1_2], // UniversalRouterV1_2_V2Support
|
||||
["0x66a9893cc07d91d95644aedd05d03f95e1dba8af", ROUTER_V2], // UniversalRouterV2
|
||||
["0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", ROUTER_V2_1], // UniversalRouterV2_1_1
|
||||
["0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", ROUTER_V2_1], // UniversalRouterV2_1_2
|
||||
],
|
||||
],
|
||||
[
|
||||
NETWORKS.sepolia.chainId,
|
||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
|
||||
[
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||
["0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", 0x3f], // UniversalRouterV2
|
||||
["0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", 0x7f], // UniversalRouterV2_1_1
|
||||
["0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", 0x7f], // UniversalRouterV2_1_2
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", ROUTER_V1_2], // UniversalRouterV1_2_V2Support
|
||||
["0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", ROUTER_V2], // UniversalRouterV2
|
||||
["0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", ROUTER_V2_1], // UniversalRouterV2_1_1
|
||||
["0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", ROUTER_V2_1], // UniversalRouterV2_1_2
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// The mask the Universal Router deployment at `address` on the network with
|
||||
// this chain id reads a command's type with, or null when `address` is not one
|
||||
// of them. Address case is ignored.
|
||||
function routerCommandTypeMask(address, chainId) {
|
||||
// The release that the Universal Router deployment at `address` on the
|
||||
// network with this chain id runs, or null when `address` is not one of them.
|
||||
// Address case is ignored.
|
||||
function routerRelease(address, chainId) {
|
||||
const routers = ROUTER_ADDRESSES.get(chainId) || [];
|
||||
const wanted = (address || "").toLowerCase();
|
||||
const router = routers.find(([a]) => a.toLowerCase() === wanted);
|
||||
return router ? router[1] : null;
|
||||
}
|
||||
|
||||
// The allow-revert flag on a command byte (see ROUTER_V1).
|
||||
const ALLOW_REVERT_FLAG = 0x80;
|
||||
|
||||
// The Uniswap V2 factory and pair init code hash the Universal Routers on each
|
||||
// network were deployed with. A V2 swap step swaps through the pair its path
|
||||
// starts at, which the router finds as `UniswapV2Library.pairFor()` does: the
|
||||
@@ -123,7 +172,7 @@ function v2PairFor(chainId, tokenA, tokenB) {
|
||||
}
|
||||
|
||||
// Universal Router command types, each read from its command byte with the
|
||||
// router's mask (see ROUTER_ADDRESSES). Only the routers with the 0x7f mask
|
||||
// router's mask (see ROUTER_V1). Only the routers with the 0x7f mask
|
||||
// have 0x07 and 0x40: 0x40 is above the other masks, and the other routers
|
||||
// revert on 0x07, so decode() names it there as it names a command not
|
||||
// listed here.
|
||||
@@ -856,8 +905,9 @@ function decodeV4Swap(input) {
|
||||
// sent to any other contract is not a swap through the router, whatever that
|
||||
// contract does with it.
|
||||
function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const commandTypeMask = routerCommandTypeMask(toAddress, chainId);
|
||||
if (commandTypeMask === null) return null;
|
||||
const release = routerRelease(toAddress, chainId);
|
||||
if (release === null) return null;
|
||||
const { commandTypeMask, allowRevert } = release;
|
||||
try {
|
||||
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
||||
if (!parsed) return null;
|
||||
@@ -986,6 +1036,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
|
||||
for (let i = 0; i < commandsBytes.length; i++) {
|
||||
const cmdId = commandsBytes[i] & commandTypeMask;
|
||||
// The router goes on to the next step if this one fails (see
|
||||
// ROUTER_V1). The flag on any other command has no effect.
|
||||
const allowedToFail =
|
||||
(commandsBytes[i] & ALLOW_REVERT_FLAG) !== 0 &&
|
||||
allowRevert.includes(cmdId);
|
||||
// Only the routers with the 0x7f mask have command 0x07 (see
|
||||
// COMMAND_NAMES).
|
||||
const name =
|
||||
@@ -1034,11 +1089,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const b = decodeBalanceCheck(inputs[i]);
|
||||
// The router passes this check whenever the owner holds at
|
||||
// least minBalance, so a zero one guarantees nothing and
|
||||
// does not replace a minimum an earlier step stated. Any
|
||||
// other minBalance sets the output side as a swap does.
|
||||
// It pays nobody.
|
||||
if (b && !(b.minBalance === 0n && present(minOutput))) {
|
||||
setOutput(b.token, b.minBalance);
|
||||
// does not replace a minimum an earlier step stated. A
|
||||
// check the router lets fail guarantees nothing either, so
|
||||
// it is read as a zero one. Any other minBalance sets the
|
||||
// output side as a swap does. It pays nobody.
|
||||
const minBalance = b && !allowedToFail ? b.minBalance : 0n;
|
||||
if (b && !(minBalance === 0n && present(minOutput))) {
|
||||
setOutput(b.token, minBalance);
|
||||
}
|
||||
} else if (cmdId === 0x00) {
|
||||
const s = decodeV3SwapExactIn(inputs[i]);
|
||||
@@ -1161,7 +1218,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
payeeUnread = true;
|
||||
}
|
||||
} else if (cmdId === 0x10) {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
let v4 = decodeV4Swap(inputs[i]);
|
||||
// A step the router lets fail guarantees nothing, so a
|
||||
// minimum it states is read as zero, as a
|
||||
// BALANCE_CHECK_ERC20's is. Only UniversalRouterV1 and
|
||||
// UniversalRouterV1_2_V2Support let this command fail; on
|
||||
// them it is an NFT market call, still read here as a V4
|
||||
// swap (https://git.eeqj.de/sneak/AutistMask/issues/592).
|
||||
if (v4 && allowedToFail && present(v4.amountOutMin)) {
|
||||
v4 = { ...v4, amountOutMin: 0n };
|
||||
}
|
||||
if (v4) {
|
||||
for (const t of v4.takes) {
|
||||
if (!isHop(i, t.currency, t.recipient)) {
|
||||
@@ -1225,6 +1291,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Skip commands we can't decode. One may pay any address.
|
||||
payeeUnread = true;
|
||||
}
|
||||
if (allowedToFail) {
|
||||
commandNames[i] += " (allowed to fail)";
|
||||
}
|
||||
}
|
||||
|
||||
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
|
||||
|
||||
+15
-7
@@ -987,13 +987,23 @@ describe("uniswap decoder", () => {
|
||||
);
|
||||
|
||||
// A sub-plan holding a V3 swap of USDT for WETH, which the decoder does
|
||||
// not read. With the allow-revert flag (0x80) the command byte is 0xa1.
|
||||
// not read. With the allow-revert flag (0x80) the command byte is 0xa1,
|
||||
// and the router lets the step fail
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/602).
|
||||
test.each([
|
||||
{ flag: "without", command: 0x21 },
|
||||
{ flag: "with", command: 0xa1 },
|
||||
{
|
||||
flag: "without",
|
||||
command: 0x21,
|
||||
steps: "Execute Sub-Plan (its steps are not shown)",
|
||||
},
|
||||
{
|
||||
flag: "with",
|
||||
command: 0xa1,
|
||||
steps: "Execute Sub-Plan (its steps are not shown) (allowed to fail)",
|
||||
},
|
||||
])(
|
||||
"an EXECUTE_SUB_PLAN step $flag the allow-revert flag says its steps are not shown",
|
||||
({ command }) => {
|
||||
({ command, steps }) => {
|
||||
const subPlan = encodeSubPlan("0x00", [
|
||||
encodeV3SwapExactIn(USER_ADDR, 2000000n, ONE_WETH, [
|
||||
USDT_ADDR,
|
||||
@@ -1013,9 +1023,7 @@ describe("uniswap decoder", () => {
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Steps").value).toBe(
|
||||
"Execute Sub-Plan (its steps are not shown)",
|
||||
);
|
||||
expect(detail(result, "Steps").value).toBe(steps);
|
||||
// The sub-plan names both currencies, so the lines must not say
|
||||
// the calldata did not.
|
||||
expect(detail(result, "Token In").value).toBe(
|
||||
|
||||
@@ -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