Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7739547b3 |
@@ -2934,17 +2934,20 @@ Currently supported:
|
|||||||
address, is not counted as paid out. When a swap step or `TAKE` pays the
|
address, is not counted as paid out. When a swap step or `TAKE` pays the
|
||||||
router the swap's output token, the output stays in the router unless a later
|
router the swap's output token, the output stays in the router unless a later
|
||||||
`SWEEP` of that token, or a later `UNWRAP_WETH` when it is WETH, pays it out.
|
`SWEEP` of that token, or a later `UNWRAP_WETH` when it is WETH, pays it out.
|
||||||
A swap step paying an address that the very next step spends, as a V2 exact-in
|
A swap step or `TAKE` paying a token to the V2 pair that the very next step
|
||||||
of the same token whose `amountIn` is `ALREADY_PAID` (zero) or
|
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
|
||||||
`CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
|
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
|
||||||
next V2 pair directly, and the router enforces that swap's own minimum. Every
|
pay the next V2 pair directly, and the router enforces that swap's own
|
||||||
other payment counts, in whichever step and of whatever token. The minimum
|
minimum. The decoder checks that the address paid is that pair, deriving it as
|
||||||
reads `Min. received` only when some step pays the transaction's sender (the
|
the router does from the V2 factory and pair init code hash the network's
|
||||||
From address on TxApproval, the sender the explorer reports on
|
routers were deployed with and the first two tokens of that step's path; a
|
||||||
TransactionDetail), no step pays any other address, no step that may pay an
|
payment to any other address is named. Every other payment counts, in
|
||||||
address went unread, and the output does not stay in the router. Otherwise it
|
whichever step and of whatever token. The minimum reads `Min. received` only
|
||||||
reads `Min. paid out`, a `Paid to` line after `Token Out` names each other
|
when some step pays the transaction's sender (the From address on TxApproval,
|
||||||
address paid, one reads
|
the sender the explorer reports on TransactionDetail), no step pays any other
|
||||||
|
address, no step that may pay an address went unread, and the output does not
|
||||||
|
stay in the router. Otherwise it reads `Min. paid out`, a `Paid to` line after
|
||||||
|
`Token Out` names each other address paid, one reads
|
||||||
`The router, where it stays (no later step pays it out)` when the output stays
|
`The router, where it stays (no later step pays it out)` when the output stays
|
||||||
in the router, and one more reads `Unknown (could not be decoded)` when such a
|
in the router, and one more reads `Unknown (could not be decoded)` when such a
|
||||||
step went unread, or `Unknown (not named in the calldata)` when no step pays
|
step went unread, or `Unknown (not named in the calldata)` when no step pays
|
||||||
|
|||||||
+72
-13
@@ -1,7 +1,15 @@
|
|||||||
// Decode Uniswap Universal Router execute() calldata into human-readable
|
// Decode Uniswap Universal Router execute() calldata into human-readable
|
||||||
// swap details. Designed to be extended with other DEX decoders later.
|
// swap details. Designed to be extended with other DEX decoders later.
|
||||||
|
|
||||||
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
const {
|
||||||
|
Interface,
|
||||||
|
AbiCoder,
|
||||||
|
getBytes,
|
||||||
|
formatUnits,
|
||||||
|
getCreate2Address,
|
||||||
|
keccak256,
|
||||||
|
solidityPacked,
|
||||||
|
} = require("ethers");
|
||||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||||
const {
|
const {
|
||||||
resolveTokenDecimals,
|
resolveTokenDecimals,
|
||||||
@@ -65,6 +73,53 @@ function routerCommandTypeMask(address, chainId) {
|
|||||||
return router ? router[1] : null;
|
return router ? router[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// whose init code hash is zero. On Sepolia neither hash is that of the
|
||||||
|
// factory's own pairs, so no contract sits at a pair any router there finds,
|
||||||
|
// and 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Universal Router command types, each read from its command byte with the
|
// Universal Router command types, each read from its command byte with the
|
||||||
// router's mask (see ROUTER_ADDRESSES)
|
// router's mask (see ROUTER_ADDRESSES)
|
||||||
const COMMAND_NAMES = {
|
const COMMAND_NAMES = {
|
||||||
@@ -295,6 +350,7 @@ function decodeV2SwapExactIn(input) {
|
|||||||
recipient: d[0],
|
recipient: d[0],
|
||||||
amountIn: d[1] === 0n ? ALREADY_PAID : d[1],
|
amountIn: d[1] === 0n ? ALREADY_PAID : d[1],
|
||||||
amountOutMin: d[2],
|
amountOutMin: d[2],
|
||||||
|
path: d[3],
|
||||||
tokenIn: d[3][0],
|
tokenIn: d[3][0],
|
||||||
tokenOut: d[3][d[3].length - 1],
|
tokenOut: d[3][d[3].length - 1],
|
||||||
};
|
};
|
||||||
@@ -786,14 +842,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
paidElsewhere.push(recipient);
|
paidElsewhere.push(recipient);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// A swap step that pays `token` to an address the very next step
|
// A swap step or V4 TAKE that pays `token` to `recipient` is a hop,
|
||||||
// spends, as a V2 exact-in of that token whose input is already paid,
|
// not a payout, when the very next step is a V2 exact-in whose input
|
||||||
// is a hop, not a payout: Uniswap's mixed routes pay the next V2 pair
|
// is already paid, whose path starts at `token`, and whose pair (see
|
||||||
// directly, and the router enforces that next swap's own minimum. Its
|
// v2PairFor()) is `recipient`: Uniswap's mixed routes pay the next V2
|
||||||
// input is already paid when its amountIn is ALREADY_PAID, or
|
// 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
|
// CONTRACT_BALANCE, which Uniswap's routes send and with which the
|
||||||
// router adds only what it holds.
|
// router adds only what it holds.
|
||||||
const isHop = (i, token) => {
|
const isHop = (i, token, recipient) => {
|
||||||
if (i + 1 >= commandsBytes.length) return false;
|
if (i + 1 >= commandsBytes.length) return false;
|
||||||
if ((commandsBytes[i + 1] & commandTypeMask) !== 0x08) return false;
|
if ((commandsBytes[i + 1] & commandTypeMask) !== 0x08) return false;
|
||||||
const next = decodeV2SwapExactIn(inputs[i + 1]);
|
const next = decodeV2SwapExactIn(inputs[i + 1]);
|
||||||
@@ -802,8 +859,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
(next.amountIn === ALREADY_PAID ||
|
(next.amountIn === ALREADY_PAID ||
|
||||||
next.amountIn === CONTRACT_BALANCE) &&
|
next.amountIn === CONTRACT_BALANCE) &&
|
||||||
present(token) &&
|
present(token) &&
|
||||||
present(next.tokenIn) &&
|
next.path.length >= 2 &&
|
||||||
next.tokenIn.toLowerCase() === token.toLowerCase()
|
next.tokenIn.toLowerCase() === token.toLowerCase() &&
|
||||||
|
recipient.toLowerCase() ===
|
||||||
|
v2PairFor(chainId, next.path[0], next.path[1]).toLowerCase()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -847,7 +906,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// the last swap step determines the final output token
|
// the last swap step determines the final output token
|
||||||
// and minimum received amount.
|
// and minimum received amount.
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
if (!isHop(i, s.tokenOut)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -858,7 +917,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountIn);
|
setInputOnce(s.tokenIn, s.amountIn);
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
if (!isHop(i, s.tokenOut)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -871,7 +930,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountInMax);
|
setInputOnce(s.tokenIn, s.amountInMax);
|
||||||
setOutput(s.tokenOut, s.amountOut);
|
setOutput(s.tokenOut, s.amountOut);
|
||||||
if (!isHop(i, s.tokenOut)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -902,7 +961,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
const v4 = decodeV4Swap(inputs[i]);
|
const v4 = decodeV4Swap(inputs[i]);
|
||||||
if (v4) {
|
if (v4) {
|
||||||
for (const t of v4.takes) {
|
for (const t of v4.takes) {
|
||||||
if (!isHop(i, t.currency)) {
|
if (!isHop(i, t.currency, t.recipient)) {
|
||||||
addPayee(t.recipient, t.currency);
|
addPayee(t.recipient, t.currency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-17
@@ -14,27 +14,25 @@ const DAI_ADDR = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
|||||||
const SEPOLIA_WETH_ADDR = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
const SEPOLIA_WETH_ADDR = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||||
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
// AutistMask's first-ever swap, 2026-02-27.
|
// AutistMask's first-ever swap, 2026-02-27, sent from USER_ADDR.
|
||||||
// Swapped USDT for ETH via Uniswap V4 Universal Router.
|
// Swapped USDT for ETH via Uniswap V4 Universal Router.
|
||||||
// https://etherscan.io/tx/0x6749f50c4e8f975b6d14780d5f539cf151d1594796ac49b7d6a5348ba0735e77
|
// https://etherscan.io/tx/0x6749f50c4e8f975b6d14780d5f539cf151d1594796ac49b7d6a5348ba0735e77
|
||||||
|
// This is that transaction's input, as a public Ethereum node returns it.
|
||||||
const FIRST_SWAP_CALLDATA =
|
const FIRST_SWAP_CALLDATA =
|
||||||
"0x3593564c" +
|
"0x3593564c" +
|
||||||
"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0" +
|
"000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000069a1550f0000000000000000000000000000000000000000000000000000000000000002" +
|
||||||
"0000000000000000000000000000000000000000000000000000000069a1550f00000000000000000000000000000000000000000000000000000000000000020a10000000000000000000000000000000000000000000000000000000000000" +
|
"0a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c0" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c0" +
|
"0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000ffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000069c8daf6" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000160000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000ffffffffffffffffffffffffffffffffffffffff" +
|
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af0000000000000000000000000000000000000000000000000000000069a154fe00000000000000000000000000000000000000000000000000000000000000e0" +
|
||||||
"0000000000000000000000000000000000000000000000000000000069c8daf6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066a9893cc07d91d95644aedd05d03f95e1dba8af" +
|
"0000000000000000000000000000000000000000000000000000000000000041230249bb7133205db7b2389b587c723cc182302907b9545dc40c59c33ad1d53078a65732f4182fedbc0d9d85c51d580bdc93db3556fac38f18e140da47d0eb631c00000000000000000000000000000000000000000000000000000000000000" +
|
||||||
"0000000000000000000000000000000000000000000000000000000069a154fe00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000041" +
|
|
||||||
"230249bb7133205db7b2389b587c723cc182302907b9545dc40c59c33ad1d53078a65732f4182fedbc0d9d85c51d580bdc93db3556fac38f18e140da47d0eb631c00000000000000000000000000000000000000000000000000000000000000" +
|
|
||||||
"00000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003" +
|
"00000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003" +
|
||||||
"070b0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220" +
|
"070b0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220" +
|
||||||
"00000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7" +
|
"00000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000007a1200000000000000000000000000000000000000000000000000000dcb050d338e7" +
|
"0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000007a1200000000000000000000000000000000000000000000000000000dcb050d338e70000000000000000000000000000000000000000000000000000000000000001" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064" +
|
"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000001" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0" +
|
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060" +
|
||||||
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
|
"000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060" +
|
||||||
"dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
|
"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066133e8ea0f5d1d612d2502a968757d1048c214a0000000000000000000000000000000000000000000000000000000000000000756e6978000000000012";
|
||||||
"66133e8ea0f5d1d612d2502a968757d1048c214a0000000000000000000000000000000000000000000000000000000000000000756e6978000000000012";
|
|
||||||
|
|
||||||
const coder = AbiCoder.defaultAbiCoder();
|
const coder = AbiCoder.defaultAbiCoder();
|
||||||
const routerIface = new Interface([
|
const routerIface = new Interface([
|
||||||
@@ -362,11 +360,11 @@ describe("uniswap decoder", () => {
|
|||||||
expect(result.details.find((d) => d.label === "Amount").value).toBe(
|
expect(result.details.find((d) => d.label === "Amount").value).toBe(
|
||||||
"Unlimited",
|
"Unlimited",
|
||||||
);
|
);
|
||||||
// This fixture's TAKE parameters are one byte long, so who it pays
|
// Its TAKE pays the From address, so the minimum is received.
|
||||||
// cannot be read, and the minimum is not said to be received.
|
|
||||||
expect(
|
expect(
|
||||||
result.details.find((d) => d.label === "Min. paid out").value,
|
result.details.find((d) => d.label === "Min. received").value,
|
||||||
).toBe("0.0002 ETH");
|
).toBe("0.0002 ETH");
|
||||||
|
expect(labels).not.toContain("Paid to");
|
||||||
|
|
||||||
// Real mainnet calldata is in the deployed shape, so its V4 step is
|
// Real mainnet calldata is in the deployed shape, so its V4 step is
|
||||||
// not said to be undecoded.
|
// not said to be undecoded.
|
||||||
|
|||||||
+113
-16
@@ -424,18 +424,24 @@ describe("an output left in the router is not received", () => {
|
|||||||
|
|
||||||
// Uniswap's mixed routes pay a V2 pair directly when the next step swaps
|
// Uniswap's mixed routes pay a V2 pair directly when the next step swaps
|
||||||
// through it. That V2 swap spends what the pair holds: its amountIn is
|
// through it. That V2 swap spends what the pair holds: its amountIn is
|
||||||
// CONTRACT_BALANCE, paid from the router, or ALREADY_PAID.
|
// CONTRACT_BALANCE, paid from the router, or ALREADY_PAID. The pairs below
|
||||||
|
// are the addresses the routers find for the first two tokens of that V2
|
||||||
|
// swap's path.
|
||||||
describe("a swap paid to the pair the next V2 swap spends", () => {
|
describe("a swap paid to the pair the next V2 swap spends", () => {
|
||||||
const PAIR = "0x5555555555555555555555555555555555555555";
|
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||||
|
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||||
const CONTRACT_BALANCE = 2n ** 255n;
|
const CONTRACT_BALANCE = 2n ** 255n;
|
||||||
const swapToPair = v3SwapExactIn(PAIR, USDT_2000, 0n, USDT, WETH);
|
const AMOUNTS_IN = [
|
||||||
|
{ kind: "CONTRACT_BALANCE", amountIn: CONTRACT_BALANCE },
|
||||||
|
{ kind: "ALREADY_PAID", amountIn: 0n },
|
||||||
|
];
|
||||||
|
const swapTo = (recipient) =>
|
||||||
|
v3SwapExactIn(recipient, USDT_2000, 0n, USDT, WETH);
|
||||||
|
const swapToPair = swapTo(PAIR);
|
||||||
const pairSpends = (amountIn, path) =>
|
const pairSpends = (amountIn, path) =>
|
||||||
v2Swap(0x08, MSG_SENDER, amountIn, USDC_1990, path, false);
|
v2Swap(0x08, MSG_SENDER, amountIn, USDC_1990, path, false);
|
||||||
|
|
||||||
test.each([
|
test.each(AMOUNTS_IN)(
|
||||||
{ kind: "CONTRACT_BALANCE", amountIn: CONTRACT_BALANCE },
|
|
||||||
{ kind: "ALREADY_PAID", amountIn: 0n },
|
|
||||||
])(
|
|
||||||
"a V3 swap, then a V2 swap whose amountIn is $kind, reads as a swap paid to the user",
|
"a V3 swap, then a V2 swap whose amountIn is $kind, reads as a swap paid to the user",
|
||||||
({ amountIn }) => {
|
({ amountIn }) => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
@@ -450,22 +456,113 @@ describe("a swap paid to the pair the next V2 swap spends", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test("a V4 step whose TAKE pays the pair, then the V2 swap, reads the same", () => {
|
test.each(AMOUNTS_IN)(
|
||||||
const result = decode(
|
"a V3 swap paid to another address, then a V2 swap whose amountIn is $kind, names that address",
|
||||||
|
({ amountIn }) => {
|
||||||
|
const result = decode(
|
||||||
|
execute(swapTo(OTHER), pairSpends(amountIn, [WETH, USDC])),
|
||||||
|
);
|
||||||
|
expect(labels(result)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(result)).toEqual([OTHER]);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe(
|
||||||
|
"1990.0000 USDC",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ recipient: PAIR, lines: PAID_TO_USER, paid: [] },
|
||||||
|
{ recipient: OTHER, lines: PAID_ELSEWHERE, paid: [OTHER] },
|
||||||
|
])(
|
||||||
|
"a V4 step whose TAKE pays $recipient, then the V2 swap",
|
||||||
|
({ recipient, lines, paid }) => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
SETTLE_USDT,
|
||||||
|
take(USDC, recipient),
|
||||||
|
),
|
||||||
|
v2Swap(
|
||||||
|
0x08,
|
||||||
|
MSG_SENDER,
|
||||||
|
CONTRACT_BALANCE,
|
||||||
|
ONE_WETH,
|
||||||
|
[USDC, WETH],
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(labels(result)).toEqual(lines);
|
||||||
|
expect(paidTo(result)).toEqual(paid);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("on Sepolia", () => {
|
||||||
|
const SEPOLIA = "0xaa36a7";
|
||||||
|
// UniversalRouterV2_1_2 on Sepolia.
|
||||||
|
const SEPOLIA_ROUTER = "0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3";
|
||||||
|
// The WETH/USDC pair the routers on Sepolia find. Sepolia's V2
|
||||||
|
// factory reports another address for it, and no contract sits at
|
||||||
|
// this one (see V2_FACTORIES in src/shared/uniswap.js).
|
||||||
|
const SEPOLIA_PAIR = "0x541b50d32B73E769ac42F0FddD86D15FcAa861Fb";
|
||||||
|
const SEPOLIA_UNI = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984";
|
||||||
|
const SEPOLIA_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||||
|
const SEPOLIA_USDC = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238";
|
||||||
|
// UNI for WETH, paid to `recipient`, then WETH for USDC through the
|
||||||
|
// pair.
|
||||||
|
const route = (recipient, amountIn) =>
|
||||||
execute(
|
execute(
|
||||||
v4Swap(SWAP_USDT_FOR_USDC, SETTLE_USDT, take(USDC, PAIR)),
|
v3SwapExactIn(
|
||||||
|
recipient,
|
||||||
|
ONE_WETH,
|
||||||
|
0n,
|
||||||
|
SEPOLIA_UNI,
|
||||||
|
SEPOLIA_WETH,
|
||||||
|
),
|
||||||
v2Swap(
|
v2Swap(
|
||||||
0x08,
|
0x08,
|
||||||
MSG_SENDER,
|
MSG_SENDER,
|
||||||
CONTRACT_BALANCE,
|
amountIn,
|
||||||
ONE_WETH,
|
USDC_1990,
|
||||||
[USDC, WETH],
|
[SEPOLIA_WETH, SEPOLIA_USDC],
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
|
||||||
|
test.each(AMOUNTS_IN)(
|
||||||
|
"paid to the pair, then a V2 swap whose amountIn is $kind, reads as a swap paid to the user",
|
||||||
|
({ amountIn }) => {
|
||||||
|
const result = uniswap.decode(
|
||||||
|
route(SEPOLIA_PAIR, amountIn),
|
||||||
|
SEPOLIA_ROUTER,
|
||||||
|
SEPOLIA,
|
||||||
|
{},
|
||||||
|
USER,
|
||||||
|
);
|
||||||
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
|
||||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
test.each(AMOUNTS_IN)(
|
||||||
|
"paid to another address, then a V2 swap whose amountIn is $kind, names that address",
|
||||||
|
({ amountIn }) => {
|
||||||
|
const result = uniswap.decode(
|
||||||
|
route(OTHER, amountIn),
|
||||||
|
SEPOLIA_ROUTER,
|
||||||
|
SEPOLIA,
|
||||||
|
{},
|
||||||
|
USER,
|
||||||
|
);
|
||||||
|
expect(labels(result)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(result)).toEqual([OTHER]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("the same route on Ethereum Mainnet names the Sepolia pair", () => {
|
||||||
|
const result = decode(route(SEPOLIA_PAIR, 0n));
|
||||||
|
expect(paidTo(result)).toEqual([SEPOLIA_PAIR]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
|
|||||||
Reference in New Issue
Block a user