decode() now records whether the user pays the V4 step that sets the input side any of its input token, and decides once, after the loop, whether that step counts as a take from the sender. A stated amount counts whoever pays it; with none stated, the step counts only when the user pays it. Paid from the router, it gets no Moved from the sender lines, with a PERMIT2_PERMIT of another token or without. When it counts and states no amount, its Unknown (could not be decoded) line also shows beside a PERMIT2_TRANSFER_FROM of its token, whose figure is on that move's own lines or, into the router, on its Paid to line. README Transaction Decoding says the same. Model: opus-5-5
2197 lines
100 KiB
JavaScript
2197 lines
100 KiB
JavaScript
// Decode Uniswap Universal Router execute() calldata into human-readable
|
|
// swap details. Designed to be extended with other DEX decoders later.
|
|
|
|
const {
|
|
Interface,
|
|
AbiCoder,
|
|
getBytes,
|
|
getAddress,
|
|
formatUnits,
|
|
getCreate2Address,
|
|
keccak256,
|
|
solidityPacked,
|
|
} = require("ethers");
|
|
const { truncateAmountNeverZero } = require("./amountDisplay");
|
|
const {
|
|
resolveTokenDecimals,
|
|
resolveTokenSymbol,
|
|
unknownDecimalsAmount,
|
|
} = require("./approvalAmount");
|
|
const { NETWORKS } = require("./networks");
|
|
|
|
const coder = AbiCoder.defaultAbiCoder();
|
|
|
|
const ROUTER_IFACE = new Interface([
|
|
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
|
|
]);
|
|
|
|
// The Universal Router releases deployed at the addresses below, each with
|
|
// three things read from its source at the commit linked for it:
|
|
//
|
|
// - `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.
|
|
// - `commands`, the command it runs for each command type, by the name that
|
|
// file gives it. Releases give some types different commands. A type the
|
|
// file names no command for, or only a placeholder, is left out: the router
|
|
// reverts on it, except UniversalRouterV1_2_V2Support on 0x14, which does
|
|
// nothing. decode() names and reads each step as its release's command.
|
|
// - `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,
|
|
commands: {
|
|
0x00: "V3_SWAP_EXACT_IN",
|
|
0x01: "V3_SWAP_EXACT_OUT",
|
|
0x02: "PERMIT2_TRANSFER_FROM",
|
|
0x03: "PERMIT2_PERMIT_BATCH",
|
|
0x04: "SWEEP",
|
|
0x05: "TRANSFER",
|
|
0x06: "PAY_PORTION",
|
|
0x08: "V2_SWAP_EXACT_IN",
|
|
0x09: "V2_SWAP_EXACT_OUT",
|
|
0x0a: "PERMIT2_PERMIT",
|
|
0x0b: "WRAP_ETH",
|
|
0x0c: "UNWRAP_WETH",
|
|
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
|
|
0x10: "SEAPORT",
|
|
0x11: "LOOKS_RARE_721",
|
|
0x12: "NFTX",
|
|
0x13: "CRYPTOPUNKS",
|
|
0x14: "LOOKS_RARE_1155",
|
|
0x15: "OWNER_CHECK_721",
|
|
0x16: "OWNER_CHECK_1155",
|
|
0x17: "SWEEP_ERC721",
|
|
0x18: "X2Y2_721",
|
|
0x19: "SUDOSWAP",
|
|
0x1a: "NFT20",
|
|
0x1b: "X2Y2_1155",
|
|
0x1c: "FOUNDATION",
|
|
0x1d: "SWEEP_ERC1155",
|
|
},
|
|
// 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,
|
|
commands: {
|
|
0x00: "V3_SWAP_EXACT_IN",
|
|
0x01: "V3_SWAP_EXACT_OUT",
|
|
0x02: "PERMIT2_TRANSFER_FROM",
|
|
0x03: "PERMIT2_PERMIT_BATCH",
|
|
0x04: "SWEEP",
|
|
0x05: "TRANSFER",
|
|
0x06: "PAY_PORTION",
|
|
0x08: "V2_SWAP_EXACT_IN",
|
|
0x09: "V2_SWAP_EXACT_OUT",
|
|
0x0a: "PERMIT2_PERMIT",
|
|
0x0b: "WRAP_ETH",
|
|
0x0c: "UNWRAP_WETH",
|
|
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
|
|
0x0e: "BALANCE_CHECK_ERC20",
|
|
0x10: "SEAPORT_V1_5",
|
|
0x11: "LOOKS_RARE_V2",
|
|
0x12: "NFTX",
|
|
0x13: "CRYPTOPUNKS",
|
|
0x15: "OWNER_CHECK_721",
|
|
0x16: "OWNER_CHECK_1155",
|
|
0x17: "SWEEP_ERC721",
|
|
0x18: "X2Y2_721",
|
|
0x19: "SUDOSWAP",
|
|
0x1a: "NFT20",
|
|
0x1b: "X2Y2_1155",
|
|
0x1c: "FOUNDATION",
|
|
0x1d: "SWEEP_ERC1155",
|
|
0x1e: "ELEMENT_MARKET",
|
|
0x20: "SEAPORT_V1_4",
|
|
0x21: "EXECUTE_SUB_PLAN",
|
|
0x22: "APPROVE_ERC20",
|
|
},
|
|
// 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,
|
|
commands: {
|
|
0x00: "V3_SWAP_EXACT_IN",
|
|
0x01: "V3_SWAP_EXACT_OUT",
|
|
0x02: "PERMIT2_TRANSFER_FROM",
|
|
0x03: "PERMIT2_PERMIT_BATCH",
|
|
0x04: "SWEEP",
|
|
0x05: "TRANSFER",
|
|
0x06: "PAY_PORTION",
|
|
0x08: "V2_SWAP_EXACT_IN",
|
|
0x09: "V2_SWAP_EXACT_OUT",
|
|
0x0a: "PERMIT2_PERMIT",
|
|
0x0b: "WRAP_ETH",
|
|
0x0c: "UNWRAP_WETH",
|
|
0x0d: "PERMIT2_TRANSFER_FROM_BATCH",
|
|
0x0e: "BALANCE_CHECK_ERC20",
|
|
0x10: "V4_SWAP",
|
|
0x11: "V3_POSITION_MANAGER_PERMIT",
|
|
0x12: "V3_POSITION_MANAGER_CALL",
|
|
0x13: "V4_INITIALIZE_POOL",
|
|
0x14: "V4_POSITION_MANAGER_CALL",
|
|
0x21: "EXECUTE_SUB_PLAN",
|
|
},
|
|
// 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,
|
|
// UniversalRouterV2's, and two more.
|
|
commands: {
|
|
...ROUTER_V2.commands,
|
|
0x07: "PAY_PORTION_FULL_PRECISION",
|
|
0x40: "ACROSS_V4_DEPOSIT_V3",
|
|
},
|
|
// 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", 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", 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 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
|
|
// CREATE2 address from the factory, with the hash of the path's first two
|
|
// tokens, sorted, as salt.
|
|
// - pairFor(): https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/modules/uniswap/v2/UniswapV2Library.sol
|
|
// - mainnet: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/script/deployParameters/DeployMainnet.s.sol
|
|
// - Sepolia: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/script/deployParameters/DeploySepolia.s.sol
|
|
// Every router in ROUTER_ADDRESSES was deployed with these (their constructor
|
|
// arguments on the explorer), except Sepolia's UniversalRouterV1_2_V2Support,
|
|
// which was deployed with two placeholders: the V2 factory
|
|
// 0x5302086A3a25d473aAbBd0356eFf8Dd811a4d89B (the `UnsupportedProtocol` its
|
|
// deployment file lists) and a zero init code hash. On Sepolia no contract
|
|
// sits at a pair any router there finds (the hash below is not that of the
|
|
// factory's own pairs), so every V2 swap step they run reverts.
|
|
const V2_FACTORIES = new Map([
|
|
[
|
|
NETWORKS.mainnet.chainId,
|
|
{
|
|
v2Factory: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f",
|
|
pairInitCodeHash:
|
|
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
|
|
},
|
|
],
|
|
[
|
|
NETWORKS.sepolia.chainId,
|
|
{
|
|
v2Factory: "0xB7f907f7A9eBC822a80BD25E224be42Ce0A698A0",
|
|
pairInitCodeHash:
|
|
"0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f",
|
|
},
|
|
],
|
|
]);
|
|
|
|
// The address of the V2 pair of `tokenA` and `tokenB` on the network with this
|
|
// chain id, found as the router finds it (see V2_FACTORIES).
|
|
function v2PairFor(chainId, tokenA, tokenB) {
|
|
const { v2Factory, pairInitCodeHash } = V2_FACTORIES.get(chainId);
|
|
const [token0, token1] =
|
|
tokenA.toLowerCase() < tokenB.toLowerCase()
|
|
? [tokenA, tokenB]
|
|
: [tokenB, tokenA];
|
|
return getCreate2Address(
|
|
v2Factory,
|
|
keccak256(solidityPacked(["address", "address"], [token0, token1])),
|
|
pairInitCodeHash,
|
|
);
|
|
}
|
|
|
|
// The Steps line's name for each command in the releases' `commands` (see
|
|
// ROUTER_V1).
|
|
const COMMAND_NAMES = {
|
|
V3_SWAP_EXACT_IN: "V3 Swap (Exact In)",
|
|
V3_SWAP_EXACT_OUT: "V3 Swap (Exact Out)",
|
|
PERMIT2_TRANSFER_FROM: "Permit2 Transfer",
|
|
PERMIT2_PERMIT_BATCH: "Permit2 Permit Batch",
|
|
SWEEP: "Sweep",
|
|
TRANSFER: "Transfer",
|
|
PAY_PORTION: "Pay Portion",
|
|
PAY_PORTION_FULL_PRECISION: "Pay Portion (Full Precision)",
|
|
V2_SWAP_EXACT_IN: "V2 Swap (Exact In)",
|
|
V2_SWAP_EXACT_OUT: "V2 Swap (Exact Out)",
|
|
PERMIT2_PERMIT: "Permit2 Permit",
|
|
WRAP_ETH: "Wrap ETH",
|
|
UNWRAP_WETH: "Unwrap WETH",
|
|
PERMIT2_TRANSFER_FROM_BATCH: "Permit2 Transfer Batch",
|
|
BALANCE_CHECK_ERC20: "Balance Check",
|
|
V4_SWAP: "V4 Swap",
|
|
V3_POSITION_MANAGER_PERMIT: "V3 Position Mgr Permit",
|
|
V3_POSITION_MANAGER_CALL: "V3 Position Mgr Call",
|
|
V4_INITIALIZE_POOL: "V4 Initialize Pool",
|
|
V4_POSITION_MANAGER_CALL: "V4 Position Mgr Call",
|
|
EXECUTE_SUB_PLAN: "Execute Sub-Plan",
|
|
ACROSS_V4_DEPOSIT_V3: "Across Bridge Deposit",
|
|
// The NFT commands of UniversalRouterV1 and UniversalRouterV1_2_V2Support,
|
|
// and the latter's APPROVE_ERC20.
|
|
SEAPORT: "Seaport",
|
|
SEAPORT_V1_4: "Seaport V1.4",
|
|
SEAPORT_V1_5: "Seaport V1.5",
|
|
LOOKS_RARE_721: "LooksRare 721",
|
|
LOOKS_RARE_1155: "LooksRare 1155",
|
|
LOOKS_RARE_V2: "LooksRare V2",
|
|
NFTX: "NFTX",
|
|
CRYPTOPUNKS: "CryptoPunks",
|
|
OWNER_CHECK_721: "Owner Check 721",
|
|
OWNER_CHECK_1155: "Owner Check 1155",
|
|
SWEEP_ERC721: "Sweep ERC-721",
|
|
SWEEP_ERC1155: "Sweep ERC-1155",
|
|
X2Y2_721: "X2Y2 721",
|
|
X2Y2_1155: "X2Y2 1155",
|
|
SUDOSWAP: "Sudoswap",
|
|
NFT20: "NFT20",
|
|
FOUNDATION: "Foundation",
|
|
ELEMENT_MARKET: "Element Market",
|
|
APPROVE_ERC20: "Approve ERC-20",
|
|
};
|
|
|
|
// The swap's Amount and Min. received lines land on the same approval screen,
|
|
// and Amount is carried to the wait/success/error screens as the ERC-20 line
|
|
// is, so they take the same nonzero floor: a swap of an amount below 0.0001 is
|
|
// not "0.0000", and a slippage floor of one base unit does not read as "you may
|
|
// receive nothing".
|
|
function formatAmount(raw, decimals) {
|
|
return truncateAmountNeverZero(formatUnits(raw, decimals));
|
|
}
|
|
|
|
// One wording for either side of the screen: a currency the calldata never
|
|
// named. It reads as a refusal, the same stance unknownDecimalsAmount() takes
|
|
// on a scale — not as a token name, and not as a quantity. The Paid to line
|
|
// uses it too, when no step names anyone paid.
|
|
const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
|
|
|
// The same refusal when a V4 swap action could not be read (see
|
|
// 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. The Paid to line uses it too, when a step may pay an
|
|
// address this decoder did not read, and so do the lines for what a step
|
|
// takes from the sender, when this decoder does not know it.
|
|
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
|
|
|
// The Paid to line for each token paid to the router, the swap's output or
|
|
// another, that no later step pays out of the router.
|
|
const STAYS_IN_ROUTER =
|
|
"The router, where it stays (no later step pays it out)";
|
|
|
|
// The Paid to line for an ACROSS_V4_DEPOSIT_V3 step (see
|
|
// decodeAcrossV4DepositV3()).
|
|
const PAID_TO_BRIDGE =
|
|
"The bridge, which pays the deposit's recipient on another network";
|
|
|
|
// Explicit presence, never truthiness. Every gate in this file that guards a
|
|
// decoded value goes through here: an address is never falsy once set, but an
|
|
// amount of 0n is, and a gate that cannot tell a genuine zero from an absent
|
|
// value is the trap this decoder has now been bitten by five times.
|
|
function present(value) {
|
|
return value !== null && value !== undefined;
|
|
}
|
|
|
|
// Whether `a` and `b` are both known and the same address.
|
|
function sameAddress(a, b) {
|
|
return present(a) && present(b) && a.toLowerCase() === b.toLowerCase();
|
|
}
|
|
|
|
// Uniswap V4 spells "use the whole open delta" as an amount of zero:
|
|
// v4-periphery `src/libraries/ActionConstants.sol` declares
|
|
// `uint128 internal constant OPEN_DELTA = 0` ("used to signal that an action
|
|
// should use the input value of the open delta on the pool manager or of the
|
|
// balance that the contract holds"), and `src/V4Router.sol` substitutes the
|
|
// full open credit whenever an exact-in swap action's `amountIn` equals it:
|
|
//
|
|
// uint128 amountIn = params.amountIn;
|
|
// if (amountIn == ActionConstants.OPEN_DELTA) {
|
|
// amountIn = _getFullCredit(...).toUint128();
|
|
// }
|
|
//
|
|
// in both `_swapExactInputSingle` and `_swapExactInput`. Sentinel and literal
|
|
// zero are the same uint128 word, so the encoding CANNOT distinguish them —
|
|
// and the router does not try: it reads every zero as the sentinel, so in V4
|
|
// there is no such thing as an exact-in swap of literally zero. The amount is
|
|
// therefore not stated by the calldata at all; it is whatever credit is open
|
|
// at execution time. It is carried as this sentinel rather than as 0n because
|
|
// printing "0.0000" would state the exact inverse of what will happen —
|
|
// "nothing is being swapped" for a step that swaps the entire balance.
|
|
//
|
|
// `amountOutMinimum` gets no such mapping: V4Router compares it directly
|
|
// (`if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived`), so
|
|
// a zero minimum is a literal zero slippage floor and is stated as one. Nor
|
|
// does the V3 path have it — universal-router's `V3SwapRouter.v3SwapExactInput`
|
|
// special-cases only `ActionConstants.CONTRACT_BALANCE` (1<<255), never zero —
|
|
// so a zero V3 `amountIn` is a literal zero and is displayed as one. The V2
|
|
// exact-in path gives zero a meaning of its own: see ALREADY_PAID.
|
|
const OPEN_DELTA = Symbol("v4-open-delta");
|
|
|
|
// The Universal Router's V2 exact-in spells "the pair already holds the input
|
|
// tokens" as an amount of zero: universal-router
|
|
// `contracts/libraries/Constants.sol` declares
|
|
// `uint256 internal constant ALREADY_PAID = 0` ("Used for identifying cases
|
|
// when a v2 pair has already received input tokens"), and
|
|
// `V2SwapRouter.v2SwapExactInput` makes no payment of its own when `amountIn`
|
|
// equals it. The swap then spends whatever an earlier step sent to the pair.
|
|
// As with OPEN_DELTA, the calldata states no quantity, and "0.0000" would say
|
|
// that nothing is swapped.
|
|
const ALREADY_PAID = Symbol("v2-already-paid");
|
|
|
|
// The router's whole-balance amount, universal-router
|
|
// `ActionConstants.CONTRACT_BALANCE`: a V2 exact-in given it as `amountIn`
|
|
// pays the pair all the router holds of the input token.
|
|
const CONTRACT_BALANCE = 2n ** 255n;
|
|
|
|
// The amount lines that state a fact instead of a quantity. Same register as
|
|
// UNNAMED_CURRENCY — a sentence in the value slot, so it cannot be misread as a
|
|
// number — and deliberately not another phrasing of "not named": these say
|
|
// different things.
|
|
const OPEN_DELTA_AMOUNT = "All available (V4 open delta)";
|
|
const ALREADY_PAID_AMOUNT =
|
|
"Whatever an earlier step sent to the pair (V2 already paid)";
|
|
const NO_MINIMUM = "None (no minimum guaranteed)";
|
|
|
|
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
|
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
|
|
|
|
// WETH, the token WRAP_ETH turns ETH into and UNWRAP_WETH turns into ETH, on
|
|
// each network, by chain id: `weth9` in the deployment parameters
|
|
// V2_FACTORIES cites, which every router in ROUTER_ADDRESSES was deployed
|
|
// with (their constructor arguments on the explorer).
|
|
const WETH = new Map([
|
|
[NETWORKS.mainnet.chainId, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"],
|
|
[NETWORKS.sepolia.chainId, "0xfff9976782d46cc05630d1f6ebab18b2324d6b14"],
|
|
]);
|
|
// Either one counts on either network as the WETH UNWRAP_WETH turns into ETH.
|
|
const WETH_ADDRESSES = [...WETH.values()];
|
|
|
|
// Two recipient values the router reads as placeholders, not as addresses:
|
|
// MSG_SENDER pays the caller of execute(), which is the transaction's sender,
|
|
// and ADDRESS_THIS pays the router itself, which holds what it is paid until a
|
|
// later step pays it out. The swap commands, SWEEP, WRAP_ETH, UNWRAP_WETH,
|
|
// TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION and, on every router but
|
|
// UniversalRouterV1, PERMIT2_TRANSFER_FROM map their recipient through the
|
|
// dispatcher's map(), and a V4 TAKE and TAKE_PORTION through V4Router's
|
|
// _mapRecipient(); TAKE_ALL always pays the caller.
|
|
// - constants: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Constants.sol
|
|
// - map(), UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Recipient.sol
|
|
// - map(), UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
|
// - constants, V4: https://github.com/Uniswap/v4-periphery/blob/444c526b77d804590f0d7bc5a481af5a3277c952/src/libraries/ActionConstants.sol
|
|
// - _mapRecipient(): https://github.com/Uniswap/v4-periphery/blob/444c526b77d804590f0d7bc5a481af5a3277c952/src/base/BaseActionsRouter.sol
|
|
// - TAKE, TAKE_ALL and TAKE_PORTION: https://github.com/Uniswap/v4-periphery/blob/444c526b77d804590f0d7bc5a481af5a3277c952/src/V4Router.sol
|
|
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
|
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
|
|
|
// `decimals` is null when nothing knows this token's scale. It is not
|
|
// defaulted to 18: the swap lines land on the same approval screen as the
|
|
// ERC-20 line, and a scale guessed there is what showed a 1,000 USDT swap as
|
|
// 0.000000000001. `sources` is { trackedTokens, wallets }, shaped as they are
|
|
// on `state`; resolveTokenDecimals() reads the bundled list, then those.
|
|
//
|
|
// A null `address` means UNDETERMINED — the calldata named no currency for
|
|
// that side — and is refused rather than named. It is not native ETH: Uniswap
|
|
// V4 spells native ETH as `Currency.wrap(address(0))` (v4-core
|
|
// `type Currency is address`), and a Currency is ABI-encoded as a plain
|
|
// address word, so every decode site here gets back the truthy string
|
|
// "0x0000000000000000000000000000000000000000" for it — never null. WRAP_ETH
|
|
// sets that same explicit zero address, and an UNWRAP_WETH output is caught by
|
|
// its caller before this is consulted, so nothing that genuinely is ETH
|
|
// arrives null. Naming a null ETH states the wrong asset and formats its
|
|
// amount at the wrong scale.
|
|
function tokenInfo(address, sources) {
|
|
if (!address) {
|
|
return { symbol: null, decimals: null, address: null };
|
|
}
|
|
if (address === "0x0000000000000000000000000000000000000000") {
|
|
return { symbol: "ETH", decimals: 18, address: null };
|
|
}
|
|
return {
|
|
symbol: resolveTokenSymbol(address, sources),
|
|
decimals: resolveTokenDecimals(address, sources),
|
|
address,
|
|
};
|
|
}
|
|
|
|
// A swap amount line. With a scale it is the token quantity; with none it is
|
|
// the base-unit integer with the unknown scale stated, the same refusal the
|
|
// ERC-20 amount line makes, so the screen has one way of saying it. `display`
|
|
// is the line on the screen, `raw` is what the status screens carry.
|
|
function amountText(raw, info) {
|
|
if (info.decimals === null) {
|
|
const unknown = unknownDecimalsAmount(raw);
|
|
return { raw: unknown, display: unknown };
|
|
}
|
|
const formatted = formatAmount(raw, info.decimals);
|
|
return {
|
|
raw: formatted,
|
|
display: formatted + (info.symbol ? " " + info.symbol : ""),
|
|
};
|
|
}
|
|
|
|
// The line giving a moved token under `label`, in the shape of the Token In
|
|
// line, so the screens show it as they show that one. ETH has no address.
|
|
function tokenMovedLine(label, token, sources) {
|
|
const info = tokenInfo(token, sources);
|
|
if (!present(info.address)) {
|
|
return { label, value: info.symbol };
|
|
}
|
|
return {
|
|
label,
|
|
value: info.symbol ? info.symbol + " (" + token + ")" : token,
|
|
address: token,
|
|
isToken: true,
|
|
};
|
|
}
|
|
|
|
// Decode PERMIT2_PERMIT (command 0x0a) input bytes.
|
|
// ABI: ((address token, uint160 amount, uint48 expiration, uint48 nonce),
|
|
// address spender, uint256 sigDeadline), bytes signature
|
|
function decodePermit2(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
[
|
|
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
|
|
"bytes",
|
|
],
|
|
input,
|
|
);
|
|
return { token: d[0][0][0], amount: d[0][0][1], spender: d[0][1] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode BALANCE_CHECK_ERC20 (command 0x0e) input bytes. The router passes the
|
|
// check when `owner` holds at least `minBalance` of `token`, and reads `owner`
|
|
// as written, without map(), on every release that has the command:
|
|
// - UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/blob/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV2_1_1 and UniversalRouterV2_1_2, the same file at both
|
|
// commits: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/base/Dispatcher.sol
|
|
// ABI: (address owner, address token, uint256 minBalance)
|
|
function decodeBalanceCheck(input) {
|
|
try {
|
|
const d = coder.decode(["address", "address", "uint256"], input);
|
|
return { owner: d[0], token: d[1], minBalance: d[2] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V2_SWAP_EXACT_IN (command 0x08) input bytes.
|
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
|
// address[] path, bool payerIsUser)
|
|
// A zero `amountIn` is read the way the router reads it, as ALREADY_PAID.
|
|
// With `payerIsUser` false the router pays the input from its own balance.
|
|
function decodeV2SwapExactIn(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "address[]", "bool"],
|
|
input,
|
|
);
|
|
return {
|
|
recipient: d[0],
|
|
amountIn: d[1] === 0n ? ALREADY_PAID : d[1],
|
|
amountOutMin: d[2],
|
|
path: d[3],
|
|
tokenIn: d[3][0],
|
|
tokenOut: d[3][d[3].length - 1],
|
|
payerIsUser: d[4],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
|
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
|
// address[] path, bool payerIsUser)
|
|
// With `payerIsUser` false the router pays the input from its own balance.
|
|
function decodeV2SwapExactOut(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "address[]", "bool"],
|
|
input,
|
|
);
|
|
return {
|
|
recipient: d[0],
|
|
amountOut: d[1],
|
|
amountInMax: d[2],
|
|
tokenIn: d[3][0],
|
|
tokenOut: d[3][d[3].length - 1],
|
|
payerIsUser: d[4],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// The first and last token of a V3 swap path (packed: token(20) + fee(3) +
|
|
// token(20) ...), or null when it is not one token followed by whole hops of
|
|
// a fee and a token (40 hex digits, then 46 per hop). The router reads only
|
|
// whole hops and ignores any bytes after the last one, so the last 20 bytes
|
|
// of such a path are not the token it reads there.
|
|
function decodeV3Path(pathHex) {
|
|
const hex = pathHex.startsWith("0x") ? pathHex.slice(2) : pathHex;
|
|
if (hex.length < 40 || (hex.length - 40) % 46 !== 0) return null;
|
|
const first = "0x" + hex.slice(0, 40);
|
|
const last = "0x" + hex.slice(-40);
|
|
return { first, last };
|
|
}
|
|
|
|
// Decode V3_SWAP_EXACT_IN (command 0x00) input bytes.
|
|
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
|
// bytes path, bool payerIsUser)
|
|
// With `payerIsUser` false the router pays the input from its own balance.
|
|
function decodeV3SwapExactIn(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "bytes", "bool"],
|
|
input,
|
|
);
|
|
const path = decodeV3Path(d[3]);
|
|
if (!path) return null;
|
|
return {
|
|
recipient: d[0],
|
|
amountIn: d[1],
|
|
amountOutMin: d[2],
|
|
tokenIn: path.first,
|
|
tokenOut: path.last,
|
|
payerIsUser: d[4],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode V3_SWAP_EXACT_OUT (command 0x01) input bytes.
|
|
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
|
// bytes path, bool payerIsUser)
|
|
// The router reads an exact-out path from the token it buys to the token it
|
|
// spends: it buys `amountOut` of the path's first token from the path's first
|
|
// pool and pays at most `amountInMax` of its last token to its last pool
|
|
// (`v3SwapExactOutput()` and `uniswapV3SwapCallback()` in each release's
|
|
// `contracts/modules/uniswap/v3/V3SwapRouter.sol`, at the commit linked for
|
|
// it above).
|
|
// With `payerIsUser` false the router pays the input from its own balance.
|
|
function decodeV3SwapExactOut(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
["address", "uint256", "uint256", "bytes", "bool"],
|
|
input,
|
|
);
|
|
const path = decodeV3Path(d[3]);
|
|
if (!path) return null;
|
|
return {
|
|
recipient: d[0],
|
|
amountOut: d[1],
|
|
amountInMax: d[2],
|
|
tokenIn: path.last,
|
|
tokenOut: path.first,
|
|
payerIsUser: d[4],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode SWEEP (command 0x04) input bytes. The router pays its whole balance
|
|
// of `token` to the recipient.
|
|
// ABI: (address token, address recipient, uint256 amountMin)
|
|
function decodeSweep(input) {
|
|
try {
|
|
const d = coder.decode(["address", "address", "uint256"], input);
|
|
return { token: d[0], recipient: d[1] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION and PERMIT2_TRANSFER_FROM
|
|
// read their parameters as each release's dispatcher reads them:
|
|
// - UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/blob/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/blob/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts/base/Dispatcher.sol
|
|
// - UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/base/Dispatcher.sol
|
|
|
|
// Decode TRANSFER (command 0x05) input bytes, and those of PAY_PORTION (0x06)
|
|
// and PAY_PORTION_FULL_PRECISION (0x07), which share its layout. Each pays
|
|
// `token` out of the router to the recipient. TRANSFER pays `amount`, or, of a
|
|
// token other than ETH, all the router holds when `amount` is
|
|
// CONTRACT_BALANCE; it sends ETH as written, so a TRANSFER of
|
|
// CONTRACT_BALANCE of ETH (`token` the zero address) always reverts.
|
|
// PAY_PORTION pays `amount` ten-thousandths of what the router holds, and
|
|
// PAY_PORTION_FULL_PRECISION `amount` 1e18ths of it, of ETH too.
|
|
// - UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/modules/Payments.sol
|
|
// - UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/modules/Payments.sol
|
|
// ABI: (address token, address recipient, uint256 amount)
|
|
function decodeTransfer(input) {
|
|
try {
|
|
const d = coder.decode(["address", "address", "uint256"], input);
|
|
return { token: d[0], recipient: d[1], amount: d[2] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode PERMIT2_TRANSFER_FROM (command 0x02) input bytes. Permit2 moves
|
|
// `amount` of `token` from the transaction's sender to the recipient.
|
|
// UniversalRouterV1 hands Permit2 the recipient as written, without map(), so
|
|
// on that router address(1) and address(2) are paid as those addresses.
|
|
// ABI: (address token, address recipient, uint160 amount)
|
|
function decodePermit2TransferFrom(input) {
|
|
try {
|
|
const d = coder.decode(["address", "address", "uint160"], input);
|
|
return { token: d[0], recipient: d[1], amount: d[2] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode ACROSS_V4_DEPOSIT_V3 (command 0x40) input bytes. The router hands
|
|
// the bridge `inputAmount` of `inputToken`, or all it holds of it when that is
|
|
// CONTRACT_BALANCE; with `useNative` set it hands over its ETH instead. It
|
|
// passes `depositor` and `recipient` on as written, without map(). The bridge
|
|
// pays the recipient on another network what the deposit states, and refunds
|
|
// a deposit nobody fills to the depositor, who may also change the deposit's
|
|
// recipient.
|
|
// - UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/blob/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts/modules/ChainedActions.sol
|
|
// - UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/modules/ChainedActions.sol
|
|
// - parameters, the same at both commits: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/interfaces/IUniversalRouter.sol
|
|
// - the bridge's depositV3() and its refund to the depositor: https://github.com/across-protocol/contracts/blob/950e9154f3d61e59590eb7d4403fae5d3cae84f8/contracts/spoke-pools/SpokePool.sol
|
|
// ABI: (tuple(address depositor, address recipient, address inputToken,
|
|
// address outputToken, uint256 inputAmount, uint256 outputAmount,
|
|
// uint256 destinationChainId, address exclusiveRelayer,
|
|
// uint32 quoteTimestamp, uint32 fillDeadline,
|
|
// uint32 exclusivityDeadline, bytes message, bool useNative))
|
|
function decodeAcrossV4DepositV3(input) {
|
|
try {
|
|
const d = coder.decode(
|
|
[
|
|
"tuple(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes,bool)",
|
|
],
|
|
input,
|
|
);
|
|
return {
|
|
depositor: d[0][0],
|
|
recipient: d[0][1],
|
|
inputToken: d[0][2],
|
|
inputAmount: d[0][4],
|
|
useNative: d[0][12],
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode WRAP_ETH (command 0x0b) input bytes. The router pays the WETH to the
|
|
// recipient.
|
|
// ABI: (address recipient, uint256 amount)
|
|
function decodeWrapEth(input) {
|
|
try {
|
|
const d = coder.decode(["address", "uint256"], input);
|
|
return { recipient: d[0], amount: d[1] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Decode UNWRAP_WETH (command 0x0c) input bytes. The router turns its whole
|
|
// WETH balance into ETH and pays it to the recipient.
|
|
// ABI: (address recipient, uint256 amountMin)
|
|
function decodeUnwrapWeth(input) {
|
|
try {
|
|
const d = coder.decode(["address", "uint256"], input);
|
|
return { recipient: d[0] };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// V4 inner action IDs
|
|
const V4_SWAP_EXACT_IN_SINGLE = 0x06;
|
|
const V4_SWAP_EXACT_IN = 0x07;
|
|
const V4_SWAP_EXACT_OUT_SINGLE = 0x08;
|
|
const V4_SWAP_EXACT_OUT = 0x09;
|
|
const V4_SETTLE = 0x0b;
|
|
const V4_SETTLE_ALL = 0x0c;
|
|
const V4_TAKE = 0x0e;
|
|
const V4_TAKE_ALL = 0x0f;
|
|
const V4_TAKE_PORTION = 0x10;
|
|
|
|
// A V4 exact-in `amountIn`, read the way V4Router reads it: zero is
|
|
// ActionConstants.OPEN_DELTA, not a quantity of zero. See the OPEN_DELTA
|
|
// comment above. The exact-OUT actions below decode no amounts at all, so
|
|
// their own OPEN_DELTA mapping on `amountOut` never reaches the screen.
|
|
function v4ExactInAmount(raw) {
|
|
return raw === 0n ? OPEN_DELTA : raw;
|
|
}
|
|
|
|
// The V4 swap actions are read only in the parameter structs V4Router has
|
|
// deployed on mainnet and Sepolia. v4-periphery `main` has since added a
|
|
// `minHopPriceX36` field to all four (`src/interfaces/IV4Router.sol`), and
|
|
// reading a shape nobody has deployed would be guessing. ethers alone would
|
|
// read the newer structs anyway: it takes whatever word sits where a
|
|
// deployed field would be, ignores bytes left over, and holds back an error
|
|
// in a field until that field is used. The newer multi-hop exact-in struct
|
|
// then yields the offset of its hop-price array as `amountIn`. So a decode
|
|
// counts only when encoding the result again gives back the same bytes;
|
|
// otherwise this throws, and decodeV4Swap() marks the step as not decoded.
|
|
function decodeExactly(types, data) {
|
|
const decoded = coder.decode(types, data);
|
|
if (coder.encode(types, decoded) !== data) {
|
|
throw new Error("Not in the deployed V4 parameter shape.");
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
// Decode one V4 swap action's parameters into the currency it spends
|
|
// (`tokenIn`), the currency it buys (`tokenOut`) and, for an exact-in action,
|
|
// its `amountIn` and `amountOutMin`. The exact-out actions decode no amounts.
|
|
// A field the action does not state is null. Throws when the parameters are
|
|
// not in the deployed shape (see decodeExactly()).
|
|
function decodeV4SwapAction(actionId, param) {
|
|
if (actionId === V4_SWAP_EXACT_IN) {
|
|
// ExactInputParams: (address currencyIn,
|
|
// tuple(address,uint24,int24,address,bytes)[] path,
|
|
// uint128 amountIn, uint128 amountOutMin)
|
|
const s = decodeExactly(
|
|
[
|
|
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
|
],
|
|
param,
|
|
);
|
|
const path = s[0][1];
|
|
return {
|
|
tokenIn: s[0][0],
|
|
tokenOut: path.length > 0 ? path[path.length - 1][0] : null,
|
|
amountIn: v4ExactInAmount(s[0][2]),
|
|
amountOutMin: s[0][3],
|
|
};
|
|
}
|
|
if (actionId === V4_SWAP_EXACT_IN_SINGLE) {
|
|
// ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
|
|
// bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
|
|
const s = decodeExactly(
|
|
[
|
|
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
|
],
|
|
param,
|
|
);
|
|
const poolKey = s[0][0];
|
|
const zeroForOne = s[0][1];
|
|
return {
|
|
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
|
|
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
|
|
amountIn: v4ExactInAmount(s[0][2]),
|
|
amountOutMin: s[0][3],
|
|
};
|
|
}
|
|
if (actionId === V4_SWAP_EXACT_OUT) {
|
|
// ExactOutputParams: (address currencyOut,
|
|
// tuple(address,uint24,int24,address,bytes)[] path,
|
|
// uint128 amountOut, uint128 amountInMaximum)
|
|
const s = decodeExactly(
|
|
[
|
|
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
|
],
|
|
param,
|
|
);
|
|
// The router works through the path from its last entry back to its
|
|
// first, starting from currencyOut, so the first entry is what the
|
|
// action spends.
|
|
const path = s[0][1];
|
|
return {
|
|
tokenIn: path.length > 0 ? path[0][0] : null,
|
|
tokenOut: s[0][0],
|
|
amountIn: null,
|
|
amountOutMin: null,
|
|
};
|
|
}
|
|
// V4_SWAP_EXACT_OUT_SINGLE. ExactOutputSingleParams: (poolKey as above,
|
|
// bool zeroForOne, uint128 amountOut, uint128 amountInMaximum,
|
|
// bytes hookData)
|
|
const s = decodeExactly(
|
|
[
|
|
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
|
],
|
|
param,
|
|
);
|
|
const poolKey = s[0][0];
|
|
const zeroForOne = s[0][1];
|
|
return {
|
|
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
|
|
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
|
|
amountIn: null,
|
|
amountOutMin: null,
|
|
};
|
|
}
|
|
|
|
// The currencies in `list` that `other` does not hold, each listed once.
|
|
function currenciesNotIn(list, other) {
|
|
return [...new Set(list.filter((c) => !other.includes(c)))];
|
|
}
|
|
|
|
// The amount that the swap actions on one side of a V4 step state together:
|
|
// the one action's own amount, or the sum when there are several and each
|
|
// states a quantity. Exact-out actions state none here, and the open delta is
|
|
// not a quantity. Otherwise null.
|
|
function totalAmount(amounts) {
|
|
if (amounts.length === 1) return amounts[0];
|
|
if (amounts.length === 0) return null;
|
|
if (!amounts.every((a) => typeof a === "bigint")) return null;
|
|
return amounts.reduce((sum, a) => sum + a, 0n);
|
|
}
|
|
|
|
// Decode V4_SWAP (command 0x10) input bytes.
|
|
// The input is ABI-encoded as (bytes actions, bytes[] params). Each swap
|
|
// action gives the currency it spends and the currency it buys (see
|
|
// decodeV4SwapAction()). The step's sides then follow three rules:
|
|
//
|
|
// 1. Token In is the currency SETTLE or SETTLE_ALL names; otherwise the one
|
|
// currency that some swap action spends and none buys. Token Out is the
|
|
// currency TAKE or TAKE_ALL names; otherwise the one currency that some
|
|
// swap action buys and none spends. If there is not exactly one such
|
|
// currency, that side is unknown.
|
|
// 2. Amount is the amountIn of the swap action that spends Token In. If
|
|
// several exact-in actions spend it, each with a stated amount, it is
|
|
// their sum; in any other case there is none. Min. received is the same
|
|
// for amountOutMin and the actions that buy Token Out.
|
|
// 3. If any swap action in the step cannot be read, only SETTLE, SETTLE_ALL,
|
|
// TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
|
|
// received.
|
|
//
|
|
// A currency a swap action does not name, such as what an exact-in action
|
|
// with an empty path buys, is null and counts in rules 1 and 2 as a currency
|
|
// like any other: a step of only that action has an unknown Token Out and
|
|
// the action's amountOutMin as Min. received.
|
|
//
|
|
// `hasSwapAction` is true when the step has a swap action, read or not.
|
|
// decode() then always sets the output side from this step, and sets the
|
|
// input side from it only when no earlier step set it. A side it sets that
|
|
// these rules leave unknown is set unknown, with no figure. A step with no
|
|
// swap action sets only the sides SETTLE, SETTLE_ALL, TAKE and TAKE_ALL
|
|
// name. Under each of the three rules, rule 3 included, when the user pays
|
|
// the step any of Token In (`paidByUser` below), decode() takes what the user
|
|
// pays it of Token In as the step's amount in place of the rules' Amount, or
|
|
// no amount when this decoder cannot tell what that is.
|
|
//
|
|
// `takes` is every TAKE, TAKE_ALL and TAKE_PORTION, as the currency it takes
|
|
// and its recipient as written, MSG_SENDER for a TAKE_ALL. TAKE_PORTION takes
|
|
// a share of the currency, so it does not name the output side as TAKE does.
|
|
// `recipientUnread` is set when the step has a payout this decoder did not
|
|
// read, and `settleUnread` when it has a SETTLE or SETTLE_ALL this decoder
|
|
// did not read. `settledFromRouter` is every SETTLE that the router pays from
|
|
// its own balance (`payerIsUser` false), as its currency and the amount it
|
|
// pays: a quantity, CONTRACT_BALANCE (all the router holds), or null when
|
|
// this decoder cannot tell (see below). `paidByUser` is what the user pays
|
|
// the step, once per currency, as that currency and the amount: the total of
|
|
// every SETTLE paid by the user (`payerIsUser` true) and every SETTLE_ALL,
|
|
// which V4Router always has the user pay, of that currency, or null when
|
|
// this decoder cannot tell what one of them pays.
|
|
function decodeV4Swap(input) {
|
|
try {
|
|
const d = coder.decode(["bytes", "bytes[]"], input);
|
|
const actions = getBytes(d[0]);
|
|
const params = d[1];
|
|
|
|
let settleToken = null;
|
|
let takeToken = null;
|
|
const takes = [];
|
|
// Every SETTLE and SETTLE_ALL, as its currency, its amount as
|
|
// written, whether the user pays it, and its index among the step's
|
|
// actions. A SETTLE_ALL pays what a SETTLE of OPEN_DELTA (zero) paid
|
|
// by the user pays, all the step still owes of its currency, so it
|
|
// is kept as one; its maxAmount only caps that.
|
|
const settles = [];
|
|
// The index of the step's last swap action, read or not, or -1.
|
|
let lastSwapAction = -1;
|
|
// Set when an action of the step could not be read.
|
|
let actionUnread = false;
|
|
let recipientUnread = false;
|
|
let settleUnread = false;
|
|
// The step's swap actions that could be read, as decodeV4SwapAction()
|
|
// reads them.
|
|
const swaps = [];
|
|
// Set when a swap action's parameters are not in the deployed shape
|
|
// (see decodeExactly()), so nothing was read from that action. SETTLE,
|
|
// SETTLE_ALL, TAKE and TAKE_ALL may still name both currencies.
|
|
let swapUndecoded = false;
|
|
|
|
for (let i = 0; i < actions.length; i++) {
|
|
const actionId = actions[i];
|
|
try {
|
|
if (actionId === V4_SETTLE) {
|
|
// SETTLE: (address currency, uint256 maxAmount, bool payerIsUser)
|
|
const s = coder.decode(
|
|
["address", "uint256", "bool"],
|
|
params[i],
|
|
);
|
|
settleToken = s[0];
|
|
settles.push({
|
|
currency: s[0],
|
|
amount: s[1],
|
|
payerIsUser: s[2],
|
|
index: i,
|
|
});
|
|
} else if (actionId === V4_SETTLE_ALL) {
|
|
// SETTLE_ALL: (address currency, uint256 maxAmount)
|
|
const s = coder.decode(["address", "uint256"], params[i]);
|
|
settleToken = s[0];
|
|
settles.push({
|
|
currency: s[0],
|
|
amount: 0n,
|
|
payerIsUser: true,
|
|
index: i,
|
|
});
|
|
} else if (actionId === V4_TAKE) {
|
|
// TAKE: (address currency, address recipient, uint256 amount)
|
|
const t = coder.decode(
|
|
["address", "address", "uint256"],
|
|
params[i],
|
|
);
|
|
takeToken = t[0];
|
|
takes.push({ currency: t[0], recipient: t[1] });
|
|
} else if (actionId === V4_TAKE_ALL) {
|
|
// TAKE_ALL: (address currency, uint256 minAmount)
|
|
const t = coder.decode(["address", "uint256"], params[i]);
|
|
takeToken = t[0];
|
|
takes.push({ currency: t[0], recipient: MSG_SENDER });
|
|
} else if (actionId === V4_TAKE_PORTION) {
|
|
// TAKE_PORTION: (address currency, address recipient, uint256 bips)
|
|
const t = coder.decode(
|
|
["address", "address", "uint256"],
|
|
params[i],
|
|
);
|
|
takes.push({ currency: t[0], recipient: t[1] });
|
|
} else if (
|
|
actionId === V4_SWAP_EXACT_IN_SINGLE ||
|
|
actionId === V4_SWAP_EXACT_IN ||
|
|
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
|
actionId === V4_SWAP_EXACT_OUT
|
|
) {
|
|
lastSwapAction = i;
|
|
try {
|
|
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
|
} catch {
|
|
swapUndecoded = true;
|
|
actionUnread = true;
|
|
}
|
|
} else {
|
|
// V4Router reverts on any action not named above, so
|
|
// none of them pays anyone. This decoder reads none.
|
|
actionUnread = true;
|
|
}
|
|
} catch {
|
|
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
|
|
// TAKE_PORTION among them is a payout this decoder did not
|
|
// read, and a SETTLE or SETTLE_ALL may take from the user
|
|
// what it did not read: the router still runs both.
|
|
actionUnread = true;
|
|
if (
|
|
actionId === V4_TAKE ||
|
|
actionId === V4_TAKE_ALL ||
|
|
actionId === V4_TAKE_PORTION
|
|
) {
|
|
recipientUnread = true;
|
|
}
|
|
if (actionId === V4_SETTLE || actionId === V4_SETTLE_ALL) {
|
|
settleUnread = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// What a SETTLE or SETTLE_ALL pays. A SETTLE of any amount but
|
|
// OPEN_DELTA (zero) pays as written: a quantity, or, for
|
|
// CONTRACT_BALANCE, as much as the router holds. A SETTLE of
|
|
// OPEN_DELTA, and a SETTLE_ALL, pay whatever the step still owes of
|
|
// the currency when they run. That is what the step's swap actions
|
|
// state they spend of it only in the plain shape of Uniswap's own
|
|
// routes, so only then is it given that amount: every action in the
|
|
// step was read; every swap action names the currency it spends and
|
|
// the one it buys (an exact-in action with an empty path swaps
|
|
// nothing); it is the step's only SETTLE or SETTLE_ALL of its
|
|
// currency and comes after every swap action; no swap action buys
|
|
// that currency and nothing in the step takes it (a TAKE of a stated
|
|
// amount adds to what the step owes); and the swap actions that spend
|
|
// it state a quantity together (see totalAmount()). Otherwise it is
|
|
// given null.
|
|
const pays = (s) => {
|
|
if (s.amount !== 0n) return s.amount;
|
|
const stated = totalAmount(
|
|
swaps
|
|
.filter((w) => w.tokenIn === s.currency)
|
|
.map((w) => w.amountIn),
|
|
);
|
|
const paysWhatSwapsSpend =
|
|
!actionUnread &&
|
|
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
|
|
settles.filter((c) => c.currency === s.currency).length === 1 &&
|
|
s.index > lastSwapAction &&
|
|
!swaps.some((w) => w.tokenOut === s.currency) &&
|
|
!takes.some((t) => t.currency === s.currency) &&
|
|
typeof stated === "bigint";
|
|
return paysWhatSwapsSpend ? stated : null;
|
|
};
|
|
const settledFromRouter = settles
|
|
.filter((s) => !s.payerIsUser)
|
|
.map((s) => ({ currency: s.currency, amount: pays(s) }));
|
|
const paidByUser = [];
|
|
for (const s of settles.filter((c) => c.payerIsUser)) {
|
|
const amount = pays(s);
|
|
const paid = paidByUser.find((p) => p.currency === s.currency);
|
|
if (!paid) {
|
|
paidByUser.push({ currency: s.currency, amount });
|
|
} else {
|
|
paid.amount =
|
|
typeof paid.amount === "bigint" &&
|
|
typeof amount === "bigint"
|
|
? paid.amount + amount
|
|
: null;
|
|
}
|
|
}
|
|
|
|
// Rule 3.
|
|
if (swapUndecoded) {
|
|
return {
|
|
tokenIn: settleToken,
|
|
tokenOut: takeToken,
|
|
amountIn: null,
|
|
amountOutMin: null,
|
|
takes,
|
|
settledFromRouter,
|
|
paidByUser,
|
|
recipientUnread,
|
|
settleUnread,
|
|
swapUndecoded,
|
|
hasSwapAction: true,
|
|
};
|
|
}
|
|
|
|
const spent = swaps.map((s) => s.tokenIn);
|
|
const bought = swaps.map((s) => s.tokenOut);
|
|
|
|
// Rules 1 and 2 for Token In.
|
|
let tokenIn = null;
|
|
let amountIn = null;
|
|
const spentOnly = currenciesNotIn(spent, bought);
|
|
if (present(settleToken) || spentOnly.length === 1) {
|
|
tokenIn = present(settleToken) ? settleToken : spentOnly[0];
|
|
amountIn = totalAmount(
|
|
swaps
|
|
.filter((s) => s.tokenIn === tokenIn)
|
|
.map((s) => s.amountIn),
|
|
);
|
|
}
|
|
|
|
// Rules 1 and 2 for Token Out.
|
|
let tokenOut = null;
|
|
let amountOutMin = null;
|
|
const boughtOnly = currenciesNotIn(bought, spent);
|
|
if (present(takeToken) || boughtOnly.length === 1) {
|
|
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
|
|
amountOutMin = totalAmount(
|
|
swaps
|
|
.filter((s) => s.tokenOut === tokenOut)
|
|
.map((s) => s.amountOutMin),
|
|
);
|
|
}
|
|
|
|
return {
|
|
tokenIn,
|
|
tokenOut,
|
|
amountIn,
|
|
amountOutMin,
|
|
takes,
|
|
settledFromRouter,
|
|
paidByUser,
|
|
recipientUnread,
|
|
settleUnread,
|
|
swapUndecoded,
|
|
hasSwapAction: swaps.length > 0,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Try to decode a Universal Router execute() call sent from `fromAddress`, the
|
|
// user, to `toAddress` on the network with chain id `chainId`.
|
|
// Returns { name, description, details } matching the format used by
|
|
// the approval UI, or null if the calldata is not a recognised execute() or
|
|
// `toAddress` is not a Universal Router on that network: the same calldata
|
|
// 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 release = routerRelease(toAddress, chainId);
|
|
if (release === null) return null;
|
|
const { commandTypeMask, commands, allowRevert } = release;
|
|
try {
|
|
const parsed = ROUTER_IFACE.parseTransaction({ data });
|
|
if (!parsed) return null;
|
|
|
|
const commandsBytes = getBytes(parsed.args[0]);
|
|
const inputs = parsed.args[1];
|
|
const deadline = parsed.args[2];
|
|
|
|
let inputToken = null;
|
|
let inputAmount = null;
|
|
let inputEstablished = false;
|
|
// Whether the user pays the V4 step that set the input side any of
|
|
// its input currency (see decodeV4Swap()). Only a V4 step can set the
|
|
// input side with no amount, the one case in which this is read.
|
|
let inputPaidByUser = false;
|
|
// The last PERMIT2_PERMIT this decoder read, or null (see
|
|
// permitStands after the loop).
|
|
let permit = null;
|
|
// The index of each step this decoder did not read in full: a
|
|
// command it does not read, a step whose input it cannot read, in
|
|
// whole or in part, and one it fails on (see permitStands).
|
|
const stepsUnread = [];
|
|
let outputToken = null;
|
|
let minOutput = null;
|
|
let outputEstablished = false;
|
|
let hasUnwrapWeth = false;
|
|
let hasV2OrV3ExactOut = false;
|
|
let swapUndecoded = false;
|
|
let hasSubPlan = false;
|
|
const commandNames = [];
|
|
|
|
// THE INVARIANT: an amount and the token it is counted in always come
|
|
// from the same hop, or from hops that all spend that token. A figure
|
|
// is never rendered against a token that did not supply it.
|
|
//
|
|
// Both sides are therefore set as a PAIR — never field by field, and
|
|
// never on truthiness. An address is never falsy once set but an
|
|
// amount of 0n is, so gating the two halves independently let a hop
|
|
// with a zero amount fix the token and leave the amount open; the next
|
|
// hop's figure was then displayed against the first hop's token, at the
|
|
// first hop's scale. A V3 USDT->WETH hop with amountIn 0 followed by a
|
|
// V2 WETH->USDC hop of 0.5e18 rendered "500000000000.0000 USDT".
|
|
//
|
|
// The input side is fixed by the first hop that states either half, the
|
|
// output side by the last, because the final leg is what the user
|
|
// receives. A V4 step with a swap action sets a side even when it
|
|
// states neither half: the output side always, and the input side
|
|
// when no earlier hop set it. A half the establishing hop did not
|
|
// state stays null and the line says so, rather than being filled in
|
|
// from a different hop. A PERMIT2_PERMIT sets no side here: after the
|
|
// loop, the last one read names Token In, and the Amount line is
|
|
// formed there, in one place (see permitStands).
|
|
const setInput = (token, amount) => {
|
|
inputToken = present(token) ? token : null;
|
|
inputAmount = present(amount) ? amount : null;
|
|
inputEstablished = true;
|
|
};
|
|
const setInputOnce = (token, amount) => {
|
|
if (inputEstablished) return;
|
|
if (!present(token) && !present(amount)) return;
|
|
setInput(token, amount);
|
|
};
|
|
const setOutput = (token, amount) => {
|
|
if (!present(token) && !present(amount)) return;
|
|
outputToken = present(token) ? token : null;
|
|
minOutput = present(amount) ? amount : null;
|
|
outputEstablished = true;
|
|
};
|
|
|
|
// What the steps take from the sender beyond the amount of the step
|
|
// that sets the input side, as { token, amount }, in step order.
|
|
// After the loop each joins the Amount line or gets lines of its own
|
|
// (see permitStands).
|
|
const userPaid = [];
|
|
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
|
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes
|
|
// nothing.
|
|
const addUserPaid = (token, amount) => {
|
|
if (amount !== ALREADY_PAID) userPaid.push({ token, amount });
|
|
};
|
|
// A step may take from the sender what this decoder did not read: a
|
|
// V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it
|
|
// cannot read; a V2 or V3 swap step or PERMIT2_TRANSFER_FROM whose
|
|
// input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
|
|
// EXECUTE_SUB_PLAN; and a step it fails on. It is recorded with a null
|
|
// token and amount: one line says so, there is no Amount line, whose
|
|
// figure may then be too low, and the amount of a permit after it
|
|
// does not stand (see permitStands).
|
|
const spentUnread = () => {
|
|
userPaid.push({ token: null, amount: null });
|
|
};
|
|
|
|
// Who the steps pay, in whichever step and of whatever token.
|
|
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
|
|
// its own address, holds what it is paid until a later step pays it
|
|
// out.
|
|
const sender = (fromAddress || "").toLowerCase();
|
|
let paidToSender = false;
|
|
// Each other address paid, once.
|
|
const paidElsewhere = [];
|
|
// Set when a step may pay an address this decoder did not read.
|
|
let payeeUnread = false;
|
|
// Set when an ACROSS_V4_DEPOSIT_V3 step pays the bridge.
|
|
let bridgeDeposit = false;
|
|
// What each PERMIT2_TRANSFER_FROM moved from the sender into the
|
|
// router, as { token, amount }. Later steps may leave part of it
|
|
// there: nothing here checks that they spend all of it.
|
|
const movedIntoRouter = [];
|
|
// What each PERMIT2_TRANSFER_FROM not into the router moves from the
|
|
// sender, in step order, whoever counts as paid, as
|
|
// { token, recipient, amount, to }: `to` says where it goes, the
|
|
// sender or the V2 pair the next swap spends, and is null when it
|
|
// goes to `recipient` as written. A move into the router is in
|
|
// `movedIntoRouter`, whose lines give its amount already.
|
|
const movedFromSender = [];
|
|
// Each token a step paid to the router that no later step paid out
|
|
// or spent in full, by its address, lowercased, ETH as the zero
|
|
// address. The router is paid what a swap step, V4 TAKE or
|
|
// TAKE_PORTION pays it, the WETH of a WRAP_ETH into it, and the ETH
|
|
// of an UNWRAP_WETH into it, in place of the WETH it held. All the
|
|
// router holds of a token is paid out or spent by a SWEEP of that
|
|
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
|
|
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
|
|
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
|
|
// from the router (see spendFromRouter()). The value is the amount the
|
|
// router holds, in base units, when a WRAP_ETH stated it, otherwise
|
|
// null.
|
|
const heldByRouter = new Map();
|
|
const isRouter = (recipient) => {
|
|
const r = recipient.toLowerCase();
|
|
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
|
};
|
|
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
|
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
|
// counts against the amount a WRAP_ETH stated, and the steps pay all
|
|
// of it once their amounts reach that one: a swap paid in ETH wraps
|
|
// what its swap steps then spend. An amount that is not a quantity,
|
|
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
|
// cannot tell, or one of a token whose amount is not known, pays out
|
|
// nothing this decoder can see.
|
|
const spendFromRouter = (token, amount) => {
|
|
const t = token.toLowerCase();
|
|
const held = heldByRouter.get(t);
|
|
if (amount === CONTRACT_BALANCE) {
|
|
heldByRouter.delete(t);
|
|
} else if (typeof held === "bigint" && typeof amount === "bigint") {
|
|
if (amount >= held) {
|
|
heldByRouter.delete(t);
|
|
} else {
|
|
heldByRouter.set(t, held - amount);
|
|
}
|
|
}
|
|
};
|
|
// A step pays `recipient`, an address other than the sender's or the
|
|
// router's, as written.
|
|
const addPaidElsewhere = (recipient) => {
|
|
const r = recipient.toLowerCase();
|
|
if (!paidElsewhere.some((a) => a.toLowerCase() === r)) {
|
|
paidElsewhere.push(recipient);
|
|
}
|
|
};
|
|
// A step pays `recipient`. `output` is the token a swap step or a V4
|
|
// TAKE or TAKE_PORTION pays; other steps pass none.
|
|
const addPayee = (recipient, output) => {
|
|
const r = recipient.toLowerCase();
|
|
if (isRouter(recipient)) {
|
|
if (present(output)) {
|
|
heldByRouter.set(output.toLowerCase(), null);
|
|
}
|
|
} else if (r === MSG_SENDER || r === sender) {
|
|
paidToSender = true;
|
|
} else {
|
|
addPaidElsewhere(recipient);
|
|
}
|
|
};
|
|
// A step that pays `token` to `recipient` is a hop, not a payout,
|
|
// when the very next step is a V2 exact-in whose input is already
|
|
// paid, whose path starts at `token`, and whose pair (see v2PairFor())
|
|
// is `recipient`: Uniswap's mixed routes pay the next V2 pair
|
|
// directly, and the router enforces that next swap's own minimum.
|
|
// Its input is already paid when its amountIn is ALREADY_PAID, or
|
|
// CONTRACT_BALANCE, which Uniswap's routes send and with which the
|
|
// router adds only what it holds.
|
|
const isHop = (i, token, recipient) => {
|
|
if (i + 1 >= commandsBytes.length) return false;
|
|
const nextCommand =
|
|
commands[commandsBytes[i + 1] & commandTypeMask];
|
|
if (nextCommand !== "V2_SWAP_EXACT_IN") return false;
|
|
const next = decodeV2SwapExactIn(inputs[i + 1]);
|
|
return (
|
|
next !== null &&
|
|
(next.amountIn === ALREADY_PAID ||
|
|
next.amountIn === CONTRACT_BALANCE) &&
|
|
present(token) &&
|
|
next.path.length >= 2 &&
|
|
next.tokenIn.toLowerCase() === token.toLowerCase() &&
|
|
recipient.toLowerCase() ===
|
|
v2PairFor(chainId, next.path[0], next.path[1]).toLowerCase()
|
|
);
|
|
};
|
|
|
|
for (let i = 0; i < commandsBytes.length; i++) {
|
|
const cmdId = commandsBytes[i] & commandTypeMask;
|
|
// The command this release runs for the step (see ROUTER_V1),
|
|
// undefined for a type it names no command for.
|
|
const command = commands[cmdId];
|
|
// 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);
|
|
commandNames.push(
|
|
present(command)
|
|
? COMMAND_NAMES[command]
|
|
: "Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
|
);
|
|
|
|
try {
|
|
if (command === "EXECUTE_SUB_PLAN") {
|
|
// 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.
|
|
// Those commands may pay any address and take from the
|
|
// sender.
|
|
hasSubPlan = true;
|
|
commandNames[i] += " (its steps are not shown)";
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
} else if (command === "PERMIT2_PERMIT") {
|
|
const p = decodePermit2(inputs[i]);
|
|
if (p) {
|
|
// Whether a step before it took its token, or may
|
|
// have: the step that set the input side or one in
|
|
// `userPaid`, when its token is the permit's or not
|
|
// known.
|
|
const mayBeItsToken = (token) =>
|
|
!present(token) || sameAddress(token, p.token);
|
|
const tookBefore =
|
|
(inputEstablished && mayBeItsToken(inputToken)) ||
|
|
userPaid.some((t) => mayBeItsToken(t.token));
|
|
permit = {
|
|
...p,
|
|
step: i,
|
|
allowedToFail,
|
|
beforeEveryTake: !tookBefore,
|
|
};
|
|
} else {
|
|
stepsUnread.push(i);
|
|
}
|
|
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
|
const p = decodePermit2TransferFrom(inputs[i]);
|
|
if (!p) {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
} else if (
|
|
commandTypeMask === 0x1f &&
|
|
[MSG_SENDER, ADDRESS_THIS].includes(
|
|
p.recipient.toLowerCase(),
|
|
)
|
|
) {
|
|
// UniversalRouterV1, the router with the 0x1f mask,
|
|
// pays address(1) and address(2) as written (see
|
|
// decodePermit2TransferFrom()).
|
|
addPaidElsewhere(p.recipient);
|
|
movedFromSender.push({ ...p, to: null });
|
|
} else if (isRouter(p.recipient)) {
|
|
movedIntoRouter.push(p);
|
|
} else if (isHop(i, p.token, p.recipient)) {
|
|
movedFromSender.push({
|
|
...p,
|
|
to: "The V2 pair the next swap spends",
|
|
});
|
|
} else {
|
|
addPayee(p.recipient);
|
|
movedFromSender.push({
|
|
...p,
|
|
to:
|
|
p.recipient.toLowerCase() === MSG_SENDER
|
|
? "The sender"
|
|
: null,
|
|
});
|
|
}
|
|
} else if (command === "BALANCE_CHECK_ERC20") {
|
|
const b = decodeBalanceCheck(inputs[i]);
|
|
if (!b) {
|
|
// The router still runs a check whose input does not
|
|
// decode, such as an owner word with a nonzero top
|
|
// byte, of which it reads the last 20 bytes, so the
|
|
// owner may be any address. It sets no side, and the
|
|
// step says so.
|
|
commandNames[i] += " (could not be decoded)";
|
|
stepsUnread.push(i);
|
|
} else if (b.owner.toLowerCase() !== sender) {
|
|
// The router reads the owner as written (see
|
|
// decodeBalanceCheck()), so address(1) and address(2)
|
|
// are not the sender and the router here. A check of
|
|
// any address but the sender's says nothing about
|
|
// what the user receives: it sets no side, and the
|
|
// step says so.
|
|
commandNames[i] += " (of another address)";
|
|
} else {
|
|
// 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. 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 = allowedToFail ? 0n : b.minBalance;
|
|
if (!(minBalance === 0n && present(minOutput))) {
|
|
setOutput(b.token, minBalance);
|
|
}
|
|
}
|
|
} else if (command === "V3_SWAP_EXACT_IN") {
|
|
const s = decodeV3SwapExactIn(inputs[i]);
|
|
if (s) {
|
|
if (!inputEstablished) {
|
|
setInputOnce(s.tokenIn, s.amountIn);
|
|
} else if (s.payerIsUser) {
|
|
addUserPaid(s.tokenIn, s.amountIn);
|
|
}
|
|
// Always update output: in multi-step swaps (V3 → V4),
|
|
// the last swap step determines the final output token
|
|
// and minimum received amount.
|
|
setOutput(s.tokenOut, s.amountOutMin);
|
|
if (!s.payerIsUser) {
|
|
spendFromRouter(s.tokenIn, s.amountIn);
|
|
}
|
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
|
addPayee(s.recipient, s.tokenOut);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
}
|
|
} else if (command === "V2_SWAP_EXACT_IN") {
|
|
const s = decodeV2SwapExactIn(inputs[i]);
|
|
if (s) {
|
|
if (!inputEstablished) {
|
|
setInputOnce(s.tokenIn, s.amountIn);
|
|
} else if (s.payerIsUser) {
|
|
addUserPaid(s.tokenIn, s.amountIn);
|
|
}
|
|
setOutput(s.tokenOut, s.amountOutMin);
|
|
if (!s.payerIsUser) {
|
|
spendFromRouter(s.tokenIn, s.amountIn);
|
|
}
|
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
|
addPayee(s.recipient, s.tokenOut);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
}
|
|
} else if (
|
|
command === "V2_SWAP_EXACT_OUT" ||
|
|
command === "V3_SWAP_EXACT_OUT"
|
|
) {
|
|
// Buys exactly amountOut and spends at most amountInMax.
|
|
// Paid from the router, it spends an amount the calldata
|
|
// does not state, so it spends nothing spendFromRouter()
|
|
// could count.
|
|
hasV2OrV3ExactOut = true;
|
|
const s =
|
|
command === "V2_SWAP_EXACT_OUT"
|
|
? decodeV2SwapExactOut(inputs[i])
|
|
: decodeV3SwapExactOut(inputs[i]);
|
|
if (s) {
|
|
if (!inputEstablished) {
|
|
setInputOnce(s.tokenIn, s.amountInMax);
|
|
} else if (s.payerIsUser) {
|
|
addUserPaid(s.tokenIn, s.amountInMax);
|
|
}
|
|
setOutput(s.tokenOut, s.amountOut);
|
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
|
addPayee(s.recipient, s.tokenOut);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
}
|
|
} else if (command === "PERMIT2_TRANSFER_FROM_BATCH") {
|
|
// It moves the sender's tokens to any addresses, and this
|
|
// decoder does not read it.
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
} else if (command === "SWEEP") {
|
|
const s = decodeSweep(inputs[i]);
|
|
if (s) {
|
|
addPayee(s.recipient);
|
|
if (!isRouter(s.recipient)) {
|
|
heldByRouter.delete(s.token.toLowerCase());
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} else if (
|
|
command === "TRANSFER" ||
|
|
command === "PAY_PORTION" ||
|
|
command === "PAY_PORTION_FULL_PRECISION"
|
|
) {
|
|
const t = decodeTransfer(inputs[i]);
|
|
if (t) {
|
|
if (!isHop(i, t.token, t.recipient)) {
|
|
addPayee(t.recipient);
|
|
}
|
|
// The amount with which TRANSFER, PAY_PORTION or
|
|
// PAY_PORTION_FULL_PRECISION pays all the router
|
|
// holds of the token, as SWEEP does.
|
|
const all =
|
|
command === "TRANSFER"
|
|
? CONTRACT_BALANCE
|
|
: command === "PAY_PORTION"
|
|
? 10000n
|
|
: 10n ** 18n;
|
|
// A TRANSFER of ETH has no such amount: the router
|
|
// sends ETH as written, so a TRANSFER of
|
|
// CONTRACT_BALANCE of ETH always reverts.
|
|
const ethTransfer =
|
|
command === "TRANSFER" &&
|
|
t.token ===
|
|
"0x0000000000000000000000000000000000000000";
|
|
if (
|
|
t.amount === all &&
|
|
!ethTransfer &&
|
|
!isRouter(t.recipient)
|
|
) {
|
|
heldByRouter.delete(t.token.toLowerCase());
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} else if (command === "ACROSS_V4_DEPOSIT_V3") {
|
|
const a = decodeAcrossV4DepositV3(inputs[i]);
|
|
if (a) {
|
|
bridgeDeposit = true;
|
|
// The bridge pays or refunds these as written, so
|
|
// only the sender's own address is the sender.
|
|
for (const r of [a.recipient, a.depositor]) {
|
|
if (r.toLowerCase() !== sender) {
|
|
addPaidElsewhere(r);
|
|
}
|
|
}
|
|
// A deposit of CONTRACT_BALANCE pays all the router
|
|
// holds of its token, or of ETH with useNative set.
|
|
if (a.inputAmount === CONTRACT_BALANCE) {
|
|
heldByRouter.delete(
|
|
a.useNative
|
|
? "0x0000000000000000000000000000000000000000"
|
|
: a.inputToken.toLowerCase(),
|
|
);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} else if (command === "WRAP_ETH") {
|
|
const w = decodeWrapEth(inputs[i]);
|
|
if (w) {
|
|
setInputOnce(
|
|
"0x0000000000000000000000000000000000000000",
|
|
w.amount,
|
|
);
|
|
addPayee(w.recipient);
|
|
// CONTRACT_BALANCE wraps all the router's ETH.
|
|
if (w.amount === CONTRACT_BALANCE) {
|
|
heldByRouter.delete(
|
|
"0x0000000000000000000000000000000000000000",
|
|
);
|
|
}
|
|
// Into the router, the router holds the WETH. Its
|
|
// amount is known when the step states it and the
|
|
// router held no WETH before.
|
|
if (isRouter(w.recipient) && w.amount > 0n) {
|
|
const weth = WETH.get(chainId);
|
|
heldByRouter.set(
|
|
weth,
|
|
w.amount === CONTRACT_BALANCE ||
|
|
heldByRouter.has(weth)
|
|
? null
|
|
: w.amount,
|
|
);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} else if (command === "V4_SWAP") {
|
|
const v4 = decodeV4Swap(inputs[i]);
|
|
const inputSetBefore = inputEstablished;
|
|
if (v4) {
|
|
// Each SETTLE paid from the router spends the amount
|
|
// decodeV4Swap() says it pays.
|
|
for (const s of v4.settledFromRouter) {
|
|
spendFromRouter(s.currency, s.amount);
|
|
}
|
|
for (const t of v4.takes) {
|
|
if (!isHop(i, t.currency, t.recipient)) {
|
|
addPayee(t.recipient, t.currency);
|
|
}
|
|
}
|
|
}
|
|
if (!v4 || v4.recipientUnread) {
|
|
payeeUnread = true;
|
|
}
|
|
if (
|
|
!v4 ||
|
|
v4.recipientUnread ||
|
|
v4.settleUnread ||
|
|
v4.swapUndecoded
|
|
) {
|
|
stepsUnread.push(i);
|
|
}
|
|
if (v4 && v4.swapUndecoded) {
|
|
// A swap action of this step went unread, so the step
|
|
// is marked.
|
|
swapUndecoded = true;
|
|
commandNames[i] += " (could not be decoded)";
|
|
}
|
|
// The step's amount, if it sets the input side: what the
|
|
// user pays it of its input currency (see
|
|
// decodeV4Swap()), or, when the user pays none of it,
|
|
// what its swap actions spend of it, whoever pays that.
|
|
const paidIn = v4?.paidByUser.find(
|
|
(p) => p.currency === v4.tokenIn,
|
|
);
|
|
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
|
if (v4 && v4.hasSwapAction) {
|
|
// A step with a swap action always sets the output
|
|
// side, as the last swap step wins, and sets the
|
|
// input side only when no earlier step set it. Each
|
|
// side it sets is set as a pair, even when the step
|
|
// states neither half, so a side decodeV4Swap() left
|
|
// unknown is set unknown with no figure. A token
|
|
// stated without a figure, or a figure without a
|
|
// token, likewise never sits beside an earlier step's
|
|
// other half.
|
|
if (!inputEstablished) {
|
|
setInput(v4.tokenIn, amountIn);
|
|
}
|
|
outputToken = v4.tokenOut;
|
|
minOutput = v4.amountOutMin;
|
|
outputEstablished = true;
|
|
} else if (v4) {
|
|
// A step with no swap action sets only the sides its
|
|
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
|
// setInputOnce() and setOutput() leave alone a side
|
|
// whose two halves are both unstated.
|
|
setInputOnce(v4.tokenIn, amountIn);
|
|
setOutput(v4.tokenOut, v4.amountOutMin);
|
|
}
|
|
// Every V4 step, with a swap action or without, takes
|
|
// from the sender what the user pays it (an amount an
|
|
// unread action may change is null), less what the step
|
|
// that sets the input side counts as its amount. One
|
|
// whose input, or one of whose SETTLEs or SETTLE_ALLs,
|
|
// this decoder could not read may take anything: the
|
|
// router still runs it.
|
|
const setInputHere = inputEstablished && !inputSetBefore;
|
|
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
|
if (v4) {
|
|
for (const p of v4.paidByUser) {
|
|
if (setInputHere && p === paidIn) continue;
|
|
addUserPaid(p.currency, p.amount);
|
|
}
|
|
}
|
|
if (!v4 || v4.settleUnread) {
|
|
spentUnread();
|
|
}
|
|
} else if (command === "UNWRAP_WETH") {
|
|
hasUnwrapWeth = true;
|
|
const u = decodeUnwrapWeth(inputs[i]);
|
|
if (u) {
|
|
addPayee(u.recipient);
|
|
// It pays out all the router's WETH, as ETH. Into the
|
|
// router, the router then holds as ETH the WETH a
|
|
// step paid it.
|
|
const heldWeth = WETH_ADDRESSES.some((w) =>
|
|
heldByRouter.has(w),
|
|
);
|
|
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
|
if (isRouter(u.recipient) && heldWeth) {
|
|
heldByRouter.set(
|
|
"0x0000000000000000000000000000000000000000",
|
|
null,
|
|
);
|
|
}
|
|
} else {
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} else {
|
|
// Any other step may pay an address this decoder does not
|
|
// read: PERMIT2_PERMIT_BATCH and the NFT commands among
|
|
// them, and a type the release names no command for.
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
}
|
|
} catch {
|
|
// Skip commands we can't decode. One may pay any address and
|
|
// take from the sender.
|
|
payeeUnread = true;
|
|
stepsUnread.push(i);
|
|
spentUnread();
|
|
}
|
|
if (allowedToFail) {
|
|
commandNames[i] += " (allowed to fail)";
|
|
}
|
|
}
|
|
|
|
// The Amount line, and the lines for what the steps take beyond it.
|
|
//
|
|
// The total is what the steps take of Token In: the amount of the
|
|
// step that set the input side, with each later take of that token
|
|
// added to it while both are quantities. A take that cannot be added,
|
|
// being of another token or coming after an amount that is not a
|
|
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
|
|
// lines of its own. One whose amount is not known also leaves the
|
|
// total unknown, with no figure.
|
|
//
|
|
// The last PERMIT2_PERMIT read names Token In. Its amount stands as
|
|
// the Amount figure in place of the total only in the plain shape of
|
|
// Uniswap's own routes: it is the only PERMIT2_PERMIT, its spender is
|
|
// the router, the router may not let it fail, no step before it took
|
|
// its token or one not known, and this decoder read every step after
|
|
// it in full (see stepsUnread). Permit2 then lets the router take no
|
|
// more of the token than that amount, so the permit covers every take
|
|
// of it. A later step this decoder did not read may raise what Permit2
|
|
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step that
|
|
// may take from the sender what this decoder did not read leaves no
|
|
// figure, permit or not (see spentUnread()), and an EXECUTE_SUB_PLAN
|
|
// step leaves both sides unknown (below).
|
|
const stepCommands = Array.from(
|
|
commandsBytes,
|
|
(b) => commands[b & commandTypeMask],
|
|
);
|
|
const permitStands =
|
|
permit !== null &&
|
|
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
|
|
sameAddress(permit.spender, toAddress) &&
|
|
!permit.allowedToFail &&
|
|
permit.beforeEveryTake &&
|
|
!stepsUnread.some((step) => step > permit.step);
|
|
// What the steps take that the Amount line does not give, as
|
|
// { token, amount }. A null token is a step this decoder did not read
|
|
// (see spentUnread()), or a token it does not know.
|
|
const spentFromSender = [];
|
|
let total = null;
|
|
// Whether the step that set the input side counts as a take from the
|
|
// sender, which may get lines of its own. The amount it stated counts
|
|
// whoever pays it. Stating none, it counts only when the user pays it
|
|
// some of its input token: paid from the router, it takes nothing
|
|
// from the sender.
|
|
const hasFirstTake =
|
|
inputEstablished && (present(inputAmount) || inputPaidByUser);
|
|
// The take of the step that set the input side, when the total
|
|
// starts from it and it stated no amount (see below), or null.
|
|
let firstTakeUnstated = null;
|
|
if (permit === null || sameAddress(inputToken, permit.token)) {
|
|
total = inputAmount;
|
|
if (hasFirstTake && !present(inputAmount)) {
|
|
firstTakeUnstated = { token: inputToken, amount: null };
|
|
}
|
|
} else if (hasFirstTake) {
|
|
// The step that set the input side spent a token other than the
|
|
// permit's, or one not known, so the total starts with no figure.
|
|
spentFromSender.push({ token: inputToken, amount: inputAmount });
|
|
}
|
|
if (permit !== null) inputToken = permit.token;
|
|
for (const t of userPaid) {
|
|
const ofTokenIn = sameAddress(t.token, inputToken);
|
|
if (ofTokenIn && permitStands) continue;
|
|
if (
|
|
ofTokenIn &&
|
|
typeof total === "bigint" &&
|
|
typeof t.amount === "bigint"
|
|
) {
|
|
total += t.amount;
|
|
continue;
|
|
}
|
|
spentFromSender.push(t);
|
|
if (ofTokenIn && !present(t.amount)) total = null;
|
|
}
|
|
inputAmount = permitStands ? permit.amount : total;
|
|
// With no Amount figure, a later take's own figure, or that of a
|
|
// PERMIT2_TRANSFER_FROM of the same token, into the router or not,
|
|
// could read as all that leaves, so the step that set the input side,
|
|
// when the user pays it and it stated no amount, then gets lines of
|
|
// its own too, first, saying its amount is not known.
|
|
if (
|
|
!present(inputAmount) &&
|
|
firstTakeUnstated !== null &&
|
|
(spentFromSender.some((s) => present(s.amount)) ||
|
|
[...movedFromSender, ...movedIntoRouter].some((m) =>
|
|
sameAddress(m.token, firstTakeUnstated.token),
|
|
))
|
|
) {
|
|
spentFromSender.unshift(firstTakeUnstated);
|
|
}
|
|
|
|
// 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 name Token In. 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, 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());
|
|
const inInfo = tokenInfo(inputToken, sources);
|
|
const outInfo =
|
|
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
|
? { symbol: "ETH", decimals: 18, address: null }
|
|
: tokenInfo(outputToken, sources);
|
|
|
|
const inSymbol = inInfo.symbol;
|
|
const outSymbol = outInfo.symbol;
|
|
|
|
const name =
|
|
inSymbol && outSymbol
|
|
? "Swap " + inSymbol + " \u2192 " + outSymbol
|
|
: "Uniswap Swap";
|
|
|
|
const details = [];
|
|
|
|
details.push({
|
|
label: "Protocol",
|
|
value: "Uniswap Universal Router",
|
|
address: toAddress,
|
|
});
|
|
|
|
if (present(inputToken) && present(inInfo.address)) {
|
|
const label = inSymbol
|
|
? inSymbol + " (" + inputToken + ")"
|
|
: inputToken;
|
|
details.push({
|
|
label: "Token In",
|
|
value: label,
|
|
address: inputToken,
|
|
isToken: true,
|
|
});
|
|
} else if (inSymbol === "ETH") {
|
|
details.push({ label: "Token In", value: "ETH (native)" });
|
|
} else {
|
|
// Nothing established the input token, so the line says that
|
|
// rather than going missing or naming a token by default. Same
|
|
// wording as the Token Out refusal below: the two sides of this
|
|
// screen must not describe the same condition in two ways.
|
|
details.push({
|
|
label: "Token In",
|
|
value:
|
|
swapUndecoded || hasSubPlan
|
|
? UNDECODED_CURRENCY
|
|
: UNNAMED_CURRENCY,
|
|
});
|
|
}
|
|
|
|
// The text of an amount the swap steps spend, for the Amount line and
|
|
// the lines for other tokens below it. Three amounts need no scale to
|
|
// describe and are named rather than formatted: V4's open delta and
|
|
// V2's already-paid zero, neither of which is a quantity at all (see
|
|
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
|
// tests come first — the sentinels are not bigints and cannot be
|
|
// compared with one.
|
|
const spentText = (amount, info) => {
|
|
if (amount === OPEN_DELTA) {
|
|
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
|
}
|
|
if (amount === ALREADY_PAID) {
|
|
return {
|
|
raw: ALREADY_PAID_AMOUNT,
|
|
display: ALREADY_PAID_AMOUNT,
|
|
};
|
|
}
|
|
if (amount >= MAX_UINT160) {
|
|
return { raw: "Unlimited", display: "Unlimited" };
|
|
}
|
|
if (hasV2OrV3ExactOut) {
|
|
// A V2 or V3 exact-out swap spends at most this figure,
|
|
// whichever steps gave it (an amountInMax, the WRAP_ETH of a
|
|
// swap paid in ETH, a permit, or several steps' total), so it
|
|
// is said to be a maximum, in `raw` too: the wait, success and
|
|
// error screens show `raw` as the transaction's amount.
|
|
const most = amountText(amount, info);
|
|
return {
|
|
raw: "Up to " + most.raw,
|
|
display: "Up to " + most.display,
|
|
};
|
|
}
|
|
return amountText(amount, info);
|
|
};
|
|
|
|
// A null token in `spentFromSender`, a step this decoder did not read
|
|
// or a token it does not know, leaves no Amount figure, which may
|
|
// then be too low.
|
|
if (
|
|
present(inputAmount) &&
|
|
spentFromSender.every((s) => present(s.token))
|
|
) {
|
|
const amount = spentText(inputAmount, inInfo);
|
|
details.push({
|
|
label: "Amount",
|
|
value: amount.display,
|
|
rawValue: amount.raw,
|
|
});
|
|
}
|
|
|
|
// What steps take from the sender beyond the Amount line, in the shape
|
|
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
|
// decoder does not know is said so, and a step it did not read gets
|
|
// that one line alone.
|
|
for (const s of spentFromSender) {
|
|
details.push({
|
|
label: "Moved from the sender",
|
|
value: present(s.amount)
|
|
? spentText(s.amount, tokenInfo(s.token, sources)).display
|
|
: UNDECODED_CURRENCY,
|
|
});
|
|
if (present(s.token)) {
|
|
details.push(tokenMovedLine("Token moved", s.token, sources));
|
|
details.push({
|
|
label: "Moved to",
|
|
value: "The swap step that spends it",
|
|
});
|
|
}
|
|
}
|
|
|
|
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
|
|
// where it goes, an address on a line of its own.
|
|
for (const m of movedFromSender) {
|
|
details.push({
|
|
label: "Moved from the sender",
|
|
value: amountText(m.amount, tokenInfo(m.token, sources))
|
|
.display,
|
|
});
|
|
details.push(tokenMovedLine("Token moved", m.token, sources));
|
|
details.push(
|
|
present(m.to)
|
|
? { label: "Moved to", value: m.to }
|
|
: {
|
|
label: "Moved to",
|
|
value: m.recipient,
|
|
address: m.recipient,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Keyed on the address, not the symbol: a token absent from the
|
|
// bundled list has no symbol, and gating the line on one dropped it
|
|
// entirely, leaving a Min. received figure with nothing saying what is
|
|
// being received. The Token In line above already falls back to the
|
|
// address; this does the same.
|
|
if (present(outInfo.address)) {
|
|
const label = outSymbol
|
|
? outSymbol + " (" + outInfo.address + ")"
|
|
: outInfo.address;
|
|
details.push({
|
|
label: "Token Out",
|
|
value: label,
|
|
address: outInfo.address,
|
|
isToken: true,
|
|
});
|
|
} else if (outSymbol) {
|
|
details.push({ label: "Token Out", value: outSymbol });
|
|
} else {
|
|
// Nothing established the output token, so the line says that
|
|
// rather than going missing or naming a token by default, and a
|
|
// Min. received figure below it is never attached to a token the
|
|
// calldata did not state.
|
|
details.push({
|
|
label: "Token Out",
|
|
value:
|
|
swapUndecoded || hasSubPlan
|
|
? UNDECODED_CURRENCY
|
|
: UNNAMED_CURRENCY,
|
|
});
|
|
}
|
|
|
|
// A Paid to line names each other address paid, and says when a
|
|
// step pays the bridge, when one moves the sender's tokens into the
|
|
// router, when a token stays in the router, and when a payout could
|
|
// not be read. The output is received by the sender only when some
|
|
// step pays the sender and there is no such line. When no step names
|
|
// anyone paid, one line says so. `paidTo` holds these lines and the
|
|
// token line after each line about a token in the router.
|
|
const outputHeld =
|
|
present(outputToken) && heldByRouter.has(outputToken.toLowerCase());
|
|
const paidTo = paidElsewhere.map((address) => ({
|
|
label: "Paid to",
|
|
value: address,
|
|
address,
|
|
}));
|
|
if (bridgeDeposit) {
|
|
paidTo.push({ label: "Paid to", value: PAID_TO_BRIDGE });
|
|
}
|
|
for (const m of movedIntoRouter) {
|
|
const info = tokenInfo(m.token, sources);
|
|
paidTo.push({
|
|
label: "Paid to",
|
|
value:
|
|
"The router, " +
|
|
amountText(m.amount, info).display +
|
|
" from the sender (later steps may not spend all of it)",
|
|
});
|
|
paidTo.push(
|
|
tokenMovedLine("Token moved into the router", m.token, sources),
|
|
);
|
|
}
|
|
if (outputHeld) {
|
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
|
}
|
|
// Any other token the router still holds after the last step, such
|
|
// as one a swap step bought on the way to the output, what a V4
|
|
// SETTLE took beyond what the step's swap spent, or the WETH of a
|
|
// WRAP_ETH, stays there too, and its line is followed by the token's.
|
|
for (const token of heldByRouter.keys()) {
|
|
if (present(outputToken) && token === outputToken.toLowerCase()) {
|
|
continue;
|
|
}
|
|
paidTo.push({ label: "Paid to", value: STAYS_IN_ROUTER });
|
|
paidTo.push(
|
|
tokenMovedLine(
|
|
"Token moved into the router",
|
|
getAddress(token),
|
|
sources,
|
|
),
|
|
);
|
|
}
|
|
if (payeeUnread) {
|
|
paidTo.push({ label: "Paid to", value: UNDECODED_CURRENCY });
|
|
}
|
|
const received = paidToSender && paidTo.length === 0;
|
|
if (!paidToSender && paidTo.length === 0) {
|
|
paidTo.push({ label: "Paid to", value: UNNAMED_CURRENCY });
|
|
}
|
|
details.push(...paidTo);
|
|
|
|
if (present(minOutput)) {
|
|
// A zero floor is the one case the user most needs stated: the
|
|
// swap guarantees nothing back. It is said in words, in the same
|
|
// register as UNNAMED_CURRENCY, because "0.0000 WETH" reads as an
|
|
// artifact of the four-decimal rule rather than as "this may
|
|
// return nothing" — and because it is true at every scale, so it
|
|
// holds even when the output token's decimals are unknown.
|
|
details.push({
|
|
label: received ? "Min. received" : "Min. paid out",
|
|
value:
|
|
minOutput === 0n
|
|
? NO_MINIMUM
|
|
: amountText(minOutput, outInfo).display,
|
|
});
|
|
}
|
|
|
|
details.push({ label: "Steps", value: commandNames.join(" \u2192 ") });
|
|
|
|
// A JavaScript date reaches only to 275760-09-13 00:00:00 UTC. A
|
|
// later deadline, such as the uint256 maximum, makes an invalid date,
|
|
// and toISOString() throws on one, so that deadline is said in words.
|
|
const deadlineDate = new Date(Number(deadline) * 1000);
|
|
details.push({
|
|
label: "Deadline",
|
|
value: isNaN(deadlineDate.getTime())
|
|
? "After 275760-09-13 00:00:00 (no deadline in practice)"
|
|
: deadlineDate.toISOString().replace("T", " ").slice(0, 19),
|
|
});
|
|
|
|
return {
|
|
name,
|
|
description: "Swap via Uniswap Universal Router",
|
|
details,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = { decode };
|