fix: a PERMIT2_PERMIT_BATCH, and a permit that cannot be read, state what they allow (closes #652)
A PERMIT2_PERMIT_BATCH was never read, and a PERMIT2_PERMIT whose input could not be read got no lines, so the approval screen showed nothing of the allowances either grants. Each allowance of a batch now gets the three lines a permit whose amount does not stand gets: Permit2 allowance, Allowance token and Allowance spender. A permit or batch that cannot be read gets one Permit2 allowance line reading Unknown (could not be decoded) and leaves no Amount line. Any batch in the transaction keeps a PERMIT2_PERMIT's amount from standing. A batch pays nobody, so it no longer adds a Paid to line. README Transaction Decoding and the TxApproval element list match. Model: opus-5-5
This commit was merged in pull request #656.
This commit is contained in:
+89
-30
@@ -336,7 +336,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
||||
// currencies, and those commands may name one, so saying the calldata did not
|
||||
// name it would be false. The Paid to line uses it too, when a step may pay an
|
||||
// address this decoder did not read, and so do the lines for what a step
|
||||
// takes from the sender, when this decoder does not know it.
|
||||
// takes from the sender, when this decoder does not know it, and for what a
|
||||
// permit it could not read allows.
|
||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||
|
||||
// The Paid to line for each token paid to the router, the swap's output or
|
||||
@@ -528,6 +529,32 @@ function decodePermit2(input) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decode PERMIT2_PERMIT_BATCH (command 0x03) input bytes into the allowances
|
||||
// it grants, one for each token it names, all for its one spender, in the
|
||||
// shape decodePermit2() returns. Each release's
|
||||
// `contracts/base/Dispatcher.sol`, at the commit linked for it above, hands
|
||||
// Permit2 the batch in this shape.
|
||||
// ABI: (((address token, uint160 amount, uint48 expiration, uint48 nonce)[],
|
||||
// address spender, uint256 sigDeadline), bytes signature)
|
||||
function decodePermit2Batch(input) {
|
||||
try {
|
||||
const d = coder.decode(
|
||||
[
|
||||
"tuple(tuple(address,uint160,uint48,uint48)[],address,uint256)",
|
||||
"bytes",
|
||||
],
|
||||
input,
|
||||
);
|
||||
return d[0][0].map((details) => ({
|
||||
token: details[0],
|
||||
amount: details[1],
|
||||
spender: d[0][1],
|
||||
}));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode BALANCE_CHECK_ERC20 (command 0x0e) input bytes. The router passes the
|
||||
// check when `owner` holds at least `minBalance` of `token`, and reads `owner`
|
||||
// as written, without map(), on every release that has the command:
|
||||
@@ -1214,10 +1241,12 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||
// input side, or null (see permitStands after the loop).
|
||||
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).
|
||||
// Every allowance a PERMIT2_PERMIT or PERMIT2_PERMIT_BATCH grants,
|
||||
// before a step set the input side or after, in step order, as
|
||||
// { token, amount, spender, step }. A permit or batch whose input
|
||||
// this decoder cannot read is one with a null token, amount and
|
||||
// spender. Each but a permit whose amount is 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
|
||||
// command it does not read, a step whose input it cannot read, in
|
||||
@@ -1322,6 +1351,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const spentUnread = () => {
|
||||
userPaid.push({ token: null, amount: null });
|
||||
};
|
||||
// A PERMIT2_PERMIT or PERMIT2_PERMIT_BATCH at `step` whose input this
|
||||
// decoder cannot read may let any spender take any amount of any
|
||||
// token: one line says so, and there is no Amount line, whose figure
|
||||
// may then be too low.
|
||||
const permitUnread = (step) => {
|
||||
permits.push({ token: null, amount: null, spender: null, step });
|
||||
stepsUnread.push(step);
|
||||
};
|
||||
|
||||
// Who the steps pay, in whichever step and of whatever token.
|
||||
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
|
||||
@@ -1477,7 +1514,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
permits.push({ ...p, step: i });
|
||||
keepsRouterWeth = true;
|
||||
} else {
|
||||
stepsUnread.push(i);
|
||||
permitUnread(i);
|
||||
}
|
||||
// Only a permit whose amount stands as the Amount figure
|
||||
// names Token In (see permitStands). Every other permit,
|
||||
@@ -1502,6 +1539,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
beforeEveryTake: !tookBefore,
|
||||
};
|
||||
}
|
||||
} else if (command === "PERMIT2_PERMIT_BATCH") {
|
||||
// Each allowance it grants gets the lines of a permit
|
||||
// whose amount does not stand. It pays nobody.
|
||||
const batch = decodePermit2Batch(inputs[i]);
|
||||
if (batch) {
|
||||
permits.push(...batch.map((p) => ({ ...p, step: i })));
|
||||
} else {
|
||||
permitUnread(i);
|
||||
}
|
||||
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
||||
const p = decodePermit2TransferFrom(inputs[i]);
|
||||
if (!p) {
|
||||
@@ -1910,8 +1956,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else {
|
||||
// Any other step may pay an address this decoder does not
|
||||
// read: PERMIT2_PERMIT_BATCH and the NFT commands among
|
||||
// them, and a type the release names no command for.
|
||||
// read: the NFT commands among them, and a type the
|
||||
// release names no command for.
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
}
|
||||
@@ -1961,17 +2007,19 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Amount figure in place of the total, which it does only in the
|
||||
// plain shape of Uniswap's own routes: it comes before the step that
|
||||
// set the input side, it is the only PERMIT2_PERMIT in the
|
||||
// transaction, its spender is 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 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. Every other PERMIT2_PERMIT leaves Token In
|
||||
// and the Amount line as they read with no permit, and gets lines of
|
||||
// its own below. 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 step leaves both sides unknown (below).
|
||||
// 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
|
||||
// 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 permit or batch, or a later step this decoder did
|
||||
// not read, may raise what Permit2 lets the router take. Every other
|
||||
// PERMIT2_PERMIT leaves Token In and the Amount line as they read with
|
||||
// no permit, and gets lines of its own below. A step 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).
|
||||
const stepCommands = Array.from(
|
||||
commandsBytes,
|
||||
(b) => commands[b & commandTypeMask],
|
||||
@@ -1979,6 +2027,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
const permitStands =
|
||||
permit !== null &&
|
||||
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
|
||||
!stepCommands.includes("PERMIT2_PERMIT_BATCH") &&
|
||||
sameAddress(permit.spender, toAddress) &&
|
||||
!permit.allowedToFail &&
|
||||
permit.beforeEveryTake &&
|
||||
@@ -2147,11 +2196,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
};
|
||||
|
||||
// A null token in `spentFromSender`, a step this decoder did not read
|
||||
// or a token it does not know, leaves no Amount figure, which may
|
||||
// then be too low.
|
||||
// or a token it does not know, and one in `permits`, a permit or
|
||||
// batch it could not read, leave no Amount figure, which may then be
|
||||
// too low.
|
||||
if (
|
||||
present(inputAmount) &&
|
||||
spentFromSender.every((s) => present(s.token))
|
||||
spentFromSender.every((s) => present(s.token)) &&
|
||||
permits.every((p) => present(p.token))
|
||||
) {
|
||||
const amount = spentText(inputAmount, inInfo);
|
||||
details.push({
|
||||
@@ -2201,16 +2252,24 @@ 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.
|
||||
// What each allowance of a PERMIT2_PERMIT or PERMIT2_PERMIT_BATCH 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 allowance, one after the
|
||||
// step that set the input side included, gets these lines, so nothing
|
||||
// it lets anyone take goes unstated. A permit or batch this decoder
|
||||
// could not read gets one line saying so.
|
||||
for (const p of permits) {
|
||||
if (permitStands && p.step === permit.step) continue;
|
||||
if (!present(p.token)) {
|
||||
details.push({
|
||||
label: "Permit2 allowance",
|
||||
value: UNDECODED_CURRENCY,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
details.push({
|
||||
label: "Permit2 allowance",
|
||||
value:
|
||||
|
||||
Reference in New Issue
Block a user