fix: an allowance to another spender leaves no Amount figure for its token and no Min. received (closes #658)
Code that a step runs, such as a V4 pool's hook, can call a spender other than the router, which can then take up to its allowance from the user's wallet before the transaction ends. The Amount figure and Min. received took no account of it. In decode(), an allowance of Token In to a spender other than the router now leaves no Amount line, as an unread permit already did, so the swap steps' own takes get lines of their own. Any allowance to another spender, or a permit or batch the decoder cannot read, makes the minimum read Min. paid out. Permits to the router read as before. Model: opus-5-5
This commit was merged in pull request #666.
This commit is contained in:
+162
-59
@@ -2108,12 +2108,6 @@ describe("uniswap decoder", () => {
|
||||
const ONE_USDT_LINES = (spender) =>
|
||||
permitLines("1.0000 USDT", USDT_ADDR, "USDT", spender);
|
||||
test.each([
|
||||
{
|
||||
kind: "to another spender",
|
||||
commands: [0x0a],
|
||||
permits: [ONE_USDT_PERMIT(OTHER_ADDR)],
|
||||
lines: ONE_USDT_LINES(OTHER_ADDR),
|
||||
},
|
||||
{
|
||||
kind: "that the router lets fail",
|
||||
commands: [0x8a],
|
||||
@@ -2550,20 +2544,6 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: USDT_600_LINES,
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to another spender, then a V3 exact-in of 600 USDT,",
|
||||
commands: [0x0a, 0x00],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, USDT_5000, OTHER_ADDR),
|
||||
V3_600,
|
||||
],
|
||||
lines: permitLines(
|
||||
"5000.0000 USDT",
|
||||
USDT_ADDR,
|
||||
"USDT",
|
||||
OTHER_ADDR,
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router that the router may let fail, then a V3 exact-in of 600 USDT,",
|
||||
commands: [0x8a, 0x00],
|
||||
@@ -2677,45 +2657,41 @@ describe("uniswap decoder", () => {
|
||||
const DAI_5000 = 5000000000000000000000n;
|
||||
const MAX_UINT160 = 2n ** 160n - 1n;
|
||||
|
||||
// A batch names one spender for all its allowances.
|
||||
test.each([
|
||||
{ kind: "the router", spender: ROUTER_ADDR },
|
||||
{ kind: "another spender", spender: OTHER_ADDR },
|
||||
])(
|
||||
"a PERMIT2_PERMIT_BATCH of 5,000 USDT and unlimited DAI to $kind, then a V3 exact-in of 600 USDT, shows each allowance",
|
||||
({ spender }) => {
|
||||
const result = decodeSteps(
|
||||
[0x03, 0x00],
|
||||
[
|
||||
encodePermit2Batch(
|
||||
[
|
||||
[USDT_ADDR, USDT_5000],
|
||||
[DAI_ADDR, MAX_UINT160],
|
||||
],
|
||||
spender,
|
||||
),
|
||||
V3_600,
|
||||
],
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "600.0000 USDT"],
|
||||
...permitLines(
|
||||
"5000.0000 USDT",
|
||||
USDT_ADDR,
|
||||
"USDT",
|
||||
spender,
|
||||
// A batch names one spender for all its allowances. One to
|
||||
// another spender is under "an allowance to a spender other than
|
||||
// the router" below.
|
||||
test("a PERMIT2_PERMIT_BATCH of 5,000 USDT and unlimited DAI to the router, then a V3 exact-in of 600 USDT, shows each allowance", () => {
|
||||
const result = decodeSteps(
|
||||
[0x03, 0x00],
|
||||
[
|
||||
encodePermit2Batch(
|
||||
[
|
||||
[USDT_ADDR, USDT_5000],
|
||||
[DAI_ADDR, MAX_UINT160],
|
||||
],
|
||||
ROUTER_ADDR,
|
||||
),
|
||||
...permitLines("Unlimited", DAI_ADDR, "DAI", spender),
|
||||
]);
|
||||
// A batch pays nobody, so the swap's minimum is what the
|
||||
// sender receives.
|
||||
expect(detail(result, "Paid to")).toBeUndefined();
|
||||
expect(detail(result, "Min. received").value).toBe(
|
||||
"590.0000 USDC",
|
||||
);
|
||||
},
|
||||
);
|
||||
V3_600,
|
||||
],
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "600.0000 USDT"],
|
||||
...permitLines(
|
||||
"5000.0000 USDT",
|
||||
USDT_ADDR,
|
||||
"USDT",
|
||||
ROUTER_ADDR,
|
||||
),
|
||||
...permitLines("Unlimited", DAI_ADDR, "DAI", ROUTER_ADDR),
|
||||
]);
|
||||
// A batch pays nobody, so the swap's minimum is what the
|
||||
// sender receives.
|
||||
expect(detail(result, "Paid to")).toBeUndefined();
|
||||
expect(detail(result, "Min. received").value).toBe(
|
||||
"590.0000 USDC",
|
||||
);
|
||||
});
|
||||
|
||||
// A batch keeps a PERMIT2_PERMIT's amount from standing as the
|
||||
// Amount figure, before the permit as after it.
|
||||
@@ -2768,7 +2744,7 @@ describe("uniswap decoder", () => {
|
||||
),
|
||||
},
|
||||
])(
|
||||
"$kind that cannot be read, then a V3 exact-in of 600 USDT, says its allowances could not be decoded and shows no Amount figure",
|
||||
"$kind that cannot be read, then a V3 exact-in of 600 USDT, says its allowances could not be decoded and shows no Amount figure and no Min. received",
|
||||
({ command, input }) => {
|
||||
const result = decodeSteps(
|
||||
[command, 0x00],
|
||||
@@ -2778,6 +2754,14 @@ describe("uniswap decoder", () => {
|
||||
...USDT_600_GROUP,
|
||||
["Permit2 allowance", UNDECODED],
|
||||
]);
|
||||
// Its spender may be any address, as with an allowance
|
||||
// to a spender other than the router (below).
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/658 — on
|
||||
// next at e296546 this read Min. received.
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out").value).toBe(
|
||||
"590.0000 USDC",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2806,6 +2790,125 @@ describe("uniswap decoder", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/658 — code that a step
|
||||
// runs can call a spender other than the router, which can then take
|
||||
// up to its allowance from the sender before the transaction ends.
|
||||
// On next at e296546 the Amount figure and Min. received took no
|
||||
// account of it. An allowance of Token In to another spender now
|
||||
// leaves no Amount figure, the swap steps' own takes get lines of
|
||||
// their own, and any allowance to another spender leaves no Min.
|
||||
// received.
|
||||
describe("an allowance to a spender other than the router", () => {
|
||||
const USDT_5000 = 5000000000n;
|
||||
const DAI_5000 = 5000000000000000000000n;
|
||||
const MAX_UINT160 = 2n ** 160n - 1n;
|
||||
const USDT_5000_LINES = permitLines(
|
||||
"5000.0000 USDT",
|
||||
USDT_ADDR,
|
||||
"USDT",
|
||||
OTHER_ADDR,
|
||||
);
|
||||
|
||||
test.each([
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to another spender, then a V3 exact-in of 600 USDT,",
|
||||
commands: [0x0a, 0x00],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, USDT_5000, OTHER_ADDR),
|
||||
V3_600,
|
||||
],
|
||||
lines: [...USDT_600_GROUP, ...USDT_5000_LINES],
|
||||
minimum: "590.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a PERMIT2_PERMIT of 5,000 USDT to another spender,",
|
||||
commands: [0x00, 0x0a],
|
||||
inputs: [
|
||||
V3_600,
|
||||
encodePermit2(USDT_ADDR, USDT_5000, OTHER_ADDR),
|
||||
],
|
||||
lines: [...USDT_600_GROUP, ...USDT_5000_LINES],
|
||||
minimum: "590.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT_BATCH of 5,000 USDT and unlimited DAI to another spender, then a V3 exact-in of 600 USDT,",
|
||||
commands: [0x03, 0x00],
|
||||
inputs: [
|
||||
encodePermit2Batch(
|
||||
[
|
||||
[USDT_ADDR, USDT_5000],
|
||||
[DAI_ADDR, MAX_UINT160],
|
||||
],
|
||||
OTHER_ADDR,
|
||||
),
|
||||
V3_600,
|
||||
],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
...USDT_5000_LINES,
|
||||
...permitLines(
|
||||
"Unlimited",
|
||||
DAI_ADDR,
|
||||
"DAI",
|
||||
OTHER_ADDR,
|
||||
),
|
||||
],
|
||||
minimum: "590.0000 USDC",
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 1 USDT to another spender, then a V3 exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user,",
|
||||
commands: [0x0a, 0x00, 0x08],
|
||||
inputs: [
|
||||
ONE_USDT_PERMIT(OTHER_ADDR),
|
||||
V3_600,
|
||||
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
...USDT_400_GROUP,
|
||||
...ONE_USDT_LINES(OTHER_ADDR),
|
||||
],
|
||||
// The last swap step's minimum.
|
||||
minimum: "390.0000 USDC",
|
||||
},
|
||||
])(
|
||||
"$kind shows each take's own lines and the allowances, and no Amount figure or Min. received",
|
||||
({ commands, inputs, lines, minimum }) => {
|
||||
const result = decodeSteps(commands, inputs);
|
||||
expect(result.name).toBe("Swap USDT → USDC");
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
expect(detail(result, "Paid to")).toBeUndefined();
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out").value).toBe(minimum);
|
||||
},
|
||||
);
|
||||
|
||||
// An allowance of another token leaves the Amount figure for
|
||||
// Token In.
|
||||
test("a PERMIT2_PERMIT of 5,000 DAI to another spender, then a V3 exact-in of 600 USDT, shows the swap's 600 USDT and no Min. received", () => {
|
||||
const result = decodeSteps(
|
||||
[0x0a, 0x00],
|
||||
[encodePermit2(DAI_ADDR, DAI_5000, OTHER_ADDR), V3_600],
|
||||
);
|
||||
expect(linesBetweenTokens(result)).toEqual([
|
||||
["Amount", "600.0000 USDT"],
|
||||
...permitLines(
|
||||
"5000.0000 DAI",
|
||||
DAI_ADDR,
|
||||
"DAI",
|
||||
OTHER_ADDR,
|
||||
),
|
||||
]);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out").value).toBe(
|
||||
"590.0000 USDC",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -2802,26 +2802,37 @@ describe("the allow-revert flag", () => {
|
||||
});
|
||||
|
||||
// Only the routers from UniversalRouterV2 on let a PERMIT2_PERMIT fail.
|
||||
// The permit's spender is the router at ROUTER, which on every other
|
||||
// router is an allowance to another spender, so the minimum reads
|
||||
// Min. paid out there.
|
||||
test.each([
|
||||
{ router: "UniversalRouterV1", address: ROUTER_V1, mark: "" },
|
||||
{
|
||||
router: "UniversalRouterV1",
|
||||
address: ROUTER_V1,
|
||||
mark: "",
|
||||
minimum: "Min. paid out",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV1_2_V2Support",
|
||||
address: ROUTER_V1_2,
|
||||
mark: "",
|
||||
minimum: "Min. paid out",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2",
|
||||
address: ROUTER,
|
||||
mark: " (allowed to fail)",
|
||||
minimum: "Min. received",
|
||||
},
|
||||
{
|
||||
router: "UniversalRouterV2_1_2",
|
||||
address: ROUTER_V2_1_2,
|
||||
mark: " (allowed to fail)",
|
||||
minimum: "Min. paid out",
|
||||
},
|
||||
])(
|
||||
"on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark', and the swap's lines read as without the flag",
|
||||
({ address, mark }) => {
|
||||
({ address, mark, minimum }) => {
|
||||
const steps = [
|
||||
permit2Permit(USDT, USDT_2000),
|
||||
swapToWeth(MSG_SENDER),
|
||||
@@ -2853,7 +2864,7 @@ describe("the allow-revert flag", () => {
|
||||
expect(
|
||||
labels(withFlag).filter((l) => permitLines.includes(l)),
|
||||
).toEqual(permitLines);
|
||||
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
|
||||
expect(detail(withFlag, minimum).value).toBe("1.0000 WETH");
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user