Compare commits
1
Commits
next
...
8382fab3b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8382fab3b6 |
@@ -1189,14 +1189,8 @@ number. They can read:
|
|||||||
`amountOut`, the exact amount it buys.
|
`amountOut`, the exact amount it buys.
|
||||||
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
|
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
|
||||||
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
|
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
|
||||||
whole open delta", so the calldata states no quantity. The line shows the
|
whole open delta", so the calldata states no quantity. Transaction Decoding
|
||||||
amount of the step that sets the input side: the last `PERMIT2_PERMIT` step if
|
below gives the steps whose amounts the line shows.
|
||||||
there is one, otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH`
|
|
||||||
or V4 swap step that names an input token or amount. A V4 step with a swap
|
|
||||||
action sets the input side when no earlier step set it, even when it names
|
|
||||||
neither, as described below. A V2 exact-out step gives its `amountInMax`, and
|
|
||||||
a V4 swap step the `amountIn` of the swap action that spends its input
|
|
||||||
currency.
|
|
||||||
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's
|
- `Whatever an earlier step sent to the pair (V2 already paid)`: the swap's
|
||||||
`Amount` line, when the amount it shows is a V2 exact-in `amountIn` of zero.
|
`Amount` line, when the amount it shows is a V2 exact-in `amountIn` of zero.
|
||||||
The router reads that zero as "the pair already holds the input tokens": the
|
The router reads that zero as "the pair already holds the input tokens": the
|
||||||
@@ -2374,10 +2368,11 @@ view would leave a wallet one click from deletion.
|
|||||||
- Site origin (bold, scheme and port included) + "wants to send a
|
- Site origin (bold, scheme and port included) + "wants to send a
|
||||||
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 `PERMIT2_TRANSFER_FROM` moves and where to, steps,
|
amounts, what each swap step the user pays for spends of a token other
|
||||||
deadline, and unless a swap pays only the From address, a `Paid to` line
|
than `Token In`, what each `PERMIT2_TRANSFER_FROM` moves and where to,
|
||||||
for each other address it pays, or saying that a token stays in the router
|
steps, deadline, and unless a swap pays only the From address, a `Paid to`
|
||||||
or who it pays is unknown (see Transaction Decoding)
|
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
|
||||||
@@ -3127,24 +3122,48 @@ Currently supported:
|
|||||||
name); and one more reads `Unknown (could not be decoded)` when a step that
|
name); and one more reads `Unknown (could not be decoded)` when a step that
|
||||||
may pay an address went unread. When no step pays anyone, a single line reads
|
may pay an address went unread. When no step pays anyone, a single line reads
|
||||||
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
`Unknown (not named in the calldata)`. Wherever there is a `Paid to` line, the
|
||||||
minimum reads `Min. paid out`. After the `Amount` line (after `Token In` when
|
minimum reads `Min. paid out`. The `Amount` line gives the amount of the step
|
||||||
there is none), each `PERMIT2_TRANSFER_FROM` but one into the router, in step
|
that sets the input side: the last `PERMIT2_PERMIT` step if there is one,
|
||||||
order, gives three lines: `Moved from the sender`, the amount it moves, for
|
otherwise the first V2 or V3 exact-in, V2 exact-out, `WRAP_ETH` or V4 swap
|
||||||
example `1000000.0000 USDT`; `Token moved`, giving the token's address as the
|
step that names an input token or amount. A V4 step with a swap action sets
|
||||||
`Token In` line does; and `Moved to`, which reads
|
the input side when no earlier step set it, even when it names neither (see
|
||||||
`The V2 pair the next swap spends` for a hop and `The sender` for `address(1)`
|
Display Consistency). A V2 exact-out step gives its `amountInMax`, and a V4
|
||||||
on every router but `UniversalRouterV1`, and otherwise gives the address. A
|
swap step the `amountIn` of the swap action that spends its input currency.
|
||||||
move into the router needs none of them, because its `Paid to` line and
|
Each later swap step that the user pays for also takes its amount, found in
|
||||||
`Token moved into the router` line already give the amount, the token and
|
the same way, from the user's wallet: a V2 or V3 exact-in or V2 exact-out step
|
||||||
where it goes, and so its amount is shown once. These lines change neither the
|
whose `payerIsUser` is true, or a V4 step with a swap action whose input
|
||||||
`Paid to` lines nor the minimum's label. A step goes unread when its recipient
|
currency a `SETTLE` paid by the user (`payerIsUser` true) or a `SETTLE_ALL`
|
||||||
does not decode as an address, or when it is a command the decoder does not
|
names. When it spends `Token In`, its amount is added to the line's, unless a
|
||||||
read: every command other than `PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`,
|
`PERMIT2_PERMIT` set the input side, whose amount the line keeps. So a V3
|
||||||
`V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`,
|
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user,
|
||||||
`SWEEP`, `TRANSFER`, `PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`,
|
read `1000.0000 USDT`. When either amount is not a quantity (a V4 open delta,
|
||||||
`ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`, `UNWRAP_WETH` and `BALANCE_CHECK_ERC20`
|
`ALREADY_PAID`, or an amount the decoder did not read), the total is not known
|
||||||
(`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and
|
and there is no `Amount` line. A step paid from the router spends what the
|
||||||
the NFT commands among them), or a type its release names no command for.
|
router holds and adds nothing, and so does a V2 exact-in whose `amountIn` is
|
||||||
|
`ALREADY_PAID`, which takes nothing from the user. A step the user pays for
|
||||||
|
that spends a token other than `Token In` gives three lines after the `Amount`
|
||||||
|
line (after `Token In` when there is none), in step order:
|
||||||
|
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
|
||||||
|
`Unknown (could not be decoded)` when the decoder read none; `Token moved`,
|
||||||
|
giving the token's address as the `Token In` line does; and `Moved to`, which
|
||||||
|
reads `The swap step that spends it`. 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 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 where it goes, and so its amount is shown once.
|
||||||
|
These lines change neither the `Paid to` lines nor the minimum's label. A step
|
||||||
|
goes unread when its recipient does not decode as an address, or when it is a
|
||||||
|
command the decoder does not read: every command other than `PERMIT2_PERMIT`,
|
||||||
|
`PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
|
||||||
|
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`, `PAY_PORTION`,
|
||||||
|
`PAY_PORTION_FULL_PRECISION`, `ACROSS_V4_DEPOSIT_V3`, `WRAP_ETH`,
|
||||||
|
`UNWRAP_WETH` and `BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`,
|
||||||
|
`PERMIT2_TRANSFER_FROM_BATCH`, `EXECUTE_SUB_PLAN` and the NFT commands among
|
||||||
|
them), or a type its release names no command for.
|
||||||
|
|
||||||
### Non-Goals Forever
|
### Non-Goals Forever
|
||||||
|
|
||||||
|
|||||||
+114
-33
@@ -335,7 +335,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
|
|||||||
// commands this decoder does not read. That action names both of its
|
// commands this decoder does not read. That action names both of its
|
||||||
// currencies, and those commands may name one, so saying the calldata did not
|
// 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
|
// name it would be false. The Paid to line uses it too, when a step may pay an
|
||||||
// address this decoder did not read.
|
// address this decoder did not read, and so does the amount a swap step the
|
||||||
|
// user pays for spends of another token, when this decoder read none.
|
||||||
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
|
||||||
|
|
||||||
// The Paid to line for each token paid to the router, the swap's output or
|
// The Paid to line for each token paid to the router, the swap's output or
|
||||||
@@ -566,6 +567,7 @@ function decodeV2SwapExactIn(input) {
|
|||||||
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
||||||
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
||||||
// address[] path, bool payerIsUser)
|
// address[] path, bool payerIsUser)
|
||||||
|
// With `payerIsUser` false the router pays the input from its own balance.
|
||||||
function decodeV2SwapExactOut(input) {
|
function decodeV2SwapExactOut(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(
|
const d = coder.decode(
|
||||||
@@ -578,6 +580,7 @@ function decodeV2SwapExactOut(input) {
|
|||||||
amountInMax: d[2],
|
amountInMax: d[2],
|
||||||
tokenIn: d[3][0],
|
tokenIn: d[3][0],
|
||||||
tokenOut: d[3][d[3].length - 1],
|
tokenOut: d[3][d[3].length - 1],
|
||||||
|
payerIsUser: d[4],
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -904,7 +907,9 @@ function totalAmount(amounts) {
|
|||||||
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
// read. `settledFromRouter` is every SETTLE that the router pays from its own
|
||||||
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
// balance (`payerIsUser` false), as its currency and the amount it pays: a
|
||||||
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
|
||||||
// decoder cannot tell (see below).
|
// decoder cannot tell (see below). `payerIsUser` is true when the user pays
|
||||||
|
// the step's Token In: a SETTLE paid by the user (`payerIsUser` true) or a
|
||||||
|
// SETTLE_ALL, which V4Router always has the user pay, names it.
|
||||||
function decodeV4Swap(input) {
|
function decodeV4Swap(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||||
@@ -919,6 +924,8 @@ function decodeV4Swap(input) {
|
|||||||
const routerSettles = [];
|
const routerSettles = [];
|
||||||
// The currency of every SETTLE and SETTLE_ALL.
|
// The currency of every SETTLE and SETTLE_ALL.
|
||||||
const settledCurrencies = [];
|
const settledCurrencies = [];
|
||||||
|
// The currency of every SETTLE and SETTLE_ALL the user pays.
|
||||||
|
const settledByUser = [];
|
||||||
// The index of the step's last swap action, read or not, or -1.
|
// The index of the step's last swap action, read or not, or -1.
|
||||||
let lastSwapAction = -1;
|
let lastSwapAction = -1;
|
||||||
// Set when an action of the step could not be read.
|
// Set when an action of the step could not be read.
|
||||||
@@ -943,7 +950,9 @@ function decodeV4Swap(input) {
|
|||||||
);
|
);
|
||||||
settleToken = s[0];
|
settleToken = s[0];
|
||||||
settledCurrencies.push(s[0]);
|
settledCurrencies.push(s[0]);
|
||||||
if (!s[2]) {
|
if (s[2]) {
|
||||||
|
settledByUser.push(s[0]);
|
||||||
|
} else {
|
||||||
routerSettles.push({
|
routerSettles.push({
|
||||||
currency: s[0],
|
currency: s[0],
|
||||||
amount: s[1],
|
amount: s[1],
|
||||||
@@ -955,6 +964,7 @@ function decodeV4Swap(input) {
|
|||||||
const s = coder.decode(["address", "uint256"], params[i]);
|
const s = coder.decode(["address", "uint256"], params[i]);
|
||||||
settleToken = s[0];
|
settleToken = s[0];
|
||||||
settledCurrencies.push(s[0]);
|
settledCurrencies.push(s[0]);
|
||||||
|
settledByUser.push(s[0]);
|
||||||
} else if (actionId === V4_TAKE) {
|
} else if (actionId === V4_TAKE) {
|
||||||
// TAKE: (address currency, address recipient, uint256 amount)
|
// TAKE: (address currency, address recipient, uint256 amount)
|
||||||
const t = coder.decode(
|
const t = coder.decode(
|
||||||
@@ -1055,6 +1065,7 @@ function decodeV4Swap(input) {
|
|||||||
recipientUnread,
|
recipientUnread,
|
||||||
swapUndecoded,
|
swapUndecoded,
|
||||||
hasSwapAction: true,
|
hasSwapAction: true,
|
||||||
|
payerIsUser: settledByUser.includes(settleToken),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,6 +1108,7 @@ function decodeV4Swap(input) {
|
|||||||
recipientUnread,
|
recipientUnread,
|
||||||
swapUndecoded,
|
swapUndecoded,
|
||||||
hasSwapAction: swaps.length > 0,
|
hasSwapAction: swaps.length > 0,
|
||||||
|
payerIsUser: present(tokenIn) && settledByUser.includes(tokenIn),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -1125,6 +1137,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
let inputToken = null;
|
let inputToken = null;
|
||||||
let inputAmount = null;
|
let inputAmount = null;
|
||||||
let inputEstablished = false;
|
let inputEstablished = false;
|
||||||
|
let inputFromPermit = false;
|
||||||
let outputToken = null;
|
let outputToken = null;
|
||||||
let minOutput = null;
|
let minOutput = null;
|
||||||
let outputEstablished = false;
|
let outputEstablished = false;
|
||||||
@@ -1135,8 +1148,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
const commandNames = [];
|
const commandNames = [];
|
||||||
|
|
||||||
// THE INVARIANT: an amount and the token it is counted in always come
|
// THE INVARIANT: an amount and the token it is counted in always come
|
||||||
// from the same hop. A figure is never rendered against a token that
|
// from the same hop, or from hops that all spend that token. A figure
|
||||||
// did not supply it.
|
// is never rendered against a token that did not supply it.
|
||||||
//
|
//
|
||||||
// Both sides are therefore set as a PAIR — never field by field, and
|
// Both sides are therefore set as a PAIR — never field by field, and
|
||||||
// never on truthiness. An address is never falsy once set but an
|
// never on truthiness. An address is never falsy once set but an
|
||||||
@@ -1152,7 +1165,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// states neither half: the output side always, and the input side
|
// states neither half: the output side always, and the input side
|
||||||
// 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.
|
// from a different hop. Each later swap step the user pays for adds
|
||||||
|
// what it spends, in addUserPaid(), the one place the total is formed.
|
||||||
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;
|
||||||
@@ -1170,6 +1184,33 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// What each swap step the user pays for spends of a token other than
|
||||||
|
// the input side's, as { token, amount }, in step order.
|
||||||
|
const otherTokensSpent = [];
|
||||||
|
// A swap step the user pays for (`payerIsUser` true), after the input
|
||||||
|
// side is set, takes `amount` of `token` from the sender. Of the input
|
||||||
|
// side's token it adds to the input side's amount, so the Amount line
|
||||||
|
// is what the steps spend of it together; when either amount is not a
|
||||||
|
// quantity, the total is not known and the amount is null. Of another
|
||||||
|
// token it goes in `otherTokensSpent`. A V2 exact-in of ALREADY_PAID
|
||||||
|
// takes nothing from the sender, and a permit's amount stays as the
|
||||||
|
// permit states it.
|
||||||
|
const addUserPaid = (token, amount) => {
|
||||||
|
if (amount === ALREADY_PAID) return;
|
||||||
|
const sameToken =
|
||||||
|
present(inputToken) &&
|
||||||
|
token.toLowerCase() === inputToken.toLowerCase();
|
||||||
|
if (!sameToken) {
|
||||||
|
otherTokensSpent.push({ token, amount });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inputFromPermit) return;
|
||||||
|
inputAmount =
|
||||||
|
typeof inputAmount === "bigint" && typeof amount === "bigint"
|
||||||
|
? inputAmount + amount
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
// Who the steps pay, in whichever step and of whatever token.
|
// Who the steps pay, in whichever step and of whatever token.
|
||||||
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
|
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
|
||||||
// its own address, holds what it is paid until a later step pays it
|
// its own address, holds what it is paid until a later step pays it
|
||||||
@@ -1309,7 +1350,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// A permit states both halves itself, so it may replace an
|
// A permit states both halves itself, so it may replace an
|
||||||
// input side an earlier hop established without breaking
|
// input side an earlier hop established without breaking
|
||||||
// the invariant.
|
// the invariant.
|
||||||
if (p) setInput(p.token, p.amount);
|
if (p) {
|
||||||
|
setInput(p.token, p.amount);
|
||||||
|
inputFromPermit = true;
|
||||||
|
}
|
||||||
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
} else if (command === "PERMIT2_TRANSFER_FROM") {
|
||||||
const p = decodePermit2TransferFrom(inputs[i]);
|
const p = decodePermit2TransferFrom(inputs[i]);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
@@ -1375,7 +1419,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||||
const s = decodeV3SwapExactIn(inputs[i]);
|
const s = decodeV3SwapExactIn(inputs[i]);
|
||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountIn);
|
if (!inputEstablished) {
|
||||||
|
setInputOnce(s.tokenIn, s.amountIn);
|
||||||
|
} else if (s.payerIsUser) {
|
||||||
|
addUserPaid(s.tokenIn, s.amountIn);
|
||||||
|
}
|
||||||
// Always update output: in multi-step swaps (V3 → V4),
|
// Always update output: in multi-step swaps (V3 → V4),
|
||||||
// the last swap step determines the final output token
|
// the last swap step determines the final output token
|
||||||
// and minimum received amount.
|
// and minimum received amount.
|
||||||
@@ -1392,7 +1440,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (command === "V2_SWAP_EXACT_IN") {
|
} else if (command === "V2_SWAP_EXACT_IN") {
|
||||||
const s = decodeV2SwapExactIn(inputs[i]);
|
const s = decodeV2SwapExactIn(inputs[i]);
|
||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountIn);
|
if (!inputEstablished) {
|
||||||
|
setInputOnce(s.tokenIn, s.amountIn);
|
||||||
|
} else if (s.payerIsUser) {
|
||||||
|
addUserPaid(s.tokenIn, s.amountIn);
|
||||||
|
}
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
@@ -1408,7 +1460,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
hasV2ExactOut = true;
|
hasV2ExactOut = true;
|
||||||
const s = decodeV2SwapExactOut(inputs[i]);
|
const s = decodeV2SwapExactOut(inputs[i]);
|
||||||
if (s) {
|
if (s) {
|
||||||
setInputOnce(s.tokenIn, s.amountInMax);
|
if (!inputEstablished) {
|
||||||
|
setInputOnce(s.tokenIn, s.amountInMax);
|
||||||
|
} else if (s.payerIsUser) {
|
||||||
|
addUserPaid(s.tokenIn, s.amountInMax);
|
||||||
|
}
|
||||||
setOutput(s.tokenOut, s.amountOut);
|
setOutput(s.tokenOut, s.amountOut);
|
||||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
@@ -1550,6 +1606,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// other half.
|
// other half.
|
||||||
if (!inputEstablished) {
|
if (!inputEstablished) {
|
||||||
setInput(v4.tokenIn, v4.amountIn);
|
setInput(v4.tokenIn, v4.amountIn);
|
||||||
|
} else if (v4.payerIsUser) {
|
||||||
|
addUserPaid(v4.tokenIn, v4.amountIn);
|
||||||
}
|
}
|
||||||
outputToken = v4.tokenOut;
|
outputToken = v4.tokenOut;
|
||||||
minOutput = v4.amountOutMin;
|
minOutput = v4.amountOutMin;
|
||||||
@@ -1672,37 +1730,43 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (present(inputAmount)) {
|
// The text of an amount the swap steps spend, for the Amount line and
|
||||||
// Three amounts need no scale to describe and are named rather
|
// the lines for other tokens below it. Three amounts need no scale to
|
||||||
// than formatted: V4's open delta and V2's already-paid zero,
|
// describe and are named rather than formatted: V4's open delta and
|
||||||
// neither of which is a quantity at all (see OPEN_DELTA and
|
// V2's already-paid zero, neither of which is a quantity at all (see
|
||||||
// ALREADY_PAID), and an unbounded permit. Those two tests come
|
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
||||||
// first — the sentinels are not bigints and cannot be compared
|
// tests come first — the sentinels are not bigints and cannot be
|
||||||
// with one.
|
// compared with one.
|
||||||
let amount;
|
const spentText = (amount, info) => {
|
||||||
if (inputAmount === OPEN_DELTA) {
|
if (amount === OPEN_DELTA) {
|
||||||
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||||
} else if (inputAmount === ALREADY_PAID) {
|
}
|
||||||
amount = {
|
if (amount === ALREADY_PAID) {
|
||||||
|
return {
|
||||||
raw: ALREADY_PAID_AMOUNT,
|
raw: ALREADY_PAID_AMOUNT,
|
||||||
display: ALREADY_PAID_AMOUNT,
|
display: ALREADY_PAID_AMOUNT,
|
||||||
};
|
};
|
||||||
} else if (inputAmount >= MAX_UINT160) {
|
}
|
||||||
amount = { raw: "Unlimited", display: "Unlimited" };
|
if (amount >= MAX_UINT160) {
|
||||||
} else if (hasV2ExactOut) {
|
return { raw: "Unlimited", display: "Unlimited" };
|
||||||
|
}
|
||||||
|
if (hasV2ExactOut) {
|
||||||
// A V2 exact-out swap spends at most this figure, whichever
|
// A V2 exact-out swap spends at most this figure, whichever
|
||||||
// step set the line (its amountInMax, the WRAP_ETH of a swap
|
// steps gave it (an amountInMax, the WRAP_ETH of a swap paid
|
||||||
// paid in ETH, a permit), so it is said to be a maximum, in
|
// in ETH, a permit, or several steps' total), so it is said to
|
||||||
// `raw` too: the wait, success and error screens show `raw` as
|
// be a maximum, in `raw` too: the wait, success and error
|
||||||
// the transaction's amount.
|
// screens show `raw` as the transaction's amount.
|
||||||
const most = amountText(inputAmount, inInfo);
|
const most = amountText(amount, info);
|
||||||
amount = {
|
return {
|
||||||
raw: "Up to " + most.raw,
|
raw: "Up to " + most.raw,
|
||||||
display: "Up to " + most.display,
|
display: "Up to " + most.display,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
amount = amountText(inputAmount, inInfo);
|
|
||||||
}
|
}
|
||||||
|
return amountText(amount, info);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (present(inputAmount)) {
|
||||||
|
const amount = spentText(inputAmount, inInfo);
|
||||||
details.push({
|
details.push({
|
||||||
label: "Amount",
|
label: "Amount",
|
||||||
value: amount.display,
|
value: amount.display,
|
||||||
@@ -1710,6 +1774,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What each swap step the user pays for spends of another token, in
|
||||||
|
// the shape of the lines below for a PERMIT2_TRANSFER_FROM. An amount
|
||||||
|
// this decoder did not read is said so.
|
||||||
|
for (const s of otherTokensSpent) {
|
||||||
|
details.push({
|
||||||
|
label: "Moved from the sender",
|
||||||
|
value: present(s.amount)
|
||||||
|
? spentText(s.amount, tokenInfo(s.token, sources)).display
|
||||||
|
: UNDECODED_CURRENCY,
|
||||||
|
});
|
||||||
|
details.push(tokenMovedLine("Token moved", s.token, sources));
|
||||||
|
details.push({
|
||||||
|
label: "Moved to",
|
||||||
|
value: "The swap step that spends it",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
|
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
|
||||||
// where it goes, an address on a line of its own.
|
// where it goes, an address on a line of its own.
|
||||||
for (const m of movedFromSender) {
|
for (const m of movedFromSender) {
|
||||||
|
|||||||
@@ -1289,6 +1289,194 @@ describe("uniswap decoder", () => {
|
|||||||
expect(detail(result, "Amount").value).not.toContain("500000000000");
|
expect(detail(result, "Amount").value).not.toContain("500000000000");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/625 — a route split across
|
||||||
|
// two swap steps the user pays for, with no PERMIT2_PERMIT, takes both
|
||||||
|
// steps' amounts from the wallet. On next at 671b4d3 the Amount line read
|
||||||
|
// only the first step's 600.0000 USDT.
|
||||||
|
describe("a swap split across steps the user pays for", () => {
|
||||||
|
const USDT_600 = 600000000n;
|
||||||
|
const USDT_400 = 400000000n;
|
||||||
|
|
||||||
|
// The lines from Amount on, as [label, value].
|
||||||
|
function linesFromAmount(result, count) {
|
||||||
|
const lines = result.details.map((d) => [d.label, d.value]);
|
||||||
|
const at = lines.findIndex(([label]) => label === "Amount");
|
||||||
|
return lines.slice(at, at + count);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a split across a V3 and a V2 step the user pays for shows the total", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
|
expect(detail(result, "Token In").address.toLowerCase()).toBe(
|
||||||
|
USDT_ADDR.toLowerCase(),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||||
|
expect(detail(result, "Amount").rawValue).toBe("1000.0000");
|
||||||
|
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a split across a V3 step and a V4 step the user pays for shows the total", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x00, 0x10]),
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
V4_SETTLE_ID,
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_TAKE_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
encodeV4Settle(USDT_ADDR), // paid by the user
|
||||||
|
encodeV4ExactInSingle(
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
USDT_400,
|
||||||
|
390000000n,
|
||||||
|
),
|
||||||
|
encodeV4Take(USDC_ADDR),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
|
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||||
|
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a second step the user pays for that spends another token shows that token's amount", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x00, 0x08]),
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
encodeV2SwapExactIn(
|
||||||
|
USER_ADDR,
|
||||||
|
400000000000000000000n, // 400 DAI
|
||||||
|
390000000n,
|
||||||
|
[DAI_ADDR, USDC_ADDR],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
|
expect(linesFromAmount(result, 4)).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"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A PERMIT2_PERMIT that sets the input side keeps its own amount: the
|
||||||
|
// steps it lets the router spend do not add to it.
|
||||||
|
test("a split after a PERMIT2_PERMIT shows the permit's amount", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x00, 0x08]),
|
||||||
|
[
|
||||||
|
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR), // 5000 USDT
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
|
expect(detail(result, "Amount").value).toBe("5000.0000 USDT");
|
||||||
|
expect(detail(result, "Moved from the sender")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// A V2 exact-out step makes every amount the swap steps spend a maximum,
|
||||||
|
// the total included.
|
||||||
|
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {
|
||||||
|
const data = buildExecute(
|
||||||
|
solidityPacked(["uint8", "uint8"], [0x00, 0x09]),
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(USER_ADDR, USDT_600, 590000000n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
encodeV2SwapExactOut(USER_ADDR, 390000000n, USDT_400, [
|
||||||
|
USDT_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
|
||||||
|
expect(detail(result, "Amount").value).toBe("Up to 1000.0000 USDT");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
||||||
// the shape the issue measured: a V4 step that states a minimum of zero
|
// the shape the issue measured: a V4 step that states a minimum of zero
|
||||||
// and names no output currency.
|
// and names no output currency.
|
||||||
|
|||||||
@@ -671,6 +671,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
v3SwapExactIn(MSG_SENDER, USDT_2000, USDC_1990, USDT, USDC),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "2000.0000 USDT",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
|
kind: "a WRAP_ETH of 1 ETH into the router, then a swap of 0.5 WETH from the router",
|
||||||
@@ -697,6 +698,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
v2Swap(0x08, MSG_SENDER, ONE_WETH, USDC_1990, [WETH, USDC]),
|
v2Swap(0x08, MSG_SENDER, ONE_WETH, USDC_1990, [WETH, USDC]),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "1.0000 WETH",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V3 swap of 1 WETH that the user pays for",
|
kind: "a WRAP_ETH of 1 ETH into the router, then a V3 swap of 1 WETH that the user pays for",
|
||||||
@@ -705,6 +707,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
v3SwapExactIn(MSG_SENDER, ONE_WETH, USDC_1990, WETH, USDC),
|
v3SwapExactIn(MSG_SENDER, ONE_WETH, USDC_1990, WETH, USDC),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "1.0000 WETH",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE of 1 WETH by the user",
|
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 1 WETH with a SETTLE of 1 WETH by the user",
|
||||||
@@ -717,6 +720,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "1.0000 WETH",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a swap paying WETH to the router, a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH from the router",
|
kind: "a swap paying WETH to the router, a WRAP_ETH of 1 ETH into the router, then a swap of 1 WETH from the router",
|
||||||
@@ -792,6 +796,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "1.0000 WETH",
|
||||||
},
|
},
|
||||||
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
|
// The user pays for the swap by a SETTLE_ALL or SETTLE whose currency
|
||||||
// the decoder cannot read, so the SETTLE from the router pays nothing.
|
// the decoder cannot read, so the SETTLE from the router pays nothing.
|
||||||
@@ -873,12 +878,17 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
left: { label: "Token moved into the router", value: "ETH" },
|
left: { label: "Token moved into the router", value: "ETH" },
|
||||||
},
|
},
|
||||||
])("$kind", ({ data, left }) => {
|
])("$kind", ({ data, left, spent }) => {
|
||||||
const result = decode(data);
|
const result = decode(data);
|
||||||
expect(labels(result)).toEqual([
|
expect(labels(result)).toEqual([
|
||||||
"Protocol",
|
"Protocol",
|
||||||
"Token In",
|
"Token In",
|
||||||
"Amount",
|
"Amount",
|
||||||
|
// A later swap step the user pays for spends `spent` of a token
|
||||||
|
// other than Token In (https://git.eeqj.de/sneak/AutistMask/issues/625).
|
||||||
|
...(spent
|
||||||
|
? ["Moved from the sender", "Token moved", "Moved to"]
|
||||||
|
: []),
|
||||||
"Token Out",
|
"Token Out",
|
||||||
"Paid to",
|
"Paid to",
|
||||||
"Token moved into the router",
|
"Token moved into the router",
|
||||||
@@ -886,6 +896,9 @@ describe("any token left in the router is not received", () => {
|
|||||||
"Steps",
|
"Steps",
|
||||||
"Deadline",
|
"Deadline",
|
||||||
]);
|
]);
|
||||||
|
if (spent) {
|
||||||
|
expect(detail(result, "Moved from the sender").value).toBe(spent);
|
||||||
|
}
|
||||||
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||||
expect(detail(result, "Token moved into the router")).toEqual(left);
|
expect(detail(result, "Token moved into the router")).toEqual(left);
|
||||||
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
||||||
|
|||||||
Reference in New Issue
Block a user