fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
The swap's Amount line took only the first swap step's amount. It now gives the total the steps take of Token In: the amount of the step that sets the input side plus what each later step the user pays takes of it. A take of another token, one after an amount that is not a quantity, or one the decoder cannot tell, gets lines of its own; an unread take, a V3_SWAP_EXACT_OUT's included, leaves no figure. A V4 step counts every SETTLE paid by the user and every SETTLE_ALL. A PERMIT2_PERMIT names Token In, and its amount stands only in the plain shape of Uniswap's own routes, with every later step read. Model: opus-5-5
This commit is contained in:
@@ -1289,6 +1289,722 @@ describe("uniswap decoder", () => {
|
||||
expect(detail(result, "Amount").value).not.toContain("500000000000");
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/625 — a route split across
|
||||
// two swap steps the user pays for, with no PERMIT2_PERMIT, takes both
|
||||
// steps' amounts from the wallet. On next at 671b4d3 the Amount line read
|
||||
// only the first step's 600.0000 USDT.
|
||||
describe("a swap split across steps the user pays for", () => {
|
||||
const USDT_600 = 600000000n;
|
||||
const USDT_400 = 400000000n;
|
||||
const DAI_400 = 400000000000000000000n;
|
||||
const OTHER_ADDR = "0x1111111111111111111111111111111111111111";
|
||||
const UNDECODED = "Unknown (could not be decoded)";
|
||||
|
||||
// The lines between Token In and Token Out, as [label, value].
|
||||
function linesBetweenTokens(result) {
|
||||
const lines = result.details.map((d) => [d.label, d.value]);
|
||||
const from = lines.findIndex(([label]) => label === "Token In");
|
||||
const to = lines.findIndex(([label]) => label === "Token Out");
|
||||
return lines.slice(from + 1, to);
|
||||
}
|
||||
|
||||
// execute() of these command bytes and inputs, decoded as sent by
|
||||
// the user.
|
||||
function decodeSteps(commands, inputs) {
|
||||
const data = buildExecute(
|
||||
solidityPacked(
|
||||
commands.map(() => "uint8"),
|
||||
commands,
|
||||
),
|
||||
inputs,
|
||||
9999999999n,
|
||||
);
|
||||
return uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
|
||||
}
|
||||
|
||||
// The first step: a V3 exact-in of 600 USDT for USDC, paid by the
|
||||
// user, which sets the input side.
|
||||
const V3_600 = encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]);
|
||||
// V4 SETTLE of `amount` of `currency`, and TAKE of `amount` of
|
||||
// `currency` to `recipient`.
|
||||
const settle = (currency, amount, payerIsUser) =>
|
||||
coder.encode(
|
||||
["address", "uint256", "bool"],
|
||||
[currency, amount, payerIsUser],
|
||||
);
|
||||
const take = (currency, recipient, amount) =>
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[currency, recipient, amount],
|
||||
);
|
||||
// `input` with the top byte of its word at `index` set. This decoder
|
||||
// cannot read such a word as an address or a length; the router reads
|
||||
// its low bytes and runs the step.
|
||||
const dirtyWord = (input, index) => {
|
||||
const at = 2 + index * 64;
|
||||
return input.slice(0, at) + "ff" + input.slice(at + 2);
|
||||
};
|
||||
|
||||
test("a split across a V3 and a V2 step the user pays for shows the total", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Token In").address.toLowerCase()).toBe(
|
||||
USDT_ADDR.toLowerCase(),
|
||||
);
|
||||
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||
expect(detail(result, "Amount").rawValue).toBe("1000.0000");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
// In the order of Uniswap's own V4 routes: the swap, then a SETTLE of
|
||||
// OPEN_DELTA by the user, which pays what the swap spends.
|
||||
test("a split across a V3 step and a V4 step the user pays for shows the total", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
USDT_400,
|
||||
390000000n,
|
||||
),
|
||||
encodeV4Settle(USDT_ADDR), // paid by the user
|
||||
encodeV4Take(USDC_ADDR),
|
||||
],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("a second step the user pays for that spends another token shows that token's amount", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(
|
||||
USER_ADDR,
|
||||
400000000000000000000n, // 400 DAI
|
||||
390000000n,
|
||||
[DAI_ADDR, USDC_ADDR],
|
||||
),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "600.0000 USDT"],
|
||||
["Moved from the sender", "400.0000 DAI"],
|
||||
["Token moved", "DAI (" + DAI_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
]);
|
||||
});
|
||||
|
||||
// Every later V4 step takes from the sender what its SETTLEs paid by
|
||||
// the user and its SETTLE_ALLs pay, each currency on its own, with a
|
||||
// swap action or without. Each of these read 600.0000 USDT alone
|
||||
// before.
|
||||
const USDT_FOR_USDC_400 = encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
USDT_400,
|
||||
390000000n,
|
||||
);
|
||||
const DAI_FOR_USDC_400 = encodeV4ExactInSingle(
|
||||
DAI_ADDR,
|
||||
USDC_ADDR,
|
||||
DAI_400,
|
||||
390000000n,
|
||||
);
|
||||
const DAI_GROUP = [
|
||||
["Moved from the sender", "400.0000 DAI"],
|
||||
["Token moved", "DAI (" + DAI_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
];
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4 step paying USDT and DAI by a SETTLE_ALL of each",
|
||||
actions: [
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
USDT_FOR_USDC_400,
|
||||
DAI_FOR_USDC_400,
|
||||
encodeV4SettleAll(USDT_ADDR, USDT_400),
|
||||
encodeV4SettleAll(DAI_ADDR, DAI_400),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
lines: [["Amount", "1000.0000 USDT"], ...DAI_GROUP],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step with a SETTLE of USDT by the user, then a SETTLE of WETH from the router",
|
||||
actions: [
|
||||
V4_SETTLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
settle(USDT_ADDR, USDT_400, true),
|
||||
settle(WETH_ADDR, 2n ** 255n, false), // CONTRACT_BALANCE
|
||||
USDT_FOR_USDC_400,
|
||||
encodeV4ExactInSingle(WETH_ADDR, USDC_ADDR, 0n, 1n),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
lines: [["Amount", "1000.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step with no swap action: a SETTLE of 400 USDT by the user and a TAKE of it to another address",
|
||||
actions: [V4_SETTLE_ID, V4_TAKE_ID],
|
||||
params: [
|
||||
settle(USDT_ADDR, USDT_400, true),
|
||||
take(USDT_ADDR, OTHER_ADDR, USDT_400),
|
||||
],
|
||||
lines: [["Amount", "1000.0000 USDT"]],
|
||||
},
|
||||
// The SETTLE's amount, not the swap's, is what the user pays.
|
||||
{
|
||||
kind: "a V4 step swapping 400 USDT with a SETTLE of 5,000 USDT by the user, the rest taken to another address",
|
||||
actions: [
|
||||
V4_SETTLE_ID,
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_TAKE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
settle(USDT_ADDR, 5000000000n, true),
|
||||
USDT_FOR_USDC_400,
|
||||
take(USDT_ADDR, OTHER_ADDR, 0n),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
lines: [["Amount", "5600.0000 USDT"]],
|
||||
},
|
||||
// A SETTLE_ALL pays all the step owes, here what the TAKE takes as
|
||||
// well as what the swap spends, so its amount is not known.
|
||||
{
|
||||
kind: "a V4 step with no swap action: a TAKE of 400 USDT to another address, then a SETTLE_ALL of USDT",
|
||||
actions: [V4_TAKE_ID, V4_SETTLE_ALL_ID],
|
||||
params: [
|
||||
take(USDT_ADDR, OTHER_ADDR, USDT_400),
|
||||
encodeV4SettleAll(USDT_ADDR, USDT_400),
|
||||
],
|
||||
lines: [
|
||||
["Moved from the sender", UNDECODED],
|
||||
["Token moved", "USDT (" + USDT_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step swapping 400 USDT, with a TAKE of 400 USDT to another address before its SETTLE_ALL of USDT",
|
||||
actions: [
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_TAKE_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
USDT_FOR_USDC_400,
|
||||
take(USDT_ADDR, OTHER_ADDR, USDT_400),
|
||||
encodeV4SettleAll(USDT_ADDR, 800000000n),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
lines: [
|
||||
["Moved from the sender", UNDECODED],
|
||||
["Token moved", "USDT (" + USDT_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
],
|
||||
},
|
||||
])(
|
||||
"a V3 step of 600 USDT, then $kind",
|
||||
({ actions, params, lines }) => {
|
||||
const result = decodeSteps(
|
||||
[0x00, 0x10],
|
||||
[V3_600, encodeV4Swap(new Uint8Array(actions), params)],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
},
|
||||
);
|
||||
|
||||
// A step that may take from the sender what the decoder did not read
|
||||
// leaves no Amount figure, which may be too low, and one line says so.
|
||||
const UNREAD_SWAP = V4_SWAP_ACTION_SHAPES[0].newer;
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4 step whose SETTLE by the user cannot be read",
|
||||
actions: [
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
USDT_FOR_USDC_400,
|
||||
dirtyWord(settle(USDT_ADDR, USDT_400, true), 0),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step whose SETTLE_ALL cannot be read",
|
||||
actions: [
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
USDT_FOR_USDC_400,
|
||||
dirtyWord(encodeV4SettleAll(USDT_ADDR, USDT_400), 0),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step whose swap action and SETTLE_ALL cannot be read",
|
||||
actions: [
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ALL_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
],
|
||||
params: [
|
||||
UNREAD_SWAP,
|
||||
dirtyWord(encodeV4SettleAll(WETH_ADDR, ONE_WETH), 0),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
},
|
||||
])("a V3 step of 600 USDT, then $kind", ({ actions, params }) => {
|
||||
const result = decodeSteps(
|
||||
[0x00, 0x10],
|
||||
[V3_600, encodeV4Swap(new Uint8Array(actions), params)],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Moved from the sender", UNDECODED],
|
||||
]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 step of 600 USDT, then a V4 step whose input cannot be read",
|
||||
commands: [0x00, 0x10],
|
||||
inputs: [
|
||||
V3_600,
|
||||
dirtyWord(
|
||||
encodeV4Swap(new Uint8Array([V4_SETTLE_ID]), [
|
||||
settle(USDT_ADDR, USDT_400, true),
|
||||
]),
|
||||
2,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V4 step with no swap action whose SETTLE_ALL cannot be read, then a V3 step of 600 USDT",
|
||||
commands: [0x10, 0x00],
|
||||
inputs: [
|
||||
encodeV4Swap(
|
||||
new Uint8Array([V4_TAKE_ID, V4_SETTLE_ALL_ID]),
|
||||
[
|
||||
take(USDT_ADDR, OTHER_ADDR, USDT_400),
|
||||
dirtyWord(
|
||||
encodeV4SettleAll(USDT_ADDR, USDT_400),
|
||||
0,
|
||||
),
|
||||
],
|
||||
),
|
||||
V3_600,
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 step of 600 USDT, then a V2 step the user pays for whose input cannot be read",
|
||||
commands: [0x00, 0x08],
|
||||
inputs: [
|
||||
V3_600,
|
||||
dirtyWord(
|
||||
encodeV2SwapExactIn(OTHER_ADDR, USDT_400, 1n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
0,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 step of 600 USDT, then a PERMIT2_TRANSFER_FROM whose input cannot be read",
|
||||
commands: [0x00, 0x02],
|
||||
inputs: [
|
||||
V3_600,
|
||||
dirtyWord(
|
||||
coder.encode(
|
||||
["address", "address", "uint160"],
|
||||
[USDT_ADDR, OTHER_ADDR, USDT_400],
|
||||
),
|
||||
1,
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 step of 600 USDT, then a PERMIT2_TRANSFER_FROM_BATCH",
|
||||
commands: [0x00, 0x0d],
|
||||
inputs: [
|
||||
V3_600,
|
||||
coder.encode(
|
||||
["tuple(address,address,uint160,address)[]"],
|
||||
[[[USER_ADDR, OTHER_ADDR, USDT_400, USDT_ADDR]]],
|
||||
),
|
||||
],
|
||||
},
|
||||
])("$kind", ({ commands, inputs }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Moved from the sender", UNDECODED],
|
||||
]);
|
||||
});
|
||||
|
||||
// A PERMIT2_PERMIT in the plain shape of Uniswap's own routes keeps
|
||||
// its own amount: Permit2 lets the router take no more of the token,
|
||||
// so the steps after it do not add to it.
|
||||
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
|
||||
[
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), // 5000 USDT
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("5000.0000 USDT");
|
||||
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||
});
|
||||
|
||||
// Out of that shape the permit caps nothing the Amount line can rely
|
||||
// on, so the line gives the total the steps take, as with no permit.
|
||||
const ONE_USDT_PERMIT = (spender) =>
|
||||
encodePermit2(USDT_ADDR, 1000000n, spender);
|
||||
test.each([
|
||||
{
|
||||
kind: "to another spender",
|
||||
commands: [0x0a],
|
||||
permits: [ONE_USDT_PERMIT(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "that the router lets fail",
|
||||
commands: [0x8a],
|
||||
permits: [ONE_USDT_PERMIT(ROUTER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "followed by a second one",
|
||||
commands: [0x0a, 0x0a],
|
||||
permits: [
|
||||
ONE_USDT_PERMIT(ROUTER_ADDR),
|
||||
ONE_USDT_PERMIT(ROUTER_ADDR),
|
||||
],
|
||||
},
|
||||
])(
|
||||
"a split after a PERMIT2_PERMIT $kind shows the total the steps take",
|
||||
({ commands, permits }) => {
|
||||
const result = decodeSteps(
|
||||
[...commands, 0x00, 0x08],
|
||||
[
|
||||
...permits,
|
||||
V3_600,
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "1000.0000 USDT"],
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
// A PERMIT2_PERMIT_BATCH, which this decoder does not read, may raise
|
||||
// what the router may take of the permit's token again.
|
||||
test("a split after a PERMIT2_PERMIT and a PERMIT2_PERMIT_BATCH shows the total the steps take", () => {
|
||||
const permitBatch = coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,uint160,uint48,uint48)[],address,uint256)",
|
||||
"bytes",
|
||||
],
|
||||
[
|
||||
[
|
||||
[[USDT_ADDR, 10000000000n, 0, 0]], // 10,000 USDT
|
||||
ROUTER_ADDR,
|
||||
9999999999,
|
||||
],
|
||||
"0x1234",
|
||||
],
|
||||
);
|
||||
const result = decodeSteps(
|
||||
[0x0a, 0x03, 0x00, 0x08],
|
||||
[
|
||||
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||
permitBatch,
|
||||
V3_600,
|
||||
encodeV2SwapExactIn(USER_ADDR, 9400000000n, 1n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "10000.0000 USDT"],
|
||||
]);
|
||||
});
|
||||
|
||||
// Any other step this decoder does not read may do the same, for
|
||||
// example through the hook of the pool a V4_INITIALIZE_POOL sets up.
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4_INITIALIZE_POOL",
|
||||
command: 0x13,
|
||||
input: coder.encode(
|
||||
[POOL_KEY, "uint160"],
|
||||
[
|
||||
[USDC_ADDR, USDT_ADDR, 3000, 60, OTHER_ADDR],
|
||||
79228162514264337593543950336n,
|
||||
],
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "a SWEEP whose input cannot be read",
|
||||
command: 0x04,
|
||||
input: dirtyWord(
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[USDC_ADDR, USER_ADDR, 0n],
|
||||
),
|
||||
1,
|
||||
),
|
||||
},
|
||||
])(
|
||||
"a split after a PERMIT2_PERMIT, with $kind between its steps, shows the total the steps take",
|
||||
({ command, input }) => {
|
||||
const result = decodeSteps(
|
||||
[0x0a, 0x00, command, 0x08],
|
||||
[
|
||||
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||
V3_600,
|
||||
input,
|
||||
encodeV2SwapExactIn(USER_ADDR, 9400000000n, 1n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "10000.0000 USDT"],
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
// This decoder does not read a V3_SWAP_EXACT_OUT, so what one takes
|
||||
// after the step that sets the input side leaves no Amount figure.
|
||||
test("a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT the user pays for, shows no Amount figure", () => {
|
||||
// Its input is (recipient, amountOut, amountInMax, path,
|
||||
// payerIsUser), laid out as encodeV3SwapExactIn() writes one; an
|
||||
// exact-out path lists the token bought first.
|
||||
const exactOut = encodeV3SwapExactIn(
|
||||
USER_ADDR,
|
||||
390000000n,
|
||||
USDT_400,
|
||||
[USDC_ADDR, USDT_ADDR],
|
||||
);
|
||||
const result = decodeSteps([0x00, 0x01], [V3_600, exactOut]);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Moved from the sender", 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.
|
||||
const V2_EXACT_OUT_400 = encodeV2SwapExactOut(
|
||||
USER_ADDR,
|
||||
390000000n,
|
||||
USDT_400,
|
||||
[USDT_ADDR, USDC_ADDR],
|
||||
);
|
||||
const UP_TO_400_USDT = [
|
||||
["Moved from the sender", "Up to 400.0000 USDT"],
|
||||
["Token moved", "USDT (" + USDT_ADDR + ")"],
|
||||
["Moved to", "The swap step that spends it"],
|
||||
];
|
||||
test.each([
|
||||
{
|
||||
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
|
||||
commands: [0x10, 0x09],
|
||||
first: encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactOutSingle(
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
ONE_WETH,
|
||||
USDT_600,
|
||||
),
|
||||
encodeV4Settle(USDT_ADDR),
|
||||
encodeV4TakeAll(WETH_ADDR, 0n),
|
||||
],
|
||||
),
|
||||
lines: UP_TO_400_USDT,
|
||||
},
|
||||
{
|
||||
kind: "a V4 exact-in step of the open delta",
|
||||
commands: [0x10, 0x09],
|
||||
first: encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ALL_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(USDT_ADDR, USDC_ADDR, 0n, 1n),
|
||||
encodeV4Settle(USDT_ADDR),
|
||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||
],
|
||||
),
|
||||
lines: [
|
||||
["Amount", "All available (V4 open delta)"],
|
||||
...UP_TO_400_USDT,
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V2 exact-in step of ALREADY_PAID",
|
||||
commands: [0x08, 0x09],
|
||||
first: encodeV2SwapExactIn(USER_ADDR, 0n, 1n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
lines: [
|
||||
[
|
||||
"Amount",
|
||||
"Whatever an earlier step sent to the pair (V2 already paid)",
|
||||
],
|
||||
...UP_TO_400_USDT,
|
||||
],
|
||||
},
|
||||
])(
|
||||
"$kind, then a V2 exact-out of at most 400 USDT the user pays for, shows the 400 USDT",
|
||||
({ commands, first, lines }) => {
|
||||
const result = decodeSteps(commands, [first, V2_EXACT_OUT_400]);
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
},
|
||||
);
|
||||
|
||||
// A V2 exact-out step makes every amount the swap steps spend a maximum,
|
||||
// the total included.
|
||||
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x00, 0x09]),
|
||||
[
|
||||
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeV2SwapExactOut(USER_ADDR, 390000000n, USDT_400, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(
|
||||
data,
|
||||
ROUTER_ADDR,
|
||||
"0x1",
|
||||
{},
|
||||
USER_ADDR,
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
||||
// the shape the issue measured: a V4 step that states a minimum of zero
|
||||
// and names no output currency.
|
||||
|
||||
Reference in New Issue
Block a user