fix: the swap decoder reads each command's type with its router's mask (closes #535)
Each Universal Router address now carries the mask its own source reads a command's type with: 0x1f for UniversalRouterV1, 0x3f for V1_2 and V2, 0x7f for V2_1_1 and V2_1_2. The fixed 0x1f dropped the 0x20 bit, so EXECUTE_SUB_PLAN (0x21) was listed as a V3 exact-out swap. A sub-plan step is now listed as "Execute Sub-Plan (its steps are not shown)", and a Token In or Token Out line that nothing named reads "Unknown (could not be decoded)", since the sub-plan may name it. Model: opus-5-5
This commit is contained in:
+54
-20
@@ -19,39 +19,54 @@ const ROUTER_IFACE = new Interface([
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
const ROUTER_ADDRESSES = new Map([
|
||||
[
|
||||
NETWORKS.mainnet.chainId,
|
||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
|
||||
[
|
||||
"0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", // UniversalRouterV1
|
||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
||||
"0x66a9893cc07d91d95644aedd05d03f95e1dba8af", // UniversalRouterV2
|
||||
"0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", // UniversalRouterV2_1_1
|
||||
"0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", // UniversalRouterV2_1_2
|
||||
["0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", 0x1f], // UniversalRouterV1
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||
["0x66a9893cc07d91d95644aedd05d03f95e1dba8af", 0x3f], // UniversalRouterV2
|
||||
["0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", 0x7f], // UniversalRouterV2_1_1
|
||||
["0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", 0x7f], // UniversalRouterV2_1_2
|
||||
],
|
||||
],
|
||||
[
|
||||
NETWORKS.sepolia.chainId,
|
||||
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
|
||||
[
|
||||
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
|
||||
"0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", // UniversalRouterV2
|
||||
"0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", // UniversalRouterV2_1_1
|
||||
"0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", // UniversalRouterV2_1_2
|
||||
["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", 0x3f], // UniversalRouterV1_2_V2Support
|
||||
["0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", 0x3f], // UniversalRouterV2
|
||||
["0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", 0x7f], // UniversalRouterV2_1_1
|
||||
["0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", 0x7f], // UniversalRouterV2_1_2
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Whether `address` is a Universal Router deployment on the network with this
|
||||
// chain id. Address case is ignored.
|
||||
function isRouterAddress(address, chainId) {
|
||||
// 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) {
|
||||
const routers = ROUTER_ADDRESSES.get(chainId) || [];
|
||||
const wanted = (address || "").toLowerCase();
|
||||
return routers.some((router) => router.toLowerCase() === wanted);
|
||||
const router = routers.find(([a]) => a.toLowerCase() === wanted);
|
||||
return router ? router[1] : null;
|
||||
}
|
||||
|
||||
// Universal Router command IDs (lower 5 bits of each command byte)
|
||||
// Universal Router command types, each read from its command byte with the
|
||||
// router's mask (see ROUTER_ADDRESSES)
|
||||
const COMMAND_NAMES = {
|
||||
0x00: "V3 Swap (Exact In)",
|
||||
0x01: "V3 Swap (Exact Out)",
|
||||
@@ -90,8 +105,10 @@ function formatAmount(raw, decimals) {
|
||||
const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
||||
|
||||
// The same refusal when a V4 swap action could not be read (see
|
||||
// decodeExactly()). That action names both of its currencies, so saying the
|
||||
// calldata did not name one would be false.
|
||||
// decodeExactly()), or the transaction has an EXECUTE_SUB_PLAN step, whose
|
||||
// commands this decoder does not read. That action names both of its
|
||||
// currencies, and those commands may name one, so saying the calldata did not
|
||||
// name it would be false.
|
||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||
|
||||
// Explicit presence, never truthiness. Every gate in this file that guards a
|
||||
@@ -603,7 +620,8 @@ 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) {
|
||||
if (!isRouterAddress(toAddress, chainId)) return null;
|
||||
const commandTypeMask = routerCommandTypeMask(toAddress, chainId);
|
||||
if (commandTypeMask === null) return null;
|
||||
try {
|
||||
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
||||
if (!parsed) return null;
|
||||
@@ -621,6 +639,7 @@ function decode(data, toAddress, chainId, sources) {
|
||||
let hasUnwrapWeth = false;
|
||||
let hasV2ExactOut = false;
|
||||
let swapUndecoded = false;
|
||||
let hasSubPlan = false;
|
||||
const commandNames = [];
|
||||
|
||||
// THE INVARIANT: an amount and the token it is counted in always come
|
||||
@@ -660,13 +679,22 @@ function decode(data, toAddress, chainId, sources) {
|
||||
};
|
||||
|
||||
for (let i = 0; i < commandsBytes.length; i++) {
|
||||
const cmdId = commandsBytes[i] & 0x1f;
|
||||
const cmdId = commandsBytes[i] & commandTypeMask;
|
||||
commandNames.push(
|
||||
COMMAND_NAMES[cmdId] ||
|
||||
"Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
||||
);
|
||||
|
||||
try {
|
||||
if (cmdId === 0x21) {
|
||||
// EXECUTE_SUB_PLAN runs a further list of commands held in
|
||||
// its input, which this decoder does not read. What those
|
||||
// commands spend or receive is missing from every other
|
||||
// line, so the step says so.
|
||||
hasSubPlan = true;
|
||||
commandNames[i] += " (its steps are not shown)";
|
||||
}
|
||||
|
||||
if (cmdId === 0x0a) {
|
||||
const p = decodePermit2(inputs[i]);
|
||||
// A permit states both halves itself, so it may replace an
|
||||
@@ -819,7 +847,10 @@ function decode(data, toAddress, chainId, sources) {
|
||||
// screen must not describe the same condition in two ways.
|
||||
details.push({
|
||||
label: "Token In",
|
||||
value: swapUndecoded ? UNDECODED_CURRENCY : UNNAMED_CURRENCY,
|
||||
value:
|
||||
swapUndecoded || hasSubPlan
|
||||
? UNDECODED_CURRENCY
|
||||
: UNNAMED_CURRENCY,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -885,7 +916,10 @@ function decode(data, toAddress, chainId, sources) {
|
||||
// calldata did not state.
|
||||
details.push({
|
||||
label: "Token Out",
|
||||
value: swapUndecoded ? UNDECODED_CURRENCY : UNNAMED_CURRENCY,
|
||||
value:
|
||||
swapUndecoded || hasSubPlan
|
||||
? UNDECODED_CURRENCY
|
||||
: UNNAMED_CURRENCY,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user