fix: a permit shows on its own lines unless its amount is the Amount figure (closes #370)
A PERMIT2_PERMIT read after a step had set the input side named Token In, and its amount could stand as the Amount figure, so the approval screen could show an allowance where the swap's own input belonged. A permit before the swap steps whose spender was not the router, or that the router may let fail, had no line naming its spender. A permit after the input side now leaves Token In and Amount to the swap. Every permit whose amount does not stand as the Amount figure gets three lines of its own: Permit2 allowance, Allowance token and Allowance spender. README Transaction Decoding and the TxApproval element list match. Model: opus-5-5
This commit is contained in:
@@ -2422,10 +2422,11 @@ view would leave a wallet one click from deletion.
|
|||||||
transaction"
|
transaction"
|
||||||
- Decoded action (if calldata is recognized): action name, token details,
|
- Decoded action (if calldata is recognized): action name, token details,
|
||||||
amounts, what each swap step spends that the `Amount` line does not give,
|
amounts, what each swap step spends that the `Amount` line does not give,
|
||||||
what each `PERMIT2_TRANSFER_FROM` moves and where to, steps, deadline, and
|
what each `PERMIT2_TRANSFER_FROM` moves and where to, what each
|
||||||
unless a swap pays only the From address, a `Paid to` line for each other
|
`PERMIT2_PERMIT` whose amount is not the `Amount` figure lets its spender
|
||||||
address it pays, or saying that a token stays in the router or who it pays
|
take, steps, deadline, and unless a swap pays only the From address, a
|
||||||
is unknown (see Transaction Decoding)
|
`Paid to` line for each other address it pays, or saying that a token
|
||||||
|
stays in the router or who it pays is unknown (see Transaction Decoding)
|
||||||
- From: color dot + full address + etherscan link
|
- From: color dot + full address + etherscan link
|
||||||
- Contract: color dot + full address + etherscan link, token symbol label if
|
- Contract: color dot + full address + etherscan link, token symbol label if
|
||||||
known; for a contract creation, the same sentence as on WaitTx
|
known; for a contract creation, the same sentence as on WaitTx
|
||||||
@@ -3215,37 +3216,37 @@ Currently supported:
|
|||||||
V2 exact-in of 400 USDT, both paid by the user, read `1000.0000 USDT`. When
|
V2 exact-in of 400 USDT, both paid by the user, read `1000.0000 USDT`. When
|
||||||
the decoder does not know a take's amount, the total is not known and there is
|
the decoder does not know a take's amount, the total is not known and there is
|
||||||
no `Amount` line. `Token In` is the token of the last `PERMIT2_PERMIT` the
|
no `Amount` line. `Token In` is the token of the last `PERMIT2_PERMIT` the
|
||||||
decoder reads, when there is one. Its amount, `Unlimited` included, stands as
|
decoder reads before the step that sets the input side, when there is one. Its
|
||||||
the `Amount` figure in place of the total only in the plain shape of Uniswap's
|
amount, `Unlimited` included, stands as the `Amount` figure in place of the
|
||||||
own routes: it is the only `PERMIT2_PERMIT`, its spender is the router, the
|
total only in the plain shape of Uniswap's own routes: it is the only
|
||||||
router may not let it fail, no step before it takes its token or a token the
|
`PERMIT2_PERMIT` in the transaction, its spender is the router, the router may
|
||||||
decoder does not know, and the decoder reads every step after it in full.
|
not let it fail, no step before it takes its token or a token the decoder does
|
||||||
Permit2 then lets the router take no more of the token than that amount, so
|
not know, and the decoder reads every step after it in full. Permit2 then lets
|
||||||
the permit covers every take of it. A step the decoder does not read in full
|
the router take no more of the token than that amount, so the permit covers
|
||||||
may raise what Permit2 lets the router take, as a `PERMIT2_PERMIT_BATCH` can:
|
every take of it. A step the decoder does not read in full may raise what
|
||||||
a command the decoder does not read (listed at the end of this section), a
|
Permit2 lets the router take, as a `PERMIT2_PERMIT_BATCH` can: a command the
|
||||||
step whose input it cannot read, in whole or in part, and a step it fails on.
|
decoder does not read (listed at the end of this section), a step whose input
|
||||||
In any other case the line gives the total, as with no permit. When the step
|
it cannot read, in whole or in part, and a step it fails on. In any other case
|
||||||
that sets the input side spends a token other than the permit's, or one the
|
the line gives the total, as with no permit. When the step that sets the input
|
||||||
decoder does not know, its amount gets lines of its own, unless it states no
|
side spends a token other than the permit's, or one the decoder does not know,
|
||||||
amount and the user pays it none of its input token, and the total has no
|
its amount gets lines of its own, unless it states no amount and the user pays
|
||||||
figure to start from: unless the permit's amount stands there is no `Amount`
|
it none of its input token, and the total has no figure to start from: unless
|
||||||
line, and each take of the permit's token gets lines of its own. Each take the
|
the permit's amount stands there is no `Amount` line, and each take of the
|
||||||
total does not count gives three lines after the `Amount` line (after
|
permit's token gets lines of its own. Each take the total does not count gives
|
||||||
`Token In` when there is none): one of a token other than `Token In`; one of
|
three lines after the `Amount` line (after `Token In` when there is none): one
|
||||||
`Token In` after an amount that is not a quantity (a V4 open delta,
|
of a token other than `Token In`; one of `Token In` after an amount that is
|
||||||
`ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated
|
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
|
||||||
amount, as of a V4 exact-out step); and one whose amount the decoder does not
|
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
|
||||||
know. When the step that sets the input side states no amount, the user pays
|
whose amount the decoder does not know. When the step that sets the input side
|
||||||
it some of its input token, and there is no `Amount` line, that step gets them
|
states no amount, the user pays it some of its input token, and there is no
|
||||||
too whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM`
|
`Amount` line, that step gets them too whenever another take's lines give a
|
||||||
of its token gives one, on the lines below or, into the router, on its
|
figure, or a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines
|
||||||
`Paid to` line, so that figure cannot read as all that leaves. When it states
|
below or, into the router, on its `Paid to` line, so that figure cannot read
|
||||||
no amount and the user pays it none of its input token, as when the router
|
as all that leaves. When it states no amount and the user pays it none of its
|
||||||
pays it, that step takes none of that token from the user's wallet and never
|
input token, as when the router pays it, that step takes none of that token
|
||||||
gets them, with a permit or without. They come in step order, the step that
|
from the user's wallet and never gets them, with a permit or without. They
|
||||||
sets the input side first when it has them: `Moved from the sender`, its
|
come in step order, the step that sets the input side first when it has them:
|
||||||
amount, worded as the `Amount` line words one, or
|
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
|
||||||
`Unknown (could not be decoded)` when the decoder does not know it;
|
`Unknown (could not be decoded)` when the decoder does not know it;
|
||||||
`Token moved`, giving the token's address as the `Token In` line does; and
|
`Token moved`, giving the token's address as the `Token In` line does; and
|
||||||
`Moved to`, which reads `The swap step that spends it`. A token the decoder
|
`Moved to`, which reads `The swap step that spends it`. A token the decoder
|
||||||
@@ -3268,7 +3269,20 @@ Currently supported:
|
|||||||
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
||||||
move into the router needs none of them, because its `Paid to` line and
|
move into the router needs none of them, because its `Paid to` line and
|
||||||
`Token moved into the router` line already give the amount, the token and
|
`Token moved into the router` line already give the amount, the token and
|
||||||
where it goes, and so its amount is shown once. These lines change neither the
|
where it goes, and so its amount is shown once. A `PERMIT2_PERMIT` after the
|
||||||
|
step that sets the input side names neither `Token In` nor the `Amount`
|
||||||
|
figure, which keep that step's token and the total: a V3 exact-in of 600 USDT
|
||||||
|
followed by a permit of 5,000 DAI reads `Swap USDT → USDC` with
|
||||||
|
`Amount 600.0000 USDT`. Every `PERMIT2_PERMIT` whose amount does not stand as
|
||||||
|
the `Amount` figure, before the step that sets the input side or after it,
|
||||||
|
gives three lines, in step order, after those for each
|
||||||
|
`PERMIT2_TRANSFER_FROM`: `Permit2 allowance`, its amount, for example
|
||||||
|
`5000.0000 DAI`, or `Unlimited` for Permit2's maximum; `Allowance token`,
|
||||||
|
giving the token's address as the `Token In` line does; and
|
||||||
|
`Allowance spender`, the address it lets take the token. So does one whose
|
||||||
|
spender is not the router, or that the router may let fail. Only a permit
|
||||||
|
whose amount stands gives none: the `Amount` line gives its amount, `Token In`
|
||||||
|
its token, and its spender is the router. These lines change neither the
|
||||||
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
|
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
|
||||||
does not decode as an address, when it is a V3 swap step whose path is shorter
|
does not decode as an address, when it is a V3 swap step whose path is shorter
|
||||||
than one token address or has bytes after its last hop, or when it is a
|
than one token address or has bytes after its last hop, or when it is a
|
||||||
|
|||||||
+69
-26
@@ -494,8 +494,9 @@ function amountText(raw, info) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The line giving a moved token under `label`, in the shape of the Token In
|
// The line giving a moved token, or the token of a permit, under `label`, in
|
||||||
// line, so the screens show it as they show that one. ETH has no address.
|
// the shape of the Token In line, so the screens show it as they show that
|
||||||
|
// one. ETH has no address.
|
||||||
function tokenMovedLine(label, token, sources) {
|
function tokenMovedLine(label, token, sources) {
|
||||||
const info = tokenInfo(token, sources);
|
const info = tokenInfo(token, sources);
|
||||||
if (!present(info.address)) {
|
if (!present(info.address)) {
|
||||||
@@ -1208,9 +1209,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// its input currency (see decodeV4Swap()). Only a V4 step can set the
|
// its input currency (see decodeV4Swap()). Only a V4 step can set the
|
||||||
// input side with no amount, the one case in which this is read.
|
// input side with no amount, the one case in which this is read.
|
||||||
let inputPaidByUser = false;
|
let inputPaidByUser = false;
|
||||||
// The last PERMIT2_PERMIT this decoder read, or null (see
|
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||||
// permitStands after the loop).
|
// input side, or null (see permitStands after the loop).
|
||||||
let permit = null;
|
let permit = null;
|
||||||
|
// Every PERMIT2_PERMIT this decoder read, before a step set the input
|
||||||
|
// side or after, in step order, as { token, amount, spender, step }.
|
||||||
|
// Each whose amount is not the Amount figure gets lines of its own
|
||||||
|
// (see after the PERMIT2_TRANSFER_FROM lines).
|
||||||
|
const permits = [];
|
||||||
// The index of each step this decoder did not read in full: a
|
// The index of each step this decoder did not read in full: a
|
||||||
// command it does not read, a step whose input it cannot read, in
|
// command it does not read, a step whose input it cannot read, in
|
||||||
// whole or in part, and one it fails on (see permitStands).
|
// whole or in part, and one it fails on (see permitStands).
|
||||||
@@ -1268,8 +1274,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// when no earlier hop set it. A half the establishing hop did not
|
// when no earlier hop set it. A half the establishing hop did not
|
||||||
// state stays null and the line says so, rather than being filled in
|
// state stays null and the line says so, rather than being filled in
|
||||||
// from a different hop. A PERMIT2_PERMIT sets no side here: after the
|
// from a different hop. A PERMIT2_PERMIT sets no side here: after the
|
||||||
// loop, the last one read names Token In, and the Amount line is
|
// loop, the last one read before a step set the input side names
|
||||||
// formed there, in one place (see permitStands).
|
// Token In, and the Amount line is formed there, in one place (see
|
||||||
|
// permitStands).
|
||||||
const setInput = (token, amount) => {
|
const setInput = (token, amount) => {
|
||||||
inputToken = present(token) ? token : null;
|
inputToken = present(token) ? token : null;
|
||||||
inputAmount = present(amount) ? amount : null;
|
inputAmount = present(amount) ? amount : null;
|
||||||
@@ -1465,24 +1472,30 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (command === "PERMIT2_PERMIT") {
|
} else if (command === "PERMIT2_PERMIT") {
|
||||||
const p = decodePermit2(inputs[i]);
|
const p = decodePermit2(inputs[i]);
|
||||||
if (p) {
|
if (p) {
|
||||||
|
permits.push({ ...p, step: i });
|
||||||
|
keepsRouterWeth = true;
|
||||||
|
} else {
|
||||||
|
stepsUnread.push(i);
|
||||||
|
}
|
||||||
|
// After a step set the input side, a permit names
|
||||||
|
// neither Token In nor the Amount figure: the swap's own
|
||||||
|
// input token and amount stay on those lines.
|
||||||
|
if (p && !inputEstablished) {
|
||||||
// Whether a step before it took its token, or may
|
// Whether a step before it took its token, or may
|
||||||
// have: the step that set the input side or one in
|
// have: one in `userPaid`, when its token is the
|
||||||
// `userPaid`, when its token is the permit's or not
|
// permit's or not known. No step has set the input
|
||||||
// known.
|
// side yet.
|
||||||
const mayBeItsToken = (token) =>
|
const mayBeItsToken = (token) =>
|
||||||
!present(token) || sameAddress(token, p.token);
|
!present(token) || sameAddress(token, p.token);
|
||||||
const tookBefore =
|
const tookBefore = userPaid.some((t) =>
|
||||||
(inputEstablished && mayBeItsToken(inputToken)) ||
|
mayBeItsToken(t.token),
|
||||||
userPaid.some((t) => mayBeItsToken(t.token));
|
);
|
||||||
permit = {
|
permit = {
|
||||||
...p,
|
...p,
|
||||||
step: i,
|
step: i,
|
||||||
allowedToFail,
|
allowedToFail,
|
||||||
beforeEveryTake: !tookBefore,
|
beforeEveryTake: !tookBefore,
|
||||||
};
|
};
|
||||||
keepsRouterWeth = true;
|
|
||||||
} else {
|
|
||||||
stepsUnread.push(i);
|
|
||||||
}
|
}
|
||||||
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
||||||
const p = decodePermit2TransferFrom(inputs[i]);
|
const p = decodePermit2TransferFrom(inputs[i]);
|
||||||
@@ -1939,18 +1952,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// lines of its own. One whose amount is not known also leaves the
|
// lines of its own. One whose amount is not known also leaves the
|
||||||
// total unknown, with no figure.
|
// total unknown, with no figure.
|
||||||
//
|
//
|
||||||
// The last PERMIT2_PERMIT read names Token In. Its amount stands as
|
// The last PERMIT2_PERMIT read before a step set the input side names
|
||||||
// the Amount figure in place of the total only in the plain shape of
|
// Token In. Its amount stands as the Amount figure in place of the
|
||||||
// Uniswap's own routes: it is the only PERMIT2_PERMIT, its spender is
|
// total only in the plain shape of Uniswap's own routes: it is the
|
||||||
// the router, the router may not let it fail, no step before it took
|
// only PERMIT2_PERMIT in the transaction, its spender is the router,
|
||||||
// its token or one not known, and this decoder read every step after
|
// the router may not let it fail, no step before it took its token or
|
||||||
// it in full (see stepsUnread). Permit2 then lets the router take no
|
// one not known, and this decoder read every step after it in full
|
||||||
// more of the token than that amount, so the permit covers every take
|
// (see stepsUnread). Permit2 then lets the router take no more of the
|
||||||
// of it. A later step this decoder did not read may raise what Permit2
|
// token than that amount, so the permit covers every take of it. A
|
||||||
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step that
|
// later step this decoder did not read may raise what Permit2 lets
|
||||||
// may take from the sender what this decoder did not read leaves no
|
// the router take, as a PERMIT2_PERMIT_BATCH can. A step that may
|
||||||
|
// take from the sender what this decoder did not read leaves no
|
||||||
// figure, permit or not (see spentUnread()), and an EXECUTE_SUB_PLAN
|
// figure, permit or not (see spentUnread()), and an EXECUTE_SUB_PLAN
|
||||||
// step leaves both sides unknown (below).
|
// step leaves both sides unknown (below). A PERMIT2_PERMIT after a
|
||||||
|
// step set the input side counts among the PERMIT2_PERMITs here.
|
||||||
|
// Every PERMIT2_PERMIT but one whose amount stands gets lines of its
|
||||||
|
// own below.
|
||||||
const stepCommands = Array.from(
|
const stepCommands = Array.from(
|
||||||
commandsBytes,
|
commandsBytes,
|
||||||
(b) => commands[b & commandTypeMask],
|
(b) => commands[b & commandTypeMask],
|
||||||
@@ -2181,6 +2198,32 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What each PERMIT2_PERMIT lets its spender take: its amount, its
|
||||||
|
// token and the spender, an address on a line of its own. Permit2's
|
||||||
|
// maximum is unbounded. Only a permit whose amount stands as the
|
||||||
|
// Amount figure (see permitStands) gets none: that line already gives
|
||||||
|
// its amount and Token In its token, and its spender is the router.
|
||||||
|
// Every other permit, one after the step that set the input side
|
||||||
|
// included, gets these lines, so nothing it lets anyone take goes
|
||||||
|
// unstated.
|
||||||
|
for (const p of permits) {
|
||||||
|
if (permitStands && p.step === permit.step) continue;
|
||||||
|
details.push({
|
||||||
|
label: "Permit2 allowance",
|
||||||
|
value:
|
||||||
|
p.amount >= MAX_UINT160
|
||||||
|
? "Unlimited"
|
||||||
|
: amountText(p.amount, tokenInfo(p.token, sources))
|
||||||
|
.display,
|
||||||
|
});
|
||||||
|
details.push(tokenMovedLine("Allowance token", p.token, sources));
|
||||||
|
details.push({
|
||||||
|
label: "Allowance spender",
|
||||||
|
value: p.spender,
|
||||||
|
address: p.spender,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Keyed on the address, not the symbol: a token absent from the
|
// Keyed on the address, not the symbol: a token absent from the
|
||||||
// bundled list has no symbol, and gating the line on one dropped it
|
// bundled list has no symbol, and gating the line on one dropped it
|
||||||
// entirely, leaving a Min. received figure with nothing saying what is
|
// entirely, leaving a Min. received figure with nothing saying what is
|
||||||
|
|||||||
+193
-9
@@ -1,4 +1,4 @@
|
|||||||
const { AbiCoder, Interface, solidityPacked } = require("ethers");
|
const { AbiCoder, Interface, getAddress, solidityPacked } = require("ethers");
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
|
||||||
// A Universal Router on Ethereum Mainnet, chain id 0x1: UniversalRouterV2,
|
// A Universal Router on Ethereum Mainnet, chain id 0x1: UniversalRouterV2,
|
||||||
@@ -1406,6 +1406,14 @@ describe("uniswap decoder", () => {
|
|||||||
return lines.slice(from + 1, to);
|
return lines.slice(from + 1, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The lines of a PERMIT2_PERMIT whose amount does not stand as the
|
||||||
|
// Amount figure, of `amount` of `token` for `spender`.
|
||||||
|
const permitLines = (amount, token, symbol, spender) => [
|
||||||
|
["Permit2 allowance", amount],
|
||||||
|
["Allowance token", symbol + " (" + token + ")"],
|
||||||
|
["Allowance spender", getAddress(spender)],
|
||||||
|
];
|
||||||
|
|
||||||
// execute() of these command bytes and inputs, decoded as sent by
|
// execute() of these command bytes and inputs, decoded as sent by
|
||||||
// the user.
|
// the user.
|
||||||
function decodeSteps(commands, inputs) {
|
function decodeSteps(commands, inputs) {
|
||||||
@@ -1711,6 +1719,7 @@ describe("uniswap decoder", () => {
|
|||||||
kind: "a V4 step swapping 400 USDT with a SETTLE of 5,000 USDT by the user, the rest taken to another address,",
|
kind: "a V4 step swapping 400 USDT with a SETTLE of 5,000 USDT by the user, the rest taken to another address,",
|
||||||
commands: [0x10],
|
commands: [0x10],
|
||||||
inputs: [V4_SETTLE_5000],
|
inputs: [V4_SETTLE_5000],
|
||||||
|
permit: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a PERMIT2_PERMIT of 1 USDT that the router may let fail, then that V4 step,",
|
kind: "a PERMIT2_PERMIT of 1 USDT that the router may let fail, then that V4 step,",
|
||||||
@@ -1719,6 +1728,12 @@ describe("uniswap decoder", () => {
|
|||||||
encodePermit2(USDT_ADDR, 1000000n, ROUTER_ADDR),
|
encodePermit2(USDT_ADDR, 1000000n, ROUTER_ADDR),
|
||||||
V4_SETTLE_5000,
|
V4_SETTLE_5000,
|
||||||
],
|
],
|
||||||
|
permit: permitLines(
|
||||||
|
"1.0000 USDT",
|
||||||
|
USDT_ADDR,
|
||||||
|
"USDT",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
// The unread SWEEP keeps the permit's amount from standing.
|
// The unread SWEEP keeps the permit's amount from standing.
|
||||||
{
|
{
|
||||||
@@ -1735,11 +1750,18 @@ describe("uniswap decoder", () => {
|
|||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
permit: permitLines(
|
||||||
|
"5000.0000 USDT",
|
||||||
|
USDT_ADDR,
|
||||||
|
"USDT",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
])("$kind shows the 5,000 USDT", ({ commands, inputs }) => {
|
])("$kind shows the 5,000 USDT", ({ commands, inputs, permit }) => {
|
||||||
const result = decodeSteps(commands, inputs);
|
const result = decodeSteps(commands, inputs);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Amount", "5000.0000 USDT"],
|
["Amount", "5000.0000 USDT"],
|
||||||
|
...permit,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2023,19 +2045,24 @@ describe("uniswap decoder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Out of that shape the permit caps nothing the Amount line can rely
|
// 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.
|
// on, so the line gives the total the steps take, as with no permit,
|
||||||
|
// and each permit gets lines of its own.
|
||||||
const ONE_USDT_PERMIT = (spender) =>
|
const ONE_USDT_PERMIT = (spender) =>
|
||||||
encodePermit2(USDT_ADDR, 1000000n, spender);
|
encodePermit2(USDT_ADDR, 1000000n, spender);
|
||||||
|
const ONE_USDT_LINES = (spender) =>
|
||||||
|
permitLines("1.0000 USDT", USDT_ADDR, "USDT", spender);
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
kind: "to another spender",
|
kind: "to another spender",
|
||||||
commands: [0x0a],
|
commands: [0x0a],
|
||||||
permits: [ONE_USDT_PERMIT(OTHER_ADDR)],
|
permits: [ONE_USDT_PERMIT(OTHER_ADDR)],
|
||||||
|
lines: ONE_USDT_LINES(OTHER_ADDR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "that the router lets fail",
|
kind: "that the router lets fail",
|
||||||
commands: [0x8a],
|
commands: [0x8a],
|
||||||
permits: [ONE_USDT_PERMIT(ROUTER_ADDR)],
|
permits: [ONE_USDT_PERMIT(ROUTER_ADDR)],
|
||||||
|
lines: ONE_USDT_LINES(ROUTER_ADDR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "followed by a second one",
|
kind: "followed by a second one",
|
||||||
@@ -2044,10 +2071,14 @@ describe("uniswap decoder", () => {
|
|||||||
ONE_USDT_PERMIT(ROUTER_ADDR),
|
ONE_USDT_PERMIT(ROUTER_ADDR),
|
||||||
ONE_USDT_PERMIT(ROUTER_ADDR),
|
ONE_USDT_PERMIT(ROUTER_ADDR),
|
||||||
],
|
],
|
||||||
|
lines: [
|
||||||
|
...ONE_USDT_LINES(ROUTER_ADDR),
|
||||||
|
...ONE_USDT_LINES(ROUTER_ADDR),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"a split after a PERMIT2_PERMIT $kind shows the total the steps take",
|
"a split after a PERMIT2_PERMIT $kind shows the total the steps take",
|
||||||
({ commands, permits }) => {
|
({ commands, permits, lines }) => {
|
||||||
const result = decodeSteps(
|
const result = decodeSteps(
|
||||||
[...commands, 0x00, 0x08],
|
[...commands, 0x00, 0x08],
|
||||||
[
|
[
|
||||||
@@ -2061,10 +2092,20 @@ describe("uniswap decoder", () => {
|
|||||||
);
|
);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Amount", "1000.0000 USDT"],
|
["Amount", "1000.0000 USDT"],
|
||||||
|
...lines,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The lines of a PERMIT2_PERMIT of 600 USDT to the router whose
|
||||||
|
// amount does not stand.
|
||||||
|
const USDT_600_LINES = permitLines(
|
||||||
|
"600.0000 USDT",
|
||||||
|
USDT_ADDR,
|
||||||
|
"USDT",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
);
|
||||||
|
|
||||||
// A PERMIT2_PERMIT_BATCH, which this decoder does not read, may raise
|
// A PERMIT2_PERMIT_BATCH, which this decoder does not read, may raise
|
||||||
// what the router may take of the permit's token again.
|
// 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", () => {
|
test("a split after a PERMIT2_PERMIT and a PERMIT2_PERMIT_BATCH shows the total the steps take", () => {
|
||||||
@@ -2096,6 +2137,7 @@ describe("uniswap decoder", () => {
|
|||||||
);
|
);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Amount", "10000.0000 USDT"],
|
["Amount", "10000.0000 USDT"],
|
||||||
|
...USDT_600_LINES,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2141,6 +2183,7 @@ describe("uniswap decoder", () => {
|
|||||||
);
|
);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Amount", "10000.0000 USDT"],
|
["Amount", "10000.0000 USDT"],
|
||||||
|
...USDT_600_LINES,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2168,8 +2211,9 @@ describe("uniswap decoder", () => {
|
|||||||
];
|
];
|
||||||
// What a V3_SWAP_EXACT_OUT takes, at most its amountInMax, counts
|
// 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.
|
// 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,
|
// The permit of 10,000 USDT stands. The one of 600 USDT comes after
|
||||||
// because the step before it took its token.
|
// the step that set the input side, so it does not stand and gets
|
||||||
|
// lines of its own.
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
kind: "a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT of at most 400 USDT",
|
kind: "a V3 exact-in of 600 USDT, then a V3_SWAP_EXACT_OUT of at most 400 USDT",
|
||||||
@@ -2201,17 +2245,152 @@ describe("uniswap decoder", () => {
|
|||||||
V3_600,
|
V3_600,
|
||||||
],
|
],
|
||||||
amount: "Up to 10000.0000 USDT",
|
amount: "Up to 10000.0000 USDT",
|
||||||
|
permit: USDT_600_LINES,
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"$kind, all paid by the user, shows the total as a maximum",
|
"$kind, all paid by the user, shows the total as a maximum",
|
||||||
({ commands, inputs, amount }) => {
|
({ commands, inputs, amount, permit = [] }) => {
|
||||||
const result = decodeSteps(commands, inputs);
|
const result = decodeSteps(commands, inputs);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Amount", amount],
|
["Amount", amount],
|
||||||
|
...permit,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/370 — a PERMIT2_PERMIT
|
||||||
|
// after the step that set the input side named Token In in place of
|
||||||
|
// that step's token, and its amount could stand as the Amount figure.
|
||||||
|
// The swap's own token and amount now stay on those lines. That
|
||||||
|
// permit, and every other whose amount does not stand as the Amount
|
||||||
|
// figure, gets lines of its own; a permit before the swap steps still
|
||||||
|
// names Token In.
|
||||||
|
describe("a PERMIT2_PERMIT whose amount does not stand as the Amount figure", () => {
|
||||||
|
const DAI_5000 = 5000000000000000000000n;
|
||||||
|
const USDT_5000 = 5000000000n;
|
||||||
|
const MAX_UINT160 = 2n ** 160n - 1n;
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a V3 exact-in of 600 USDT, then a PERMIT2_PERMIT of 5,000 DAI to the router,",
|
||||||
|
commands: [0x00, 0x0a],
|
||||||
|
inputs: [
|
||||||
|
V3_600,
|
||||||
|
encodePermit2(DAI_ADDR, DAI_5000, ROUTER_ADDR),
|
||||||
|
],
|
||||||
|
lines: permitLines(
|
||||||
|
"5000.0000 DAI",
|
||||||
|
DAI_ADDR,
|
||||||
|
"DAI",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V3 exact-in of 600 USDT, then a PERMIT2_PERMIT of unlimited DAI to another spender,",
|
||||||
|
commands: [0x00, 0x0a],
|
||||||
|
inputs: [
|
||||||
|
V3_600,
|
||||||
|
encodePermit2(DAI_ADDR, MAX_UINT160, OTHER_ADDR),
|
||||||
|
],
|
||||||
|
lines: permitLines(
|
||||||
|
"Unlimited",
|
||||||
|
DAI_ADDR,
|
||||||
|
"DAI",
|
||||||
|
OTHER_ADDR,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a V3 exact-in of 600 USDT, then a PERMIT2_PERMIT of 600 USDT, the swap's own token, to the router,",
|
||||||
|
commands: [0x00, 0x0a],
|
||||||
|
inputs: [
|
||||||
|
V3_600,
|
||||||
|
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||||
|
],
|
||||||
|
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],
|
||||||
|
inputs: [
|
||||||
|
encodePermit2(USDT_ADDR, USDT_5000, ROUTER_ADDR),
|
||||||
|
V3_600,
|
||||||
|
],
|
||||||
|
lines: permitLines(
|
||||||
|
"5000.0000 USDT",
|
||||||
|
USDT_ADDR,
|
||||||
|
"USDT",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$kind shows the swap's 600 USDT and the permit's own lines",
|
||||||
|
({ commands, inputs, lines }) => {
|
||||||
|
const result = decodeSteps(commands, inputs);
|
||||||
|
expect(result.name).toBe("Swap USDT → USDC");
|
||||||
|
expect(
|
||||||
|
detail(result, "Token In").address.toLowerCase(),
|
||||||
|
).toBe(USDT_ADDR.toLowerCase());
|
||||||
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
|
["Amount", "600.0000 USDT"],
|
||||||
|
...lines,
|
||||||
|
]);
|
||||||
|
expect(detail(result, "Amount").rawValue).toBe("600.0000");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The permit before the swap steps still names Token In. With a
|
||||||
|
// second permit its amount does not stand, so the Amount line
|
||||||
|
// gives the total the steps take of USDT, the 400 DAI the last
|
||||||
|
// step takes gets lines of its own, and so does each permit.
|
||||||
|
test("a PERMIT2_PERMIT of USDT, a V3 exact-in of 600 USDT, a PERMIT2_PERMIT of DAI and a V2 exact-in of 400 DAI shows both tokens' amounts and each permit's own lines", () => {
|
||||||
|
const result = decodeSteps(
|
||||||
|
[0x0a, 0x00, 0x0a, 0x08],
|
||||||
|
[
|
||||||
|
encodePermit2(USDT_ADDR, USDT_5000, ROUTER_ADDR),
|
||||||
|
V3_600,
|
||||||
|
encodePermit2(DAI_ADDR, DAI_400, ROUTER_ADDR),
|
||||||
|
encodeV2SwapExactIn(USER_ADDR, DAI_400, 390000000n, [
|
||||||
|
DAI_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → USDC");
|
||||||
|
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"],
|
||||||
|
...permitLines(
|
||||||
|
"5000.0000 USDT",
|
||||||
|
USDT_ADDR,
|
||||||
|
"USDT",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
|
...permitLines(
|
||||||
|
"400.0000 DAI",
|
||||||
|
DAI_ADDR,
|
||||||
|
"DAI",
|
||||||
|
ROUTER_ADDR,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// A V3_SWAP_EXACT_OUT whose path is shorter than one token cannot be
|
// 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
|
// read. It may pay any address and take from the sender, so it
|
||||||
// leaves no Amount figure.
|
// leaves no Amount figure.
|
||||||
@@ -2257,7 +2436,7 @@ describe("uniswap decoder", () => {
|
|||||||
// A V3 exact-out of 1,500 USDC for at most 0.5 WETH spends WETH, not
|
// A V3 exact-out of 1,500 USDC for at most 0.5 WETH spends WETH, not
|
||||||
// SPK. After it, a PERMIT2_PERMIT of 1 WETH and a V3 exact-in of 0.5
|
// SPK. After it, a PERMIT2_PERMIT of 1 WETH and a V3 exact-in of 0.5
|
||||||
// WETH: the permit's amount does not stand, because the step before
|
// WETH: the permit's amount does not stand, because the step before
|
||||||
// it also takes WETH.
|
// it also takes WETH, so the permit gets lines of its own.
|
||||||
test("a V3_SWAP_EXACT_OUT whose path has bytes after its last hop, then a PERMIT2_PERMIT and a V3 exact-in, shows no Amount figure", () => {
|
test("a V3_SWAP_EXACT_OUT whose path has bytes after its last hop, then a PERMIT2_PERMIT and a V3 exact-in, shows no Amount figure", () => {
|
||||||
const path = pathEndingIn([USDC_ADDR, WETH_ADDR], SPK_ADDR);
|
const path = pathEndingIn([USDC_ADDR, WETH_ADDR], SPK_ADDR);
|
||||||
const result = decodeSteps(
|
const result = decodeSteps(
|
||||||
@@ -2273,6 +2452,7 @@ describe("uniswap decoder", () => {
|
|||||||
);
|
);
|
||||||
expect(linesBetweenTokens(result)).toEqual([
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
["Moved from the sender", UNDECODED],
|
["Moved from the sender", UNDECODED],
|
||||||
|
...permitLines("1.0000 WETH", WETH_ADDR, "WETH", ROUTER_ADDR),
|
||||||
]);
|
]);
|
||||||
expect(detail(result, "Paid to").value).toBe(UNDECODED);
|
expect(detail(result, "Paid to").value).toBe(UNDECODED);
|
||||||
});
|
});
|
||||||
@@ -2443,7 +2623,11 @@ describe("uniswap decoder", () => {
|
|||||||
V4_EXACT_OUT_600,
|
V4_EXACT_OUT_600,
|
||||||
V2_EXACT_OUT_400,
|
V2_EXACT_OUT_400,
|
||||||
],
|
],
|
||||||
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT],
|
lines: [
|
||||||
|
...UNKNOWN_USDT,
|
||||||
|
...UP_TO_400_USDT,
|
||||||
|
...ONE_USDT_LINES(OTHER_ADDR),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a V4 step whose swap action cannot be read, with a SETTLE of WETH by the user, then a V2 exact-out of at most 400 USDT",
|
kind: "a V4 step whose swap action cannot be read, with a SETTLE of WETH by the user, then a V2 exact-out of at most 400 USDT",
|
||||||
|
|||||||
@@ -2809,7 +2809,7 @@ describe("the allow-revert flag", () => {
|
|||||||
mark: " (allowed to fail)",
|
mark: " (allowed to fail)",
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"on $router, a PERMIT2_PERMIT with the flag is listed as 'Permit2 Permit$mark', and the swap reads as without the flag",
|
"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 }) => {
|
||||||
const steps = [
|
const steps = [
|
||||||
permit2Permit(USDT, USDT_2000),
|
permit2Permit(USDT, USDT_2000),
|
||||||
@@ -2824,9 +2824,24 @@ describe("the allow-revert flag", () => {
|
|||||||
expect(detail(withFlag, "Steps").value).toBe(
|
expect(detail(withFlag, "Steps").value).toBe(
|
||||||
"Permit2 Permit" + mark + " → V3 Swap (Exact In)",
|
"Permit2 Permit" + mark + " → V3 Swap (Exact In)",
|
||||||
);
|
);
|
||||||
const withoutSteps = (r) =>
|
// The permit names the router at ROUTER as its spender, so its
|
||||||
r.details.filter((d) => d.label !== "Steps");
|
// amount stands as the Amount figure only there, and only without
|
||||||
expect(withoutSteps(withFlag)).toEqual(withoutSteps(plain));
|
// the flag. With the flag it stands on no router and gets lines
|
||||||
|
// of its own.
|
||||||
|
const permitLines = [
|
||||||
|
"Permit2 allowance",
|
||||||
|
"Allowance token",
|
||||||
|
"Allowance spender",
|
||||||
|
];
|
||||||
|
const swapLines = (r) =>
|
||||||
|
r.details.filter(
|
||||||
|
(d) =>
|
||||||
|
d.label !== "Steps" && !permitLines.includes(d.label),
|
||||||
|
);
|
||||||
|
expect(swapLines(withFlag)).toEqual(swapLines(plain));
|
||||||
|
expect(
|
||||||
|
labels(withFlag).filter((l) => permitLines.includes(l)),
|
||||||
|
).toEqual(permitLines);
|
||||||
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
|
expect(detail(withFlag, "Min. received").value).toBe("1.0000 WETH");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user