fix: a V3 exact-out swap step shows its tokens and amounts (closes #601)
A V3_SWAP_EXACT_OUT step was named in the Steps line but never read, so its tokens read as not named in the calldata and it gave no amounts. It is now read as a V2_SWAP_EXACT_OUT is: its path from the token it buys to the token it spends, as the router reads it, its amountInMax as the most it spends, its amountOut as the minimum, and its recipient as the address it pays. One the user pays for after the step that sets the input side joins the Amount total. One whose input cannot be read still leaves no Amount figure. Model: opus-5-5
This commit is contained in:
+131
-15
@@ -88,17 +88,42 @@ function encodeV2SwapExactOut(recipient, amountOut, amountInMax, pathAddrs) {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: encode a V3_SWAP_EXACT_IN input (command 0x00)
|
||||
function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) {
|
||||
// V3 path: token(20) + fee(3) + token(20) ...
|
||||
// Helper: a V3 path: token(20) + fee(3) + token(20) ...
|
||||
function encodeV3Path(pathTokens) {
|
||||
let pathHex = pathTokens[0].slice(2).toLowerCase();
|
||||
for (let i = 1; i < pathTokens.length; i++) {
|
||||
pathHex += "000bb8"; // fee 3000 = 0x000bb8
|
||||
pathHex += pathTokens[i].slice(2).toLowerCase();
|
||||
}
|
||||
return "0x" + pathHex;
|
||||
}
|
||||
|
||||
// Helper: encode a V3_SWAP_EXACT_IN input (command 0x00)
|
||||
function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) {
|
||||
return coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[recipient, amountIn, amountOutMin, "0x" + pathHex, true],
|
||||
[recipient, amountIn, amountOutMin, encodeV3Path(pathTokens), true],
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: encode a V3_SWAP_EXACT_OUT input (command 0x01). Its path lists
|
||||
// the token bought first and the token spent last.
|
||||
function encodeV3SwapExactOut(
|
||||
recipient,
|
||||
amountOut,
|
||||
amountInMax,
|
||||
pathTokens,
|
||||
payerIsUser = true,
|
||||
) {
|
||||
return coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[
|
||||
recipient,
|
||||
amountOut,
|
||||
amountInMax,
|
||||
encodeV3Path(pathTokens),
|
||||
payerIsUser,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -644,6 +669,79 @@ describe("uniswap decoder", () => {
|
||||
expect(detail(result, "Min. received").value).toBe("1500.0000 USDC");
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/601 — a V3_SWAP_EXACT_OUT
|
||||
// was named but not read, so its tokens read "Unknown (not named in the
|
||||
// calldata)" and it showed no amounts. It is read as a V2_SWAP_EXACT_OUT
|
||||
// is, with its path read as the router reads it: from the token it buys
|
||||
// to the token it spends. Read the other way, each path below would be a
|
||||
// swap of WETH for USDT.
|
||||
test.each([
|
||||
{ hops: "single-hop", path: [WETH_ADDR, USDT_ADDR] },
|
||||
{ hops: "multi-hop", path: [WETH_ADDR, USDC_ADDR, USDT_ADDR] },
|
||||
])(
|
||||
"decodes a $hops V3_SWAP_EXACT_OUT, stating the input amount as a maximum",
|
||||
({ path }) => {
|
||||
// Buy exactly 1 WETH for at most 2,000 USDT, paid by the user.
|
||||
const data = buildExecute(
|
||||
"0x01", // V3_SWAP_EXACT_OUT
|
||||
[encodeV3SwapExactOut(USER_ADDR, ONE_WETH, 2000000000n, path)],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(detail(result, "Token In").address).toBe(
|
||||
USDT_ADDR.toLowerCase(),
|
||||
);
|
||||
expect(detail(result, "Token Out").address).toBe(
|
||||
WETH_ADDR.toLowerCase(),
|
||||
);
|
||||
|
||||
const amount = detail(result, "Amount");
|
||||
expect(amount.value).toBe("Up to 2000.0000 USDT");
|
||||
expect(amount.rawValue).toBe("Up to 2000.0000");
|
||||
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||
},
|
||||
);
|
||||
|
||||
// The V2 exact-out swap paid in ETH above, as a V3_SWAP_EXACT_OUT paid
|
||||
// from the WETH the router holds: it takes nothing more from the user's
|
||||
// wallet.
|
||||
test("a V3 exact-out swap paid in ETH from the router shows the token it buys and a maximum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0b, 0x01, 0x0c]),
|
||||
[
|
||||
encodeWrapEth(ROUTER_ADDR, 500000000000000000n),
|
||||
encodeV3SwapExactOut(
|
||||
USER_ADDR,
|
||||
1500000000n, // amountOut: 1,500 USDC
|
||||
500000000000000000n, // amountInMax: 0.5 WETH
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
false, // paid from the router
|
||||
),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH same encoding
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
expect(result.name).toBe("Swap ETH → USDC");
|
||||
expect(detail(result, "Amount").value).toBe("Up to 0.5000 ETH");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
expect(detail(result, "Token Out").address).toBe(
|
||||
USDC_ADDR.toLowerCase(),
|
||||
);
|
||||
expect(detail(result, "Paid to")).toBeUndefined();
|
||||
expect(detail(result, "Min. received").value).toBe("1500.0000 USDC");
|
||||
});
|
||||
|
||||
// Buy exactly 0.5 ETH for at most 1,500 USDC under a permit: the swap buys
|
||||
// WETH and UNWRAP_WETH turns it into ETH.
|
||||
test("a V2 exact-out swap that buys ETH shows ETH and the permit as a maximum", () => {
|
||||
@@ -2048,11 +2146,9 @@ describe("uniswap decoder", () => {
|
||||
);
|
||||
|
||||
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
|
||||
// USDT for USDC. Its input is (recipient, amountOut, amountInMax,
|
||||
// path, payerIsUser), laid out as encodeV3SwapExactIn() writes one;
|
||||
// an exact-out path lists the token bought first.
|
||||
// USDT for USDC.
|
||||
const v3ExactOut = (amountInMax) =>
|
||||
encodeV3SwapExactIn(USER_ADDR, 390000000n, amountInMax, [
|
||||
encodeV3SwapExactOut(USER_ADDR, 390000000n, amountInMax, [
|
||||
USDC_ADDR,
|
||||
USDT_ADDR,
|
||||
]);
|
||||
@@ -2070,14 +2166,16 @@ describe("uniswap decoder", () => {
|
||||
[USDC_ADDR, USER_ADDR, 0n],
|
||||
),
|
||||
];
|
||||
// This decoder does not read a V3_SWAP_EXACT_OUT, so what one takes
|
||||
// leaves no Amount figure wherever it stands, and a permit's amount
|
||||
// does not stand.
|
||||
// What a V3_SWAP_EXACT_OUT takes, at most its amountInMax, counts
|
||||
// as a V2_SWAP_EXACT_OUT's does, and the Amount line is a maximum.
|
||||
// The permit of 10,000 USDT stands; the one of 600 USDT does not,
|
||||
// because the step before it took its token.
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT",
|
||||
kind: "a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT of at most 400 USDT",
|
||||
commands: [0x00, 0x01],
|
||||
inputs: [V3_600, v3ExactOut(USDT_400)],
|
||||
amount: "Up to 1000.0000 USDT",
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 10,000 USDT, then a V3_SWAP_EXACT_OUT, a V2_SWAP_EXACT_OUT and a SWEEP",
|
||||
@@ -2086,11 +2184,13 @@ describe("uniswap decoder", () => {
|
||||
encodePermit2(USDT_ADDR, 10000000000n, ROUTER_ADDR),
|
||||
...EXACT_OUT_SPLIT,
|
||||
],
|
||||
amount: "Up to 10000.0000 USDT",
|
||||
},
|
||||
{
|
||||
kind: "a V3_SWAP_EXACT_OUT, a V2_SWAP_EXACT_OUT and a SWEEP",
|
||||
commands: [0x01, 0x09, 0x04],
|
||||
inputs: EXACT_OUT_SPLIT,
|
||||
amount: "Up to 10000.0000 USDT",
|
||||
},
|
||||
{
|
||||
kind: "a V3_SWAP_EXACT_OUT of at most 9,400 USDT, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 600 USDT",
|
||||
@@ -2100,17 +2200,33 @@ describe("uniswap decoder", () => {
|
||||
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||
V3_600,
|
||||
],
|
||||
amount: "Up to 10000.0000 USDT",
|
||||
},
|
||||
])(
|
||||
"$kind, all paid by the user, shows no Amount figure",
|
||||
({ commands, inputs }) => {
|
||||
"$kind, all paid by the user, shows the total as a maximum",
|
||||
({ commands, inputs, amount }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Moved from the sender", UNDECODED],
|
||||
["Amount", amount],
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
// A V3_SWAP_EXACT_OUT whose path is shorter than one token cannot be
|
||||
// read. It may pay any address and take from the sender, so it
|
||||
// leaves no Amount figure.
|
||||
test("a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT whose path cannot be read, shows no Amount figure", () => {
|
||||
const unreadable = coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[USER_ADDR, 390000000n, USDT_400, "0x", true],
|
||||
);
|
||||
const result = decodeSteps([0x00, 0x01], [V3_600, unreadable]);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Moved from the sender", UNDECODED],
|
||||
]);
|
||||
expect(detail(result, "Paid to").value).toBe(UNDECODED);
|
||||
});
|
||||
|
||||
// A later step's amount that cannot be added to the Amount line,
|
||||
// because the amount of the step that set the input side is not a
|
||||
// quantity, gets lines of its own. When that step stated no amount,
|
||||
|
||||
@@ -387,6 +387,18 @@ describe("each swap command's recipient", () => {
|
||||
data: (r) => execute(swapToWeth(r)),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
// Its input has V3_SWAP_EXACT_IN's layout, with amountOut and
|
||||
// amountInMax in place of amountIn and amountOutMin, and a path
|
||||
// that lists the token bought first.
|
||||
kind: "V3_SWAP_EXACT_OUT",
|
||||
data: (r) =>
|
||||
execute([
|
||||
0x01,
|
||||
v3SwapExactIn(r, ONE_WETH, USDT_2000, WETH, USDT)[1],
|
||||
]),
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "V4_SWAP with TAKE",
|
||||
data: (r) =>
|
||||
@@ -2019,10 +2031,6 @@ describe("a payout that cannot be read is never said to be received", () => {
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "V3_SWAP_EXACT_OUT",
|
||||
step: [0x01, swapToWeth(OTHER)[1]],
|
||||
},
|
||||
{
|
||||
kind: "EXECUTE_SUB_PLAN",
|
||||
step: [
|
||||
|
||||
Reference in New Issue
Block a user