Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41dfdc8701 |
@@ -3153,36 +3153,40 @@ Currently supported:
|
||||
included, stands as the `Amount` figure in place of the total only in the
|
||||
plain shape of Uniswap's own routes: it is the only `PERMIT2_PERMIT`, its
|
||||
spender is the router, the router may not let it fail, no step before it takes
|
||||
its token or a token the decoder does not know, and no `PERMIT2_PERMIT_BATCH`
|
||||
comes after it. Permit2 then lets the router take no more of the token than
|
||||
that amount, so the permit covers every take of it. In any other case the line
|
||||
gives the total, as with no permit. When the step that sets the input side
|
||||
spends a token other than the permit's, or one the decoder does not know, its
|
||||
amount gets lines of its own and the total has no figure to start from: unless
|
||||
the permit's amount stands there is no `Amount` line, and each take of the
|
||||
permit's token gets lines of its own. Each take the total does not count gives
|
||||
three lines after the `Amount` line (after `Token In` when there is none): one
|
||||
of a token other than `Token In`; one of `Token In` after an amount that is
|
||||
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
|
||||
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
|
||||
whose amount the decoder does not know. 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;
|
||||
`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
|
||||
does not know gives the `Moved from the sender` line alone. A step may also
|
||||
take from the user's wallet what the decoder did not read: a V4 step, other
|
||||
than the one that sets the input side, whose input or one of whose `SETTLE`s
|
||||
or `SETTLE_ALL`s it cannot read, which the router still runs; a V2 or V3
|
||||
exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input it
|
||||
cannot read; and every `PERMIT2_TRANSFER_FROM_BATCH`. Each such step gives one
|
||||
`Moved from the sender` line among those, reading
|
||||
`Unknown (could not be decoded)`. That line, or any line for a token the
|
||||
decoder does not know, leaves no `Amount` line, whose figure might be too low.
|
||||
Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step order,
|
||||
gives three lines: `Moved from the sender`, the amount it moves, for example
|
||||
`1000000.0000 USDT`; `Token moved`, giving the token's address as the
|
||||
its token or a token the decoder does not know, and the decoder reads every
|
||||
step after it in full. Permit2 then lets the router take no more of the token
|
||||
than that amount, so the permit covers every take of it. A step the decoder
|
||||
does not read in full may raise what Permit2 lets the router take, as a
|
||||
`PERMIT2_PERMIT_BATCH` can: a command the decoder does not read (listed at the
|
||||
end of this section), a step whose input it cannot read, in whole or in part,
|
||||
and a step it fails on. In any other case the line gives the total, as with no
|
||||
permit. When the step that sets the input side spends a token other than the
|
||||
permit's, or one the decoder does not know, its amount gets lines of its own
|
||||
and the total has no figure to start from: unless the permit's amount stands
|
||||
there is no `Amount` line, and each take of the permit's token gets lines of
|
||||
its own. Each take the total does not count gives three lines after the
|
||||
`Amount` line (after `Token In` when there is none): one of a token other than
|
||||
`Token In`; one of `Token In` after an amount that is not a quantity (a V4
|
||||
open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or
|
||||
no stated amount, as of a V4 exact-out step); and one whose amount the decoder
|
||||
does not know. 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; `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 does not know gives the `Moved from the sender` line alone. A step
|
||||
may also take from the user's wallet what the decoder did not read: a V4 step,
|
||||
other than the one that sets the input side, whose input or one of whose
|
||||
`SETTLE`s or `SETTLE_ALL`s it cannot read, which the router still runs; a V2
|
||||
or V3 exact-in or V2 exact-out step or a `PERMIT2_TRANSFER_FROM` whose input
|
||||
it cannot read; every `PERMIT2_TRANSFER_FROM_BATCH`; and, once the input side
|
||||
is set, every `V3_SWAP_EXACT_OUT`, which the decoder does not read, and every
|
||||
step it fails on. Each such step gives one `Moved from the sender` line among
|
||||
those, reading `Unknown (could not be decoded)`. That line, or any line for a
|
||||
token the decoder does not know, leaves no `Amount` line, whose figure might
|
||||
be too low. Then each `PERMIT2_TRANSFER_FROM` but one into the router, in step
|
||||
order, gives three lines: `Moved from the sender`, the amount it moves, for
|
||||
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
|
||||
`Token In` line does; and `Moved to`, which reads
|
||||
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
|
||||
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
||||
|
||||
+56
-16
@@ -1169,6 +1169,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// The last PERMIT2_PERMIT this decoder read, or null (see
|
||||
// permitStands after the loop).
|
||||
let permit = null;
|
||||
// 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
|
||||
// whole or in part, and one it fails on (see permitStands).
|
||||
const stepsUnread = [];
|
||||
let outputToken = null;
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
@@ -1229,10 +1233,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// A step may take from the sender what this decoder did not read: a
|
||||
// V4 step, other than the one that sets the input side, whose input
|
||||
// or one of whose SETTLEs or SETTLE_ALLs it cannot read; a V2 or V3
|
||||
// swap step or PERMIT2_TRANSFER_FROM whose input it cannot read; or a
|
||||
// PERMIT2_TRANSFER_FROM_BATCH. It is recorded with a null token and
|
||||
// amount: one line says so, and there is no Amount line, whose figure
|
||||
// may then be too low.
|
||||
// exact-in or V2 exact-out step or PERMIT2_TRANSFER_FROM whose input
|
||||
// it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; and, once the input
|
||||
// side is set, a V3_SWAP_EXACT_OUT or a step it fails on. It is
|
||||
// recorded with a null token and amount: one line says so, and there
|
||||
// is no Amount line, whose figure may then be too low.
|
||||
const spentUnread = () => {
|
||||
userPaid.push({ token: null, amount: null });
|
||||
};
|
||||
@@ -1371,6 +1376,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
hasSubPlan = true;
|
||||
commandNames[i] += " (its steps are not shown)";
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
} else if (command === "PERMIT2_PERMIT") {
|
||||
const p = decodePermit2(inputs[i]);
|
||||
if (p) {
|
||||
@@ -1389,11 +1395,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
allowedToFail,
|
||||
beforeEveryTake: !tookBefore,
|
||||
};
|
||||
} else {
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
||||
const p = decodePermit2TransferFrom(inputs[i]);
|
||||
if (!p) {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
} else if (
|
||||
commandTypeMask === 0x1f &&
|
||||
@@ -1432,6 +1441,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// owner may be any address. It sets no side, and the
|
||||
// step says so.
|
||||
commandNames[i] += " (could not be decoded)";
|
||||
stepsUnread.push(i);
|
||||
} else if (b.owner.toLowerCase() !== sender) {
|
||||
// The router reads the owner as written (see
|
||||
// decodeBalanceCheck()), so address(1) and address(2)
|
||||
@@ -1473,8 +1483,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
}
|
||||
} else if (command === "V3_SWAP_EXACT_OUT") {
|
||||
// This decoder does not read it
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/601). It
|
||||
// may pay any address and take from the sender, so once
|
||||
// the input side is set it leaves no Amount figure.
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
if (inputEstablished) spentUnread();
|
||||
} else if (command === "V2_SWAP_EXACT_IN") {
|
||||
const s = decodeV2SwapExactIn(inputs[i]);
|
||||
if (s) {
|
||||
@@ -1492,6 +1511,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
}
|
||||
} else if (command === "V2_SWAP_EXACT_OUT") {
|
||||
@@ -1510,12 +1530,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
}
|
||||
} else if (command === "PERMIT2_TRANSFER_FROM_BATCH") {
|
||||
// It moves the sender's tokens to any addresses, and this
|
||||
// decoder does not read it.
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
} else if (command === "SWEEP") {
|
||||
const s = decodeSweep(inputs[i]);
|
||||
@@ -1526,6 +1548,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else if (
|
||||
command === "TRANSFER" ||
|
||||
@@ -1562,6 +1585,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else if (command === "ACROSS_V4_DEPOSIT_V3") {
|
||||
const a = decodeAcrossV4DepositV3(inputs[i]);
|
||||
@@ -1585,6 +1609,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else if (command === "WRAP_ETH") {
|
||||
const w = decodeWrapEth(inputs[i]);
|
||||
@@ -1615,6 +1640,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else if (command === "V4_SWAP") {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
@@ -1634,6 +1660,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!v4 || v4.recipientUnread) {
|
||||
payeeUnread = true;
|
||||
}
|
||||
if (
|
||||
!v4 ||
|
||||
v4.recipientUnread ||
|
||||
v4.settleUnread ||
|
||||
v4.swapUndecoded
|
||||
) {
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
if (v4 && v4.swapUndecoded) {
|
||||
// A swap action of this step went unread, so the step
|
||||
// is marked.
|
||||
@@ -1701,16 +1735,21 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} else {
|
||||
// Any other step may pay an address this decoder does not
|
||||
// read: V3_SWAP_EXACT_OUT and the NFT commands among them,
|
||||
// and a type the release names no command for.
|
||||
// read: PERMIT2_PERMIT_BATCH and the NFT commands among
|
||||
// them, and a type the release names no command for.
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
} catch {
|
||||
// Skip commands we can't decode. One may pay any address.
|
||||
// Skip commands we can't decode. One may pay any address and
|
||||
// take from the sender.
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
if (inputEstablished) spentUnread();
|
||||
}
|
||||
if (allowedToFail) {
|
||||
commandNames[i] += " (allowed to fail)";
|
||||
@@ -1731,12 +1770,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// the Amount figure in place of the total only in the plain shape of
|
||||
// Uniswap's own routes: it is the only PERMIT2_PERMIT, its spender is
|
||||
// the router, the router may not let it fail, no step before it took
|
||||
// its token or one not known, and no PERMIT2_PERMIT_BATCH comes after
|
||||
// it. Permit2 then
|
||||
// lets the router take no more of the token than that amount, so the
|
||||
// permit covers every take of it. A step this decoder does not read
|
||||
// that may take from the sender leaves no figure in any case, and an
|
||||
// EXECUTE_SUB_PLAN step leaves both sides unknown (below).
|
||||
// its token or one not known, and this decoder read every step after
|
||||
// it in full (see stepsUnread). Permit2 then lets the router take no
|
||||
// more of the token than that amount, so the permit covers every take
|
||||
// of it. A later step this decoder did not read may raise what Permit2
|
||||
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step this
|
||||
// decoder does not read that may take from the sender leaves no
|
||||
// figure, permit or not, when it comes after the step that sets the
|
||||
// input side (see spentUnread()), and an EXECUTE_SUB_PLAN step leaves
|
||||
// both sides unknown (below).
|
||||
const stepCommands = Array.from(
|
||||
commandsBytes,
|
||||
(b) => commands[b & commandTypeMask],
|
||||
@@ -1747,9 +1789,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
sameAddress(permit.spender, toAddress) &&
|
||||
!permit.allowedToFail &&
|
||||
permit.beforeEveryTake &&
|
||||
!stepCommands
|
||||
.slice(permit.step + 1)
|
||||
.includes("PERMIT2_PERMIT_BATCH");
|
||||
!stepsUnread.some((step) => step > permit.step);
|
||||
// What the steps take that the Amount line does not give, as
|
||||
// { token, amount }. A null token is a step this decoder did not read
|
||||
// (see spentUnread()), or a token it does not know.
|
||||
|
||||
@@ -1830,6 +1830,70 @@ describe("uniswap decoder", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user