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)". Its commands are not read and may spend from or pay the user wherever the step stands, so with one 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. Model: opus-5-5
This commit was merged in pull request #591.
This commit is contained in:
+74
-27
@@ -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,21 @@ 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. The step
|
||||
// says so, and after this loop neither side is named.
|
||||
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
|
||||
@@ -767,14 +794,28 @@ function decode(data, toAddress, chainId, sources) {
|
||||
}
|
||||
}
|
||||
|
||||
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
|
||||
// or pay the user, before or after any other step, and a permit among
|
||||
// them would replace the input side. What the other steps state is
|
||||
// then not the whole swap, so both sides are set unknown, with no
|
||||
// figure, wherever the step stands.
|
||||
if (hasSubPlan) {
|
||||
inputToken = null;
|
||||
inputAmount = null;
|
||||
outputToken = null;
|
||||
minOutput = null;
|
||||
outputEstablished = true;
|
||||
}
|
||||
|
||||
// Resolve token info. A null token on either side means no step named
|
||||
// a currency for it, or a V4 step with a swap action left it
|
||||
// unknown; tokenInfo() refuses rather than calling it ETH.
|
||||
// UNWRAP_WETH turns WETH into ETH, so it makes the output ETH when the
|
||||
// output side is WETH, or when no step set the output side. A V4 step
|
||||
// with a swap action did set it, even when it set it unknown. Any other
|
||||
// output keeps its own token and figure: a swap that buys USDC and
|
||||
// then unwraps the WETH it did not spend receives USDC.
|
||||
// a currency for it, a V4 step with a swap action left it unknown, or
|
||||
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
||||
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||
// when the output side is WETH, or when no step set the output side. A
|
||||
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
||||
// even when it set it unknown. Any other output keeps its own token
|
||||
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
||||
// not spend receives USDC.
|
||||
const outputIsWeth =
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||
@@ -819,7 +860,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 +929,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