fix: a PERMIT2_TRANSFER_FROM before a permit keeps its amount from standing as the Amount figure (closes #655)
A PERMIT2_TRANSFER_FROM that runs before a PERMIT2_PERMIT of the same token moves it under whatever allowance the router already had, so it can move more than the permit's amount. The permit's amount still stood as the Amount figure, which could then read lower than what leaves the wallet. decode() now counts such a transfer, into the router or anywhere else, among the steps before the permit that took its token, so the permit no longer stands: the Amount line gives the total the steps take, as with no permit, and the permit gets its own lines. A transfer after the permit, or of another token before it, still lets it stand; tests cover both. README Transaction Decoding states the condition. Model: opus-5-5
This commit was merged in pull request #661.
This commit is contained in:
@@ -3226,36 +3226,41 @@ Currently supported:
|
|||||||
which it does only in the plain shape of Uniswap's own routes: it comes before
|
which it does only in the plain shape of Uniswap's own routes: it comes before
|
||||||
the step that sets the input side, it is the only `PERMIT2_PERMIT` in the
|
the step that sets the input side, it is the only `PERMIT2_PERMIT` in the
|
||||||
transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router,
|
transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router,
|
||||||
the router may not let it fail, no step before it takes its token or a token
|
the router may not let it fail, no step before it takes its token, a
|
||||||
the decoder does not know, and the decoder reads every step after it in full.
|
`PERMIT2_TRANSFER_FROM` of it included, or a token the decoder does not know,
|
||||||
Permit2 then lets the router take no more of the token than that amount, so
|
and the decoder reads every step after it in full. Permit2 then lets the
|
||||||
the permit covers every take of it. A later permit or batch may raise what
|
router take no more of the token than that amount, so the permit covers every
|
||||||
Permit2 lets the router take, and so may a step the decoder does not read in
|
take of it. A step before the permit takes under whatever allowance the router
|
||||||
full: a command the decoder does not read (listed at the end of this section),
|
already had, so it may take more than the permit's amount, which would then
|
||||||
a step whose input it cannot read, in whole or in part, and a step it fails
|
read lower than what leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of
|
||||||
on. Every other `PERMIT2_PERMIT`, before the step that sets the input side or
|
2,000 USDT into the router, then a permit of 600 USDT to the router and a V3
|
||||||
after it, leaves `Token In` and the `Amount` line as they read with no permit:
|
exact-in of 2,000 USDT paid from the router, read `Amount 2000.0000 USDT`. A
|
||||||
a permit of 5,000 DAI to another address and then a V3 exact-in of 600 USDT
|
later permit or batch may raise what Permit2 lets the router take, and so may
|
||||||
read `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do that exact-in
|
a step the decoder does not read in full: a command the decoder does not read
|
||||||
and then a permit of 5,000 DAI to the router. When a permit's amount stands
|
(listed at the end of this section), a step whose input it cannot read, in
|
||||||
and the step that sets the input side spends a token other than the permit's,
|
whole or in part, and a step it fails on. Every other `PERMIT2_PERMIT`, before
|
||||||
or one the decoder does not know, that step's amount gets lines of its own,
|
the step that sets the input side or after it, leaves `Token In` and the
|
||||||
unless it states no amount and the user pays it none of its input token. Each
|
`Amount` line as they read with no permit: a permit of 5,000 DAI to another
|
||||||
take the total does not count gives three lines after the `Amount` line (after
|
address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with
|
||||||
`Token In` when there is none): one of a token other than `Token In`; one of
|
`Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI
|
||||||
`Token In` after an amount that is not a quantity (a V4 open delta,
|
to the router. When a permit's amount stands and the step that sets the input
|
||||||
`ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated
|
side spends a token other than the permit's, or one the decoder does not know,
|
||||||
amount, as of a V4 exact-out step); and one whose amount the decoder does not
|
that step's amount gets lines of its own, unless it states no amount and the
|
||||||
know. When the step that sets the input side states no amount, the user pays
|
user pays it none of its input token. Each take the total does not count gives
|
||||||
it some of its input token, and there is no `Amount` line, that step gets them
|
three lines after the `Amount` line (after `Token In` when there is none): one
|
||||||
too whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM`
|
of a token other than `Token In`; one of `Token In` after an amount that is
|
||||||
of its token gives one, on the lines below or, into the router, on its
|
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
|
||||||
`Paid to` line, so that figure cannot read as all that leaves. When it states
|
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
|
||||||
no amount and the user pays it none of its input token, as when the router
|
whose amount the decoder does not know. When the step that sets the input side
|
||||||
pays it, that step takes none of that token from the user's wallet and never
|
states no amount, the user pays it some of its input token, and there is no
|
||||||
gets them, with a permit or without. They come in step order, the step that
|
`Amount` line, that step gets them too whenever another take's lines give a
|
||||||
sets the input side first when it has them: `Moved from the sender`, its
|
figure, or a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines
|
||||||
amount, worded as the `Amount` line words one, or
|
below or, into the router, on its `Paid to` line, so that figure cannot read
|
||||||
|
as all that leaves. When it states no amount and the user pays it none of its
|
||||||
|
input token, as when the router pays it, that step takes none of that token
|
||||||
|
from the user's wallet and never gets them, with a permit or without. They
|
||||||
|
come in step order, the step that sets the input side first when it has them:
|
||||||
|
`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
|
||||||
|
|||||||
+20
-16
@@ -1524,14 +1524,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// side never stands, so only one before it is kept here.
|
// side never stands, so only one before it is kept here.
|
||||||
if (p && !inputEstablished) {
|
if (p && !inputEstablished) {
|
||||||
// Whether a step before it took its token, or may
|
// Whether a step before it took its token, or may
|
||||||
// have: one in `userPaid`, when its token is the
|
// have: one in `userPaid`, `movedIntoRouter` or
|
||||||
// permit's or not known. No step has set the input
|
// `movedFromSender`, when its token is the permit's
|
||||||
// side yet.
|
// or not known. No step has set the input side yet.
|
||||||
const mayBeItsToken = (token) =>
|
const mayBeItsToken = (token) =>
|
||||||
!present(token) || sameAddress(token, p.token);
|
!present(token) || sameAddress(token, p.token);
|
||||||
const tookBefore = userPaid.some((t) =>
|
const tookBefore = [
|
||||||
mayBeItsToken(t.token),
|
...userPaid,
|
||||||
);
|
...movedIntoRouter,
|
||||||
|
...movedFromSender,
|
||||||
|
].some((t) => mayBeItsToken(t.token));
|
||||||
permit = {
|
permit = {
|
||||||
...p,
|
...p,
|
||||||
step: i,
|
step: i,
|
||||||
@@ -2009,16 +2011,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// set the input side, it is the only PERMIT2_PERMIT in the
|
// set the input side, it is the only PERMIT2_PERMIT in the
|
||||||
// transaction and there is no PERMIT2_PERMIT_BATCH, its spender is
|
// transaction and there is no PERMIT2_PERMIT_BATCH, its spender is
|
||||||
// the router, the router may not let it fail, no step before it took
|
// the router, the router may not let it fail, no step before it took
|
||||||
// its token or one not known, and this decoder read every step after
|
// its token or one not known (a PERMIT2_TRANSFER_FROM of its token
|
||||||
// it in full (see stepsUnread). Permit2 then lets the router take no
|
// included), and this decoder read every step after it in full (see
|
||||||
// more of the token than that amount, so the permit covers every take
|
// stepsUnread). Permit2 then lets the router take no more of the token
|
||||||
// of it. A later permit or batch, or a later step this decoder did
|
// than that amount, so the permit covers every take of it. A step
|
||||||
// not read, may raise what Permit2 lets the router take. Every other
|
// before it takes under whatever allowance the router already had, so
|
||||||
// PERMIT2_PERMIT leaves Token In and the Amount line as they read with
|
// it may take more. A later permit or batch, or a later step this
|
||||||
// no permit, and gets lines of its own below. A step that may take
|
// decoder did not read, may raise what Permit2 lets the router take.
|
||||||
// from the sender what this decoder did not read leaves no figure,
|
// Every other PERMIT2_PERMIT leaves Token In and the Amount line as
|
||||||
// permit or not (see spentUnread()), and so does a permit or batch
|
// they read with no permit, and gets lines of its own below. A step
|
||||||
// whose input it cannot read (see permitUnread()). An
|
// that may take from the sender what this decoder did not read leaves
|
||||||
|
// no figure, permit or not (see spentUnread()), and so does a permit
|
||||||
|
// or batch whose input it cannot read (see permitUnread()). An
|
||||||
// EXECUTE_SUB_PLAN step leaves both sides unknown (below).
|
// EXECUTE_SUB_PLAN step leaves both sides unknown (below).
|
||||||
const stepCommands = Array.from(
|
const stepCommands = Array.from(
|
||||||
commandsBytes,
|
commandsBytes,
|
||||||
|
|||||||
@@ -2196,6 +2196,100 @@ describe("uniswap decoder", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/655 — a
|
||||||
|
// PERMIT2_TRANSFER_FROM of the permit's token before the permit moves
|
||||||
|
// it under whatever allowance the router already had, so it may move
|
||||||
|
// more than the permit's amount. The permit's amount then does not
|
||||||
|
// stand, and the Amount line gives the total the steps take, as with
|
||||||
|
// no permit. On next at f53b503 the first two rows read the permit's
|
||||||
|
// 600.0000 USDT as the Amount figure, with no lines for the permit.
|
||||||
|
// One after the permit, as in Uniswap's own V2 routes, moves no more
|
||||||
|
// than the permit's amount, and one of another token before it moves
|
||||||
|
// none of the permit's token, so in the last two rows the permit's
|
||||||
|
// amount stands.
|
||||||
|
const USDT_2000 = 2000000000n;
|
||||||
|
// The V2 pair of WETH and USDC on Ethereum Mainnet.
|
||||||
|
const WETH_USDC_PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||||
|
// A PERMIT2_TRANSFER_FROM of `amount` of `token` to `recipient`.
|
||||||
|
const permit2TransferFrom = (token, recipient, amount) =>
|
||||||
|
coder.encode(
|
||||||
|
["address", "address", "uint160"],
|
||||||
|
[token, recipient, amount],
|
||||||
|
);
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_TRANSFER_FROM of 2,000 USDT into the router, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 2,000 USDT paid from the router, shows the total the steps take and the permit's own lines",
|
||||||
|
commands: [0x02, 0x0a, 0x00],
|
||||||
|
inputs: [
|
||||||
|
permit2TransferFrom(USDT_ADDR, ROUTER_ADDR, USDT_2000),
|
||||||
|
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||||
|
coder.encode(
|
||||||
|
["address", "uint256", "uint256", "bytes", "bool"],
|
||||||
|
[
|
||||||
|
USER_ADDR,
|
||||||
|
USDT_2000,
|
||||||
|
1n,
|
||||||
|
encodeV3Path([USDT_ADDR, USDC_ADDR]),
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
lines: [["Amount", "2000.0000 USDT"], ...USDT_600_LINES],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_TRANSFER_FROM of 2,000 USDT to another address, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 600 USDT, shows the total the steps take and the permit's own lines",
|
||||||
|
commands: [0x02, 0x0a, 0x00],
|
||||||
|
inputs: [
|
||||||
|
permit2TransferFrom(USDT_ADDR, OTHER_ADDR, USDT_2000),
|
||||||
|
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||||
|
V3_600,
|
||||||
|
],
|
||||||
|
lines: [
|
||||||
|
["Amount", "600.0000 USDT"],
|
||||||
|
["Moved from the sender", "2000.0000 USDT"],
|
||||||
|
["Token moved", "USDT (" + USDT_ADDR + ")"],
|
||||||
|
["Moved to", OTHER_ADDR],
|
||||||
|
...USDT_600_LINES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_PERMIT of 1 WETH, then a PERMIT2_TRANSFER_FROM of 1 WETH to the V2 pair and a V2 exact-in of ALREADY_PAID, shows the permit's amount",
|
||||||
|
commands: [0x0a, 0x02, 0x08],
|
||||||
|
inputs: [
|
||||||
|
encodePermit2(WETH_ADDR, ONE_WETH, ROUTER_ADDR),
|
||||||
|
permit2TransferFrom(WETH_ADDR, WETH_USDC_PAIR, ONE_WETH),
|
||||||
|
encodeV2SwapExactIn(USER_ADDR, 0n, 1990000000n, [
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
lines: [
|
||||||
|
["Amount", "1.0000 WETH"],
|
||||||
|
["Moved from the sender", "1.0000 WETH"],
|
||||||
|
["Token moved", "WETH (" + WETH_ADDR + ")"],
|
||||||
|
["Moved to", "The V2 pair the next swap spends"],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_TRANSFER_FROM of 400 DAI to another address, then a PERMIT2_PERMIT of 600 USDT and a V3 exact-in of 600 USDT, shows the permit's amount",
|
||||||
|
commands: [0x02, 0x0a, 0x00],
|
||||||
|
inputs: [
|
||||||
|
permit2TransferFrom(DAI_ADDR, OTHER_ADDR, DAI_400),
|
||||||
|
encodePermit2(USDT_ADDR, USDT_600, ROUTER_ADDR),
|
||||||
|
V3_600,
|
||||||
|
],
|
||||||
|
lines: [
|
||||||
|
["Amount", "600.0000 USDT"],
|
||||||
|
["Moved from the sender", "400.0000 DAI"],
|
||||||
|
["Token moved", "DAI (" + DAI_ADDR + ")"],
|
||||||
|
["Moved to", OTHER_ADDR],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])("$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`
|
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
|
||||||
// USDT for USDC.
|
// USDT for USDC.
|
||||||
const v3ExactOut = (amountInMax) =>
|
const v3ExactOut = (amountInMax) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user