fix: a WRAP_ETH says where its ETH goes when no swap step spends it (closes #747)
The `Moved to` line of a WRAP_ETH into the router now reads `The router`, unless a later swap step pays from that WETH. A later WRAP_ETH of CONTRACT_BALANCE to anyone but the router now gets a `Moved from the sender` line reading `All the ETH the transaction carries`, and `Moved to` names the recipient. It gets no lines after an earlier step paid ETH to the router, because Uniswap's routes pay back unspent ETH this way. README states the new rule. Model: opus-5-5
This commit is contained in:
@@ -1416,8 +1416,10 @@ when both tokens are known. The lines are:
|
||||
after the step that sets `Token In` takes the amount it states from the
|
||||
sender. When it pays anyone but the router, that amount gets these lines. When
|
||||
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
||||
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states
|
||||
no amount, so it gets no lines. ETH that the sender pays a V4 step or a
|
||||
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to
|
||||
anyone but the router gets these lines, unless an earlier step paid ETH to the
|
||||
router. Its `Moved from the sender` line reads
|
||||
`All the ETH the transaction carries`. ETH that the sender pays a V4 step or a
|
||||
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
||||
hold ETH from an earlier step. When the decoder knows the amount, its
|
||||
`Moved from the sender` line reads `Up to` and the amount.
|
||||
|
||||
+90
-31
@@ -404,6 +404,11 @@ const OPEN_DELTA = Symbol("v4-open-delta");
|
||||
// that nothing is swapped.
|
||||
const ALREADY_PAID = Symbol("v2-already-paid");
|
||||
|
||||
// A later WRAP_ETH of CONTRACT_BALANCE wraps all the ETH the router holds, and
|
||||
// so all the ETH the transaction carries. The calldata states no quantity for
|
||||
// it. The approval screen's Value line gives that ETH.
|
||||
const TRANSACTION_ETH = Symbol("transaction-eth");
|
||||
|
||||
// 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.
|
||||
@@ -416,6 +421,7 @@ const CONTRACT_BALANCE = 2n ** 255n;
|
||||
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 TRANSACTION_ETH_AMOUNT = "All the ETH the transaction carries";
|
||||
const NO_MINIMUM = "None (no minimum guaranteed)";
|
||||
|
||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||
@@ -1271,6 +1277,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// when it is not the router; otherwise null. That WRAP_ETH pays its
|
||||
// WETH there, not to a swap step (see the Moved to lines).
|
||||
let wrapEthRecipient = null;
|
||||
// The index of the WRAP_ETH that set the input side, or null.
|
||||
let wrapEthStep = null;
|
||||
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||
// input side, or null (see permitStands after the loop).
|
||||
let permit = null;
|
||||
@@ -1289,6 +1297,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
let hasUnwrapWeth = false;
|
||||
// Set once an UNWRAP_WETH pays ETH to the router, which a later
|
||||
// WRAP_ETH of CONTRACT_BALANCE then wraps.
|
||||
let unwrappedIntoRouter = false;
|
||||
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
||||
// which the router holds; cleared whenever the output side is set
|
||||
// again, or a later step may pay that ETH out as WETH (see after the
|
||||
@@ -1308,6 +1319,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// other than WETH, and not of ETH to a WETH contract, which pays the
|
||||
// router WETH (see keepsRouterWeth in the loop). None of those can.
|
||||
let lastWethStep = -1;
|
||||
// The index of the last swap step paid from WETH the router holds, a
|
||||
// V4 SETTLE included, or -1 (see spendFromRouter()). Only such a step
|
||||
// spends the WETH of an earlier WRAP_ETH into the router.
|
||||
let lastRouterWethSwapStep = -1;
|
||||
// Where the step that set the output side paid its output: the
|
||||
// recipient of a swap step, as written, or of each TAKE, TAKE_ALL and
|
||||
// TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
|
||||
@@ -1370,7 +1385,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// `amount`: it never joins the Amount line, and its line reads
|
||||
// "Up to" and the amount. A take with a `recipient`, the ETH of a
|
||||
// later WRAP_ETH that pays anyone but the router, never joins the
|
||||
// Amount line either: its Moved to line names that recipient.
|
||||
// Amount line either: its Moved to line names that recipient. The
|
||||
// take of a later WRAP_ETH of a stated amount has that step's index as
|
||||
// `wrapStep`.
|
||||
const userPaid = [];
|
||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
||||
// of `token` from the sender, or at most that when `atMost` is set. A
|
||||
@@ -1445,17 +1462,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const r = recipient.toLowerCase();
|
||||
return r === MSG_SENDER || r === sender;
|
||||
};
|
||||
// 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) => {
|
||||
// A swap step or V4 SETTLE at `step` 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 = (step, token, amount) => {
|
||||
const t = token.toLowerCase();
|
||||
const held = heldByRouter.get(t);
|
||||
if (t === WETH.get(chainId) && heldByRouter.has(t)) {
|
||||
lastRouterWethSwapStep = step;
|
||||
}
|
||||
if (amount === CONTRACT_BALANCE) {
|
||||
heldByRouter.delete(t);
|
||||
} else if (typeof held === "bigint" && typeof amount === "bigint") {
|
||||
@@ -1741,7 +1761,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
spendFromRouter(i, s.tokenIn, s.amountIn);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
@@ -1770,7 +1790,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
spendFromRouter(i, s.tokenIn, s.amountIn);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
@@ -1786,8 +1806,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
) {
|
||||
// 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.
|
||||
// does not state, so spendFromRouter() has none to count.
|
||||
hasV2OrV3ExactOut = true;
|
||||
const s =
|
||||
command === "V2_SWAP_EXACT_OUT"
|
||||
@@ -1809,6 +1828,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
]),
|
||||
[s.recipient],
|
||||
);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(i, s.tokenIn, null);
|
||||
}
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
}
|
||||
@@ -1906,6 +1928,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
w.amount,
|
||||
);
|
||||
inputPaidByUser = true;
|
||||
wrapEthStep = i;
|
||||
if (!isRouter(w.recipient)) {
|
||||
wrapEthRecipient = w.recipient;
|
||||
}
|
||||
@@ -1916,8 +1939,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// The ETH of a stated amount comes from the
|
||||
// transaction's value. ETH an earlier step left in
|
||||
// the router may pay part of it, and the sender
|
||||
// then pays at most that amount. CONTRACT_BALANCE
|
||||
// states no amount to take.
|
||||
// then pays at most that amount.
|
||||
userPaid.push({
|
||||
token: "0x0000000000000000000000000000000000000000",
|
||||
amount: w.amount,
|
||||
@@ -1927,6 +1949,25 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
recipient: isRouter(w.recipient)
|
||||
? null
|
||||
: w.recipient,
|
||||
wrapStep: i,
|
||||
});
|
||||
} else if (
|
||||
w.amount === CONTRACT_BALANCE &&
|
||||
!isRouter(w.recipient) &&
|
||||
!unwrappedIntoRouter &&
|
||||
!heldByRouter.has(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
)
|
||||
) {
|
||||
// It pays the recipient all the ETH the transaction
|
||||
// carries. After an earlier step paid ETH to the
|
||||
// router there is no line: Uniswap's routes pay back
|
||||
// what their swap did not spend this way, and carry
|
||||
// no ETH.
|
||||
userPaid.push({
|
||||
token: "0x0000000000000000000000000000000000000000",
|
||||
amount: TRANSACTION_ETH,
|
||||
recipient: w.recipient,
|
||||
});
|
||||
}
|
||||
addPayee(w.recipient);
|
||||
@@ -1996,7 +2037,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Each SETTLE paid from the router spends the amount
|
||||
// decodeV4Swap() says it pays.
|
||||
for (const s of v4.settledFromRouter) {
|
||||
spendFromRouter(s.currency, s.amount);
|
||||
spendFromRouter(i, s.currency, s.amount);
|
||||
}
|
||||
for (const t of v4.takes) {
|
||||
if (!isHop(i, t.currency, t.recipient)) {
|
||||
@@ -2169,6 +2210,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
heldByRouter.has(w),
|
||||
);
|
||||
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
||||
if (isRouter(u.recipient)) {
|
||||
unwrappedIntoRouter = true;
|
||||
}
|
||||
if (isRouter(u.recipient) && heldWeth) {
|
||||
heldByRouter.set(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
@@ -2264,10 +2308,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
permit.beforeEveryTake &&
|
||||
!stepsUnread.some((step) => step > permit.step);
|
||||
// What the steps take that the Amount line does not give, as
|
||||
// { token, amount, atMost, recipient }. A null token is a step this
|
||||
// decoder did not read (see spentUnread()), or a token it does not
|
||||
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
|
||||
// pays elsewhere has a recipient (see wrapEthRecipient and userPaid).
|
||||
// { token, amount, atMost, recipient, wrapStep }. A null token is a
|
||||
// step this decoder did not read (see spentUnread()), or a token it
|
||||
// does not know. `atMost` is as in `userPaid`. Only the take of a
|
||||
// WRAP_ETH that pays elsewhere has a recipient (see wrapEthRecipient
|
||||
// and userPaid). Only the take of a WRAP_ETH has a `wrapStep`.
|
||||
const spentFromSender = [];
|
||||
let total = null;
|
||||
// The takes from the sender that `total` adds up, each a quantity, in
|
||||
@@ -2292,6 +2337,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
wrapStep: wrapEthStep,
|
||||
});
|
||||
}
|
||||
if (hasFirstTake && !present(inputAmount)) {
|
||||
@@ -2304,6 +2350,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
token: inputToken,
|
||||
amount: inputAmount,
|
||||
recipient: wrapEthRecipient,
|
||||
wrapStep: wrapEthStep,
|
||||
});
|
||||
}
|
||||
if (permitStands) inputToken = permit.token;
|
||||
@@ -2413,13 +2460,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
|
||||
// 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. `atMost` marks an amount that is only the most a
|
||||
// take may be (see userPaid).
|
||||
// the lines for other tokens below it. Four amounts need no scale to
|
||||
// describe and are named rather than formatted: V4's open delta, V2's
|
||||
// already-paid zero and the ETH the transaction carries, none of which
|
||||
// is a quantity at all (see OPEN_DELTA, ALREADY_PAID and
|
||||
// TRANSACTION_ETH), and an unbounded permit. Those three tests come
|
||||
// first — the sentinels are not bigints and cannot be compared with
|
||||
// one. `atMost` marks an amount that is only the most a take may be
|
||||
// (see userPaid).
|
||||
const spentText = (amount, info, atMost) => {
|
||||
if (amount === OPEN_DELTA) {
|
||||
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||
@@ -2430,6 +2478,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
display: ALREADY_PAID_AMOUNT,
|
||||
};
|
||||
}
|
||||
if (amount === TRANSACTION_ETH) {
|
||||
return {
|
||||
raw: TRANSACTION_ETH_AMOUNT,
|
||||
display: TRANSACTION_ETH_AMOUNT,
|
||||
};
|
||||
}
|
||||
if (amount >= MAX_UINT160) {
|
||||
return { raw: "Unlimited", display: "Unlimited" };
|
||||
}
|
||||
@@ -2489,8 +2543,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// 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. A take with no recipient goes to the swap step
|
||||
// that spends it. A take with a recipient goes to that recipient;
|
||||
// `MSG_SENDER` means the sender.
|
||||
// that spends it. The take of a WRAP_ETH into the router goes to the
|
||||
// router, unless a later swap step spends its WETH. A take with a
|
||||
// recipient goes to that recipient; `MSG_SENDER` means the sender.
|
||||
for (const s of spentFromSender) {
|
||||
details.push({
|
||||
label: "Moved from the sender",
|
||||
@@ -2512,7 +2567,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!present(s.recipient)) {
|
||||
details.push({
|
||||
label: "Moved to",
|
||||
value: "The swap step that spends it",
|
||||
value:
|
||||
present(s.wrapStep) &&
|
||||
s.wrapStep > lastRouterWethSwapStep
|
||||
? "The router"
|
||||
: "The swap step that spends it",
|
||||
});
|
||||
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
||||
details.push({ label: "Moved to", value: "The sender" });
|
||||
|
||||
+334
-21
@@ -2474,10 +2474,9 @@ describe("uniswap decoder", () => {
|
||||
// SETTLE of ETH from the router's own ETH, whichever payer it names.
|
||||
// After an earlier step that may leave ETH in the router, the ETH that
|
||||
// the user pays a V4 step reads as a maximum, on lines of its own.
|
||||
// Every row reads at least the ETH that can leave the wallet. The two
|
||||
// rows marked "reads as on next" read the same on next. On next every
|
||||
// other row added that ETH to the Amount line, or read it as an exact
|
||||
// amount.
|
||||
// Every row reads at least the ETH that can leave the wallet. The row
|
||||
// marked "reads as on next" reads the same on next. On next every other
|
||||
// row added that ETH to the Amount line, or read it as an exact amount.
|
||||
//
|
||||
// ADDRESS_THIS is the router on every release.
|
||||
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||
@@ -2625,7 +2624,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...wrapLines("The router"),
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
@@ -2674,7 +2673,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH, reads as on next",
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH",
|
||||
commands: [0x0b, 0x0c, 0x10],
|
||||
inputs: [
|
||||
WRAP_INTO_ROUTER,
|
||||
@@ -2697,10 +2696,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
),
|
||||
],
|
||||
lines: [
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...ethLines(UNDECODED),
|
||||
],
|
||||
lines: [...wrapLines("The router"), ...ethLines(UNDECODED)],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
|
||||
@@ -2761,8 +2757,8 @@ describe("uniswap decoder", () => {
|
||||
// does not set the input side takes the amount it states from the
|
||||
// sender. On next no line gave that ETH. The rows marked "reads as on
|
||||
// next" state no amount to take from the sender. Uniswap's own routes
|
||||
// wrap CONTRACT_BALANCE after their first step, as the last two rows
|
||||
// do.
|
||||
// wrap CONTRACT_BALANCE into the router after their first step, as the
|
||||
// last row does.
|
||||
const CONTRACT_BALANCE = 2n ** 255n;
|
||||
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
|
||||
test.each([
|
||||
@@ -2787,7 +2783,7 @@ describe("uniswap decoder", () => {
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -2796,7 +2792,7 @@ describe("uniswap decoder", () => {
|
||||
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -2839,7 +2835,9 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "1.0000 ETH"],
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", "The router"],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -2877,12 +2875,6 @@ describe("uniswap decoder", () => {
|
||||
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap of 600 USDT that takes ETH into the router, a WRAP_ETH of CONTRACT_BALANCE into the router, then a V3 exact-in of it, reads as on next",
|
||||
commands: [0x10, 0x0b, 0x00],
|
||||
@@ -2923,6 +2915,327 @@ describe("uniswap decoder", () => {
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — the WETH of a
|
||||
// WRAP_ETH into the router goes to a swap step only when a later swap
|
||||
// step paid from the router spends it. On next every row not marked
|
||||
// "reads as on next" read The swap step that spends it.
|
||||
const SWAP_STEP = "The swap step that spends it";
|
||||
// Each pays 1 WETH from the router, for USDC, to the user.
|
||||
const V3_WETH_FROM_ROUTER = coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[
|
||||
USER_ADDR,
|
||||
ONE_ETH,
|
||||
1n,
|
||||
encodeV3Path([WETH_ADDR, USDC_ADDR]),
|
||||
false,
|
||||
],
|
||||
);
|
||||
const V2_WETH_FROM_ROUTER = coder.encode(
|
||||
["address", "uint256", "uint256", "address[]", "bool"],
|
||||
[USER_ADDR, ONE_ETH, 1n, [WETH_ADDR, USDC_ADDR], false],
|
||||
);
|
||||
const V4_WETH_FROM_ROUTER = encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(WETH_ADDR, USDC_ADDR, ONE_ETH, 1n),
|
||||
settle(WETH_ADDR, 0n, false),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
);
|
||||
// SWEEP has the layout of TRANSFER, and its amount is a minimum.
|
||||
const SWEEP_WETH = payOut(WETH_ADDR, 0n);
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a SWEEP of that WETH to another address",
|
||||
commands: [0x00, 0x0b, 0x04],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, SWEEP_WETH],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a SWEEP of that WETH to another address, then a V3 exact-in paid from the router's WETH",
|
||||
commands: [0x00, 0x0b, 0x04, 0x00],
|
||||
inputs: [
|
||||
V3_600,
|
||||
WRAP_INTO_ROUTER,
|
||||
SWEEP_WETH,
|
||||
V3_WETH_FROM_ROUTER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of WETH that the user pays",
|
||||
commands: [0x00, 0x0b, 0x00],
|
||||
inputs: [
|
||||
V3_600,
|
||||
WRAP_INTO_ROUTER,
|
||||
encodeV3SwapExactIn(USER_ADDR, ONE_ETH, 1n, [
|
||||
WETH_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
["Moved from the sender", "1.0000 WETH"],
|
||||
["Token moved", "WETH (" + WETH_ADDR + ")"],
|
||||
["Moved to", SWAP_STEP],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a V3 exact-in paid from the router's WETH, then another WRAP_ETH of 1 ETH into the router",
|
||||
commands: [0x00, 0x0b, 0x00, 0x0b],
|
||||
inputs: [
|
||||
V3_600,
|
||||
WRAP_INTO_ROUTER,
|
||||
V3_WETH_FROM_ROUTER,
|
||||
WRAP_INTO_ROUTER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines(SWAP_STEP),
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH, reads as on next",
|
||||
commands: [0x00, 0x0b, 0x00],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V2 exact-in paid from the router's WETH, reads as on next",
|
||||
commands: [0x00, 0x0b, 0x08],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V3 exact-out paid from the router's WETH, reads as on next",
|
||||
commands: [0x00, 0x0b, 0x01],
|
||||
inputs: [
|
||||
V3_600,
|
||||
WRAP_INTO_ROUTER,
|
||||
encodeV3SwapExactOut(
|
||||
USER_ADDR,
|
||||
390000000n,
|
||||
ONE_ETH,
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
false,
|
||||
),
|
||||
],
|
||||
lines: [
|
||||
["Amount", "Up to 600.0000 USDT"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", SWAP_STEP],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V4 swap that the router's WETH settles, reads as on next",
|
||||
commands: [0x00, 0x0b, 0x10],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN",
|
||||
commands: [0x0b, 0x04, 0x21],
|
||||
inputs: [WRAP_INTO_ROUTER, SWEEP_WETH, SUB_PLAN],
|
||||
lines: [
|
||||
...wrapLines("The router"),
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN, reads as on next",
|
||||
commands: [0x0b, 0x00, 0x21],
|
||||
inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN],
|
||||
lines: [
|
||||
...wrapLines(SWAP_STEP),
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of 600 USDT",
|
||||
commands: [0x0a, 0x0b, 0x00],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||
WRAP_INTO_ROUTER,
|
||||
V3_600,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "5000.0000 USDT"],
|
||||
...wrapLines("The router"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH, reads as on next",
|
||||
commands: [0x0a, 0x0b, 0x00, 0x00],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||
WRAP_INTO_ROUTER,
|
||||
V3_WETH_FROM_ROUTER,
|
||||
V3_600,
|
||||
],
|
||||
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(SWAP_STEP)],
|
||||
},
|
||||
])("$kind", ({ commands, inputs, lines }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH
|
||||
// of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an
|
||||
// earlier step paid ETH to the router, that is the ETH the transaction
|
||||
// carries. On next no line gave it. Uniswap's own routes pay back what
|
||||
// their swap did not spend this way, and carry no ETH, so in the rows
|
||||
// marked "reads as on next" that WRAP_ETH gets no lines.
|
||||
const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE);
|
||||
const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE);
|
||||
// The lines of such a WRAP_ETH whose Moved to line reads `to`.
|
||||
const allEthLines = (to) => [
|
||||
["Moved from the sender", "All the ETH the transaction carries"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", to],
|
||||
];
|
||||
// The first steps of a route that spends WETH on a V4 pool of ETH:
|
||||
// a PERMIT2_TRANSFER_FROM of 1 WETH into the router, an UNWRAP_WETH
|
||||
// into the router, then a V4 exact-out paid from the router's ETH.
|
||||
const UNWRAP_ROUTE = [
|
||||
coder.encode(
|
||||
["address", "address", "uint160"],
|
||||
[WETH_ADDR, ADDRESS_THIS, ONE_ETH],
|
||||
),
|
||||
UNWRAP_INTO_ROUTER,
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactOutSingle(
|
||||
ZERO_ADDR,
|
||||
USDC_ADDR,
|
||||
390000000n,
|
||||
ONE_ETH,
|
||||
),
|
||||
settle(ZERO_ADDR, 0n, false),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
),
|
||||
];
|
||||
// The lines of that route on next: this decoder cannot tell what the
|
||||
// V4 step's SETTLE of ETH pays.
|
||||
const UNWRAP_ROUTE_LINES = [
|
||||
["Moved from the sender", UNDECODED],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", SWAP_STEP],
|
||||
];
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_ALL_TO_OTHER],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...allEthLines(OTHER_ADDR),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_ALL_TO_SENDER],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...allEthLines("The sender"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-out of at most 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
||||
commands: [0x01, 0x0b],
|
||||
inputs: [
|
||||
encodeV3SwapExactOut(USER_ADDR, 390000000n, USDT_600, [
|
||||
USDC_ADDR,
|
||||
USDT_ADDR,
|
||||
]),
|
||||
WRAP_ALL_TO_OTHER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "Up to 600.0000 USDT"],
|
||||
...allEthLines(OTHER_ADDR),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
||||
commands: [0x0b, 0x0b],
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER],
|
||||
lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
||||
commands: [0x00, 0x0c, 0x0b],
|
||||
inputs: [
|
||||
V3_600,
|
||||
encodeWrapEth(MSG_SENDER, 0n),
|
||||
WRAP_ALL_TO_OTHER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...allEthLines(OTHER_ADDR),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap of 600 USDT that takes ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
||||
commands: [0x10, 0x0b],
|
||||
inputs: [
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
ZERO_ADDR,
|
||||
USDT_600,
|
||||
1n,
|
||||
),
|
||||
settle(USDT_ADDR, 0n, true),
|
||||
take(ZERO_ADDR, ADDRESS_THIS, 0n),
|
||||
],
|
||||
),
|
||||
WRAP_ALL_TO_SENDER,
|
||||
],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
||||
commands: [0x02, 0x0c, 0x10, 0x0b],
|
||||
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_SENDER],
|
||||
lines: UNWRAP_ROUTE_LINES,
|
||||
},
|
||||
{
|
||||
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to another address, reads as on next",
|
||||
commands: [0x02, 0x0c, 0x10, 0x0b],
|
||||
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_OTHER],
|
||||
lines: UNWRAP_ROUTE_LINES,
|
||||
},
|
||||
])("$kind", ({ commands, inputs, lines }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
|
||||
// USDT for USDC.
|
||||
const v3ExactOut = (amountInMax) =>
|
||||
|
||||
Reference in New Issue
Block a user