fix: the swap decoder names where a WRAP_ETH pays and counts the router's ETH in a V4 settle (closes #665)
A WRAP_ETH that sets the input side and pays its WETH to anyone but the router now names that recipient on its Moved to line, or reads The sender for address(1). V4Router pays ETH from the router's own ETH, whoever a SETTLE names as payer. ETH the router already holds now pays first for the ETH a V4 step would take from the user, and only the rest is moved from the sender. An UNWRAP_WETH into the router keeps a known WETH amount as the ETH the router then holds. Model: opus-5-5
This commit is contained in:
@@ -3412,43 +3412,47 @@ Currently supported:
|
|||||||
as a swap paid in ETH does, but only when that amount is not
|
as a swap paid in ETH does, but only when that amount is not
|
||||||
`CONTRACT_BALANCE` and the router held no WETH from an earlier step when the
|
`CONTRACT_BALANCE` and the router held no WETH from an earlier step when the
|
||||||
`WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1
|
`WRAP_ETH` ran: after two `WRAP_ETH`s of 0.5 ETH into the router, a swap of 1
|
||||||
WETH from it reads as leaving the WETH there. A V2 or V3 exact-out step paid
|
WETH from it reads as leaving the WETH there. An `UNWRAP_WETH` into the router
|
||||||
from the router spends an amount the calldata does not state, at most its
|
turns all the router's WETH into as much ETH. The decoder then knows how much
|
||||||
`amountInMax`, so the decoder counts it as spending nothing the router holds.
|
ETH the router holds only when it knew the WETH's amount and the router held
|
||||||
A `SETTLE` of `OPEN_DELTA` (zero), and a `SETTLE_ALL`, pay only what the step
|
no ETH before. A V4 `SETTLE` of ETH spends the ETH the router holds, whoever
|
||||||
still owes of their token when they run. The decoder takes that to be the
|
pays it (see below). A V2 or V3 exact-out step paid from the router spends an
|
||||||
amounts the step's swap actions state they spend of that token only when every
|
amount the calldata does not state, at most its `amountInMax`, so the decoder
|
||||||
action in the step could be read, every swap action names the token it spends
|
counts it as spending nothing the router holds. A `SETTLE` of `OPEN_DELTA`
|
||||||
and the token it buys (a swap action with an empty path swaps nothing), the
|
(zero), and a `SETTLE_ALL`, pay only what the step still owes of their token
|
||||||
`SETTLE` or `SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that
|
when they run. The decoder takes that to be the amounts the step's swap
|
||||||
token and comes after every swap action, no swap action buys that token, no
|
actions state they spend of that token only when every action in the step
|
||||||
`TAKE`, `TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a
|
could be read, every swap action names the token it spends and the token it
|
||||||
stated amount adds to what the step owes), and the swap actions that spend it
|
buys (a swap action with an empty path swaps nothing), the `SETTLE` or
|
||||||
state a quantity together; otherwise it does not know the amount, and such a
|
`SETTLE_ALL` is the step's only `SETTLE` or `SETTLE_ALL` of that token and
|
||||||
`SETTLE` from the router counts as spending nothing. So the rest of a V4
|
comes after every swap action, no swap action buys that token, no `TAKE`,
|
||||||
`SETTLE` of more than its swap spends, taken to the router, the WETH of a
|
`TAKE_ALL` or `TAKE_PORTION` in the step takes it (a `TAKE` of a stated amount
|
||||||
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each
|
adds to what the step owes), and the swap actions that spend it state a
|
||||||
stays in the router when later steps do not spend all of it, where anyone can
|
quantity together; otherwise it does not know the amount, and such a `SETTLE`
|
||||||
take it in a later transaction. The router sends ETH as written, so a
|
from the router counts as spending nothing. So the rest of a V4 `SETTLE` of
|
||||||
`TRANSFER` of `CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A
|
more than its swap spends, taken to the router, the WETH of a `WRAP_ETH`, and
|
||||||
step paying a token to the V2 pair that the very next step spends, as a V2
|
a token a swap pays the router on the way to the output, each stays in the
|
||||||
exact-in of that token whose `amountIn` is `ALREADY_PAID` (zero) or
|
router when later steps do not spend all of it, where anyone can take it in a
|
||||||
`CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes pay the
|
later transaction. The router sends ETH as written, so a `TRANSFER` of
|
||||||
next V2 pair directly, and the router enforces that swap's own minimum. The
|
`CONTRACT_BALANCE` of ETH always reverts and pays out nothing. A step paying a
|
||||||
decoder checks that the address paid is that pair, deriving it from the V2
|
token to the V2 pair that the very next step spends, as a V2 exact-in of that
|
||||||
factory and pair init code hash the network's routers were deployed with and
|
token whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a
|
||||||
the first two tokens of that step's path; a payment to any other address is
|
hop, not a payout: Uniswap's mixed routes pay the next V2 pair directly, and
|
||||||
named. (Sepolia's `UniversalRouterV1_2_V2Support` was deployed with
|
the router enforces that swap's own minimum. The decoder checks that the
|
||||||
placeholders for both values, so no V2 swap step can succeed on that router.)
|
address paid is that pair, deriving it from the V2 factory and pair init code
|
||||||
Every other payment counts, in whichever step and of whatever token, a share
|
hash the network's routers were deployed with and the first two tokens of that
|
||||||
of the output included: an interface fee paid to another address by
|
step's path; a payment to any other address is named. (Sepolia's
|
||||||
`PAY_PORTION`, `TRANSFER` or `TAKE_PORTION` is named, and the minimum reads
|
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both
|
||||||
`Min. paid out`, because the swap's minimum includes the fee. The minimum
|
values, so no V2 swap step can succeed on that router.) Every other payment
|
||||||
reads `Min. received` only when some step pays the transaction's sender (the
|
counts, in whichever step and of whatever token, a share of the output
|
||||||
From address on TxApproval, the sender the explorer reports on
|
included: an interface fee paid to another address by `PAY_PORTION`,
|
||||||
TransactionDetail), no `Paid to` line is shown, and no permit or batch grants
|
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`,
|
||||||
an allowance to a spender other than the router (see below). `Paid to` lines
|
because the swap's minimum includes the fee. The minimum reads `Min. received`
|
||||||
after `Token Out` name each other address paid; one reads
|
only when some step pays the transaction's sender (the From address on
|
||||||
|
TxApproval, the sender the explorer reports on TransactionDetail), no
|
||||||
|
`Paid to` line is shown, and no permit or batch grants an allowance to a
|
||||||
|
spender other than the router (see below). `Paid to` lines after `Token Out`
|
||||||
|
name each other address paid; one reads
|
||||||
`The bridge, which pays the deposit's recipient on another network` for a
|
`The bridge, which pays the deposit's recipient on another network` for a
|
||||||
bridge deposit; one reads, for example,
|
bridge deposit; one reads, for example,
|
||||||
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
|
`The router, 2000.0000 USDT from the sender (later steps may not spend all of it)`
|
||||||
@@ -3477,56 +3481,62 @@ Currently supported:
|
|||||||
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
|
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
|
||||||
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
|
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
|
||||||
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
|
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
|
||||||
rest taken to another address, reads `5000.0000 USDT`. A later step paid from
|
rest taken to another address, reads `5000.0000 USDT`. V4 pays ETH from the
|
||||||
the router spends what the router holds and takes nothing, and so does a V2
|
router's own ETH, whoever a `SETTLE` names as payer. So when a V4 step would
|
||||||
exact-in whose `amountIn` is `ALREADY_PAID`. A later take of `Token In` is
|
take ETH from the user's wallet, ETH the router already holds pays first. The
|
||||||
added to the total while both are quantities: a V3 exact-in of 600 USDT and a
|
wallet pays only the rest. For example, an `UNWRAP_WETH` into the router
|
||||||
V2 exact-in of 400 USDT, both paid by the user, read `1000.0000 USDT`. When
|
leaves ETH there. When the decoder does not know how much ETH the router
|
||||||
the decoder does not know a take's amount, the total is not known and there is
|
holds, or how much the step would take, it counts the router as paying all of
|
||||||
no `Amount` line. A `PERMIT2_PERMIT` names `Token In` only when its amount,
|
that ETH. A later step paid from the router spends what the router holds and
|
||||||
`Unlimited` included, stands as the `Amount` figure in place of the total,
|
takes nothing, and so does a V2 exact-in whose `amountIn` is `ALREADY_PAID`. A
|
||||||
which it does only in the plain shape of Uniswap's own routes: it comes before
|
later take of `Token In` is added to the total while both are quantities: a V3
|
||||||
the step that sets the input side, it is the only `PERMIT2_PERMIT` in the
|
exact-in of 600 USDT and a V2 exact-in of 400 USDT, both paid by the user,
|
||||||
transaction and there is no `PERMIT2_PERMIT_BATCH`, its spender is the router,
|
read `1000.0000 USDT`. When the decoder does not know a take's amount, the
|
||||||
the router may not let it fail, no step before it takes its token, a
|
total is not known and there is no `Amount` line. A `PERMIT2_PERMIT` names
|
||||||
`PERMIT2_TRANSFER_FROM` of it included, or a token the decoder does not know,
|
`Token In` only when its amount, `Unlimited` included, stands as the `Amount`
|
||||||
and the decoder reads every step after it in full. Permit2 then lets the
|
figure in place of the total, which it does only in the plain shape of
|
||||||
router take no more of the token than that amount, so the permit covers every
|
Uniswap's own routes: it comes before the step that sets the input side, it is
|
||||||
take of it. A step before the permit takes under whatever allowance the router
|
the only `PERMIT2_PERMIT` in the transaction and there is no
|
||||||
already had, so it may take more than the permit's amount, which would then
|
`PERMIT2_PERMIT_BATCH`, its spender is the router, the router may not let it
|
||||||
read lower than what leaves the user's wallet: a `PERMIT2_TRANSFER_FROM` of
|
fail, no step before it takes its token, a `PERMIT2_TRANSFER_FROM` of it
|
||||||
2,000 USDT into the router, then a permit of 600 USDT to the router and a V3
|
included, or a token the decoder does not know, and the decoder reads every
|
||||||
exact-in of 2,000 USDT paid from the router, read `Amount 2000.0000 USDT`. A
|
step after it in full. Permit2 then lets the router take no more of the token
|
||||||
later permit or batch may raise what Permit2 lets the router take, and so may
|
than that amount, so the permit covers every take of it. A step before the
|
||||||
a step the decoder does not read in full: a command the decoder does not read
|
permit takes under whatever allowance the router already had, so it may take
|
||||||
(listed at the end of this section), a step whose input it cannot read, in
|
more than the permit's amount, which would then read lower than what leaves
|
||||||
whole or in part, and a step it fails on. Every other `PERMIT2_PERMIT`, before
|
the user's wallet: a `PERMIT2_TRANSFER_FROM` of 2,000 USDT into the router,
|
||||||
the step that sets the input side or after it, leaves `Token In` and the
|
then a permit of 600 USDT to the router and a V3 exact-in of 2,000 USDT paid
|
||||||
`Amount` line as they read with no permit, unless it lets a spender other than
|
from the router, read `Amount 2000.0000 USDT`. A later permit or batch may
|
||||||
the router take `Token In` (see below): a permit of 5,000 DAI to another
|
raise what Permit2 lets the router take, and so may a step the decoder does
|
||||||
address and then a V3 exact-in of 600 USDT read `Swap USDT → USDC` with
|
not read in full: a command the decoder does not read (listed at the end of
|
||||||
`Amount 600.0000 USDT`, and so do that exact-in and then a permit of 5,000 DAI
|
this section), a step whose input it cannot read, in whole or in part, and a
|
||||||
to the router. When a permit's amount stands and the step that sets the input
|
step it fails on. Every other `PERMIT2_PERMIT`, before the step that sets the
|
||||||
side spends a token other than the permit's, or one the decoder does not know,
|
input side or after it, leaves `Token In` and the `Amount` line as they read
|
||||||
that step's amount gets lines of its own, unless it states no amount and the
|
with no permit, unless it lets a spender other than the router take `Token In`
|
||||||
user pays it none of its input token. Each take the total does not count gives
|
(see below): a permit of 5,000 DAI to another address and then a V3 exact-in
|
||||||
three lines after the `Amount` line (after `Token In` when there is none): one
|
of 600 USDT read `Swap USDT → USDC` with `Amount 600.0000 USDT`, and so do
|
||||||
of a token other than `Token In`; one of `Token In` after an amount that is
|
that exact-in and then a permit of 5,000 DAI to the router. When a permit's
|
||||||
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
|
amount stands and the step that sets the input side spends a token other than
|
||||||
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
|
the permit's, or one the decoder does not know, that step's amount gets lines
|
||||||
whose amount the decoder does not know. When there is no `Amount` line, each
|
of its own, unless it states no amount and the user pays it none of its input
|
||||||
take the total counts gets them too, so that every amount the decoder reads is
|
token. Each take the total does not count gives three lines after the `Amount`
|
||||||
shown: the amount the step that sets the input side states, when it is a
|
line (after `Token In` when there is none): one of a token other than
|
||||||
quantity and the user pays that step (a V2 or V3 step whose `payerIsUser` is
|
`Token In`; one of `Token In` after an amount that is not a quantity (a V4
|
||||||
true, a V4 step the user pays some of its input token, or a `WRAP_ETH`, whose
|
open delta, `ALREADY_PAID`, which keep their wording on the `Amount` line, or
|
||||||
ETH comes from the transaction's value), and each later take of `Token In`
|
no stated amount, as of a V4 exact-out step); and one whose amount the decoder
|
||||||
added to it. A step that sets the input side and is paid from the router takes
|
does not know. When there is no `Amount` line, each take the total counts gets
|
||||||
nothing from the user's wallet and gets none. A `PERMIT2_PERMIT` the decoder
|
them too, so that every amount the decoder reads is shown: the amount the step
|
||||||
cannot read, then V3 exact-ins of 600 USDT and 400 DAI paid by the user, give
|
that sets the input side states, when it is a quantity and the user pays that
|
||||||
lines for both amounts. When the step that sets the input side states no
|
step (a V2 or V3 step whose `payerIsUser` is true, a V4 step the user pays
|
||||||
amount, the user pays it some of its input token, and there is no `Amount`
|
some of its input token, or a `WRAP_ETH`, whose ETH comes from the
|
||||||
line, that step gets them too whenever another take's lines give a figure, or
|
transaction's value), and each later take of `Token In` added to it. A step
|
||||||
a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into
|
that sets the input side and is paid from the router takes nothing from the
|
||||||
|
user's wallet and gets none. A `PERMIT2_PERMIT` the decoder cannot read, then
|
||||||
|
V3 exact-ins of 600 USDT and 400 DAI paid by the user, give lines for both
|
||||||
|
amounts. When the step that sets the input side states no amount, the user
|
||||||
|
pays it some of its input token, and there is no `Amount` line, that step gets
|
||||||
|
them too whenever another take's lines give a figure, or a
|
||||||
|
`PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into
|
||||||
the router, on its `Paid to` line, so that figure cannot read as all that
|
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,
|
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
|
as when the router pays it, that step takes none of that token from the user's
|
||||||
@@ -3536,11 +3546,13 @@ Currently supported:
|
|||||||
`Moved from the sender`, its 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`. For a `WRAP_ETH` that
|
||||||
does not know gives the `Moved from the sender` line alone. A step may also
|
pays its WETH to anyone but the router, `Moved to` names that recipient
|
||||||
take from the user's wallet what the decoder did not read: a V4 step whose
|
instead: `The sender` for `address(1)`, and otherwise the address. A token the
|
||||||
input or one of whose `SETTLE`s or `SETTLE_ALL`s it cannot read, which the
|
decoder does not know gives the `Moved from the sender` line alone. A step may
|
||||||
router still runs; a V2 or V3 exact-in or exact-out step or a
|
also take from the user's wallet what the decoder did not read: a V4 step
|
||||||
|
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 exact-out step or a
|
||||||
`PERMIT2_TRANSFER_FROM` whose input it cannot read, such as a V3 step whose
|
`PERMIT2_TRANSFER_FROM` whose input it cannot read, such as a V3 step whose
|
||||||
path is shorter than one token address or has bytes after its last hop; every
|
path is shorter than one token address or has bytes after its last hop; every
|
||||||
`PERMIT2_TRANSFER_FROM_BATCH`; every `EXECUTE_SUB_PLAN`, wherever it stands;
|
`PERMIT2_TRANSFER_FROM_BATCH`; every `EXECUTE_SUB_PLAN`, wherever it stands;
|
||||||
|
|||||||
+103
-32
@@ -1244,6 +1244,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// comes from the transaction's value. A step paid from the router
|
// comes from the transaction's value. A step paid from the router
|
||||||
// takes nothing from the sender.
|
// takes nothing from the sender.
|
||||||
let inputPaidByUser = false;
|
let inputPaidByUser = false;
|
||||||
|
// The recipient of a WRAP_ETH that set the input side, as written,
|
||||||
|
// when it is not the router; otherwise null. That WRAP_ETH pays its
|
||||||
|
// WETH there, not to a swap step (see the Moved to lines).
|
||||||
|
let wrapEthRecipient = null;
|
||||||
// The last PERMIT2_PERMIT this decoder read before a step set the
|
// The last PERMIT2_PERMIT this decoder read before a step set the
|
||||||
// input side, or null (see permitStands after the loop).
|
// input side, or null (see permitStands after the loop).
|
||||||
let permit = null;
|
let permit = null;
|
||||||
@@ -1399,9 +1403,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
|
// token, an UNWRAP_WETH of WETH, a WRAP_ETH of CONTRACT_BALANCE of
|
||||||
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
|
// ETH, a TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION or
|
||||||
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
|
// ACROSS_V4_DEPOSIT_V3 of all of it, or a swap step or V4 SETTLE paid
|
||||||
// from the router (see spendFromRouter()). The value is the amount the
|
// from the router (see spendFromRouter()). A V4 SETTLE of ETH paid by
|
||||||
// router holds, in base units, when a WRAP_ETH stated it, otherwise
|
// the user spends the router's ETH too (see the V4_SWAP branch). The
|
||||||
// null.
|
// value is the amount the router holds, in base units, when a
|
||||||
|
// WRAP_ETH stated it, otherwise null. An UNWRAP_WETH into the router
|
||||||
|
// turns that amount of WETH into the same amount of ETH.
|
||||||
const heldByRouter = new Map();
|
const heldByRouter = new Map();
|
||||||
const isRouter = (recipient) => {
|
const isRouter = (recipient) => {
|
||||||
const r = recipient.toLowerCase();
|
const r = recipient.toLowerCase();
|
||||||
@@ -1409,8 +1415,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
};
|
};
|
||||||
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
// A swap step or V4 SETTLE pays `amount` of `token` from the router's
|
||||||
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
// own balance. CONTRACT_BALANCE pays all of it. Any other quantity
|
||||||
// counts against the amount a WRAP_ETH stated, and the steps pay all
|
// counts against a known amount the router holds, and the steps pay
|
||||||
// of it once their amounts reach that one: a swap paid in ETH wraps
|
// all of it once their amounts reach that one: a swap paid in ETH wraps
|
||||||
// what its swap steps then spend. An amount that is not a quantity,
|
// what its swap steps then spend. An amount that is not a quantity,
|
||||||
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
// such as the null decodeV4Swap() gives a SETTLE whose payment it
|
||||||
// cannot tell, or one of a token whose amount is not known, pays out
|
// cannot tell, or one of a token whose amount is not known, pays out
|
||||||
@@ -1787,6 +1793,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
w.amount,
|
w.amount,
|
||||||
);
|
);
|
||||||
inputPaidByUser = true;
|
inputPaidByUser = true;
|
||||||
|
if (!isRouter(w.recipient)) {
|
||||||
|
wrapEthRecipient = w.recipient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
addPayee(w.recipient);
|
addPayee(w.recipient);
|
||||||
// CONTRACT_BALANCE wraps all the router's ETH.
|
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||||
@@ -1815,12 +1824,45 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (command === "V4_SWAP") {
|
} else if (command === "V4_SWAP") {
|
||||||
const v4 = decodeV4Swap(inputs[i]);
|
const v4 = decodeV4Swap(inputs[i]);
|
||||||
const inputSetBefore = inputEstablished;
|
const inputSetBefore = inputEstablished;
|
||||||
|
// What the user pays the step, as decodeV4Swap() gives
|
||||||
|
// it, less the ETH the router already holds (below).
|
||||||
|
// Empty when the step cannot be read.
|
||||||
|
const paidByUser = [];
|
||||||
if (v4) {
|
if (v4) {
|
||||||
// Each SETTLE paid from the router spends the amount
|
// Each SETTLE paid from the router spends the amount
|
||||||
// decodeV4Swap() says it pays.
|
// decodeV4Swap() says it pays.
|
||||||
for (const s of v4.settledFromRouter) {
|
for (const s of v4.settledFromRouter) {
|
||||||
spendFromRouter(s.currency, s.amount);
|
spendFromRouter(s.currency, s.amount);
|
||||||
}
|
}
|
||||||
|
// V4Router pays ETH from the router's own ETH,
|
||||||
|
// whoever a SETTLE names as payer. So ETH the router
|
||||||
|
// holds from an earlier step pays first for the ETH
|
||||||
|
// the user would pay the step. The user pays only the
|
||||||
|
// rest. When this decoder does not know how much ETH
|
||||||
|
// the router holds, or how much the user would pay,
|
||||||
|
// it counts the router as paying all of that ETH.
|
||||||
|
for (const p of v4.paidByUser) {
|
||||||
|
if (
|
||||||
|
p.currency !==
|
||||||
|
"0x0000000000000000000000000000000000000000" ||
|
||||||
|
!heldByRouter.has(p.currency)
|
||||||
|
) {
|
||||||
|
paidByUser.push(p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const held = heldByRouter.get(p.currency);
|
||||||
|
if (
|
||||||
|
typeof held === "bigint" &&
|
||||||
|
typeof p.amount === "bigint" &&
|
||||||
|
p.amount > held
|
||||||
|
) {
|
||||||
|
paidByUser.push({
|
||||||
|
currency: p.currency,
|
||||||
|
amount: p.amount - held,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
spendFromRouter(p.currency, p.amount);
|
||||||
|
}
|
||||||
for (const t of v4.takes) {
|
for (const t of v4.takes) {
|
||||||
if (!isHop(i, t.currency, t.recipient)) {
|
if (!isHop(i, t.currency, t.recipient)) {
|
||||||
addPayee(t.recipient, t.currency);
|
addPayee(t.recipient, t.currency);
|
||||||
@@ -1845,10 +1887,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
commandNames[i] += " (could not be decoded)";
|
commandNames[i] += " (could not be decoded)";
|
||||||
}
|
}
|
||||||
// The step's amount, if it sets the input side: what the
|
// The step's amount, if it sets the input side: what the
|
||||||
// user pays it of its input currency (see
|
// user pays it of its input currency (see paidByUser),
|
||||||
// decodeV4Swap()), or, when the user pays none of it,
|
// or, when the user pays none of it, what its swap
|
||||||
// what its swap actions spend of it, whoever pays that.
|
// actions spend of it, whoever pays that.
|
||||||
const paidIn = v4?.paidByUser.find(
|
const paidIn = paidByUser.find(
|
||||||
(p) => p.currency === v4.tokenIn,
|
(p) => p.currency === v4.tokenIn,
|
||||||
);
|
);
|
||||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
||||||
@@ -1902,11 +1944,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// router still runs it.
|
// router still runs it.
|
||||||
const setInputHere = inputEstablished && !inputSetBefore;
|
const setInputHere = inputEstablished && !inputSetBefore;
|
||||||
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
||||||
if (v4) {
|
for (const p of paidByUser) {
|
||||||
for (const p of v4.paidByUser) {
|
if (setInputHere && p === paidIn) continue;
|
||||||
if (setInputHere && p === paidIn) continue;
|
addUserPaid(p.currency, p.amount);
|
||||||
addUserPaid(p.currency, p.amount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!v4 || v4.settleUnread) {
|
if (!v4 || v4.settleUnread) {
|
||||||
spentUnread();
|
spentUnread();
|
||||||
@@ -1954,15 +1994,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
addPayee(u.recipient);
|
addPayee(u.recipient);
|
||||||
// It pays out all the router's WETH, as ETH. Into the
|
// It pays out all the router's WETH, as ETH. Into the
|
||||||
// router, the router then holds as ETH the WETH a
|
// router, the router then holds as ETH the WETH a
|
||||||
// step paid it.
|
// step paid it. That amount is known when the router
|
||||||
const heldWeth = WETH_ADDRESSES.some((w) =>
|
// held a known amount of WETH and no ETH before.
|
||||||
|
const heldWeth = WETH_ADDRESSES.filter((w) =>
|
||||||
heldByRouter.has(w),
|
heldByRouter.has(w),
|
||||||
);
|
);
|
||||||
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
const ethAmount =
|
||||||
if (isRouter(u.recipient) && heldWeth) {
|
heldWeth.length === 1 &&
|
||||||
|
!heldByRouter.has(
|
||||||
|
"0x0000000000000000000000000000000000000000",
|
||||||
|
)
|
||||||
|
? heldByRouter.get(heldWeth[0])
|
||||||
|
: null;
|
||||||
|
heldWeth.forEach((w) => heldByRouter.delete(w));
|
||||||
|
if (isRouter(u.recipient) && heldWeth.length > 0) {
|
||||||
heldByRouter.set(
|
heldByRouter.set(
|
||||||
"0x0000000000000000000000000000000000000000",
|
"0x0000000000000000000000000000000000000000",
|
||||||
null,
|
ethAmount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2051,14 +2099,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
permit.beforeEveryTake &&
|
permit.beforeEveryTake &&
|
||||||
!stepsUnread.some((step) => step > permit.step);
|
!stepsUnread.some((step) => step > permit.step);
|
||||||
// What the steps take that the Amount line does not give, as
|
// 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
|
// { token, amount, recipient }. A null token is a step this decoder
|
||||||
// (see spentUnread()), or a token it does not know.
|
// did not read (see spentUnread()), or a token it does not know. Only
|
||||||
|
// the take of a WRAP_ETH that pays elsewhere has a recipient (see
|
||||||
|
// wrapEthRecipient).
|
||||||
const spentFromSender = [];
|
const spentFromSender = [];
|
||||||
let total = null;
|
let total = null;
|
||||||
// The takes from the sender that `total` adds up, each a quantity, as
|
// The takes from the sender that `total` adds up, each a quantity, in
|
||||||
// { token, amount }. When there is no Amount line, each gets lines of
|
// the shape of `spentFromSender`. When there is no Amount line, each
|
||||||
// its own (see below). The amount of a step that set the input side
|
// gets lines of its own (see below). The amount of a step that set
|
||||||
// and is paid from the router counts in `total` but is not among them.
|
// the input side and is paid from the router counts in `total` but is
|
||||||
|
// not among them.
|
||||||
const inTotal = [];
|
const inTotal = [];
|
||||||
// Whether the step that set the input side counts as a take from the
|
// Whether the step that set the input side counts as a take from the
|
||||||
// sender, which may get lines of its own. The amount it stated counts
|
// sender, which may get lines of its own. The amount it stated counts
|
||||||
@@ -2073,7 +2124,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (!permitStands || sameAddress(inputToken, permit.token)) {
|
if (!permitStands || sameAddress(inputToken, permit.token)) {
|
||||||
total = inputAmount;
|
total = inputAmount;
|
||||||
if (typeof inputAmount === "bigint" && inputPaidByUser) {
|
if (typeof inputAmount === "bigint" && inputPaidByUser) {
|
||||||
inTotal.push({ token: inputToken, amount: inputAmount });
|
inTotal.push({
|
||||||
|
token: inputToken,
|
||||||
|
amount: inputAmount,
|
||||||
|
recipient: wrapEthRecipient,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (hasFirstTake && !present(inputAmount)) {
|
if (hasFirstTake && !present(inputAmount)) {
|
||||||
firstTakeUnstated = { token: inputToken, amount: null };
|
firstTakeUnstated = { token: inputToken, amount: null };
|
||||||
@@ -2081,7 +2136,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (hasFirstTake) {
|
} else if (hasFirstTake) {
|
||||||
// The step that set the input side spent a token other than the
|
// The step that set the input side spent a token other than the
|
||||||
// permit's, or one not known, so its amount gets lines of its own.
|
// permit's, or one not known, so its amount gets lines of its own.
|
||||||
spentFromSender.push({ token: inputToken, amount: inputAmount });
|
spentFromSender.push({
|
||||||
|
token: inputToken,
|
||||||
|
amount: inputAmount,
|
||||||
|
recipient: wrapEthRecipient,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (permitStands) inputToken = permit.token;
|
if (permitStands) inputToken = permit.token;
|
||||||
for (const t of userPaid) {
|
for (const t of userPaid) {
|
||||||
@@ -2260,7 +2319,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// What steps take from the sender beyond the Amount line, in the shape
|
// What steps take from the sender beyond the Amount line, in the shape
|
||||||
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
||||||
// decoder does not know is said so, and a step it did not read gets
|
// decoder does not know is said so, and a step it did not read gets
|
||||||
// that one line alone.
|
// that one line alone. A take with no recipient goes to the swap step
|
||||||
|
// that spends it. A take with a recipient goes there, and MSG_SENDER
|
||||||
|
// there is the sender.
|
||||||
for (const s of spentFromSender) {
|
for (const s of spentFromSender) {
|
||||||
details.push({
|
details.push({
|
||||||
label: "Moved from the sender",
|
label: "Moved from the sender",
|
||||||
@@ -2270,10 +2331,20 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
});
|
});
|
||||||
if (present(s.token)) {
|
if (present(s.token)) {
|
||||||
details.push(tokenMovedLine("Token moved", s.token, sources));
|
details.push(tokenMovedLine("Token moved", s.token, sources));
|
||||||
details.push({
|
if (!present(s.recipient)) {
|
||||||
label: "Moved to",
|
details.push({
|
||||||
value: "The swap step that spends it",
|
label: "Moved to",
|
||||||
});
|
value: "The swap step that spends it",
|
||||||
|
});
|
||||||
|
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
||||||
|
details.push({ label: "Moved to", value: "The sender" });
|
||||||
|
} else {
|
||||||
|
details.push({
|
||||||
|
label: "Moved to",
|
||||||
|
value: s.recipient,
|
||||||
|
address: s.recipient,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2424,6 +2424,113 @@ describe("uniswap decoder", () => {
|
|||||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/665 — a WRAP_ETH that
|
||||||
|
// sets the input side and pays its WETH to anyone but the router
|
||||||
|
// names that recipient on its Moved to line. On next each row read
|
||||||
|
// The swap step that spends it.
|
||||||
|
const ONE_ETH = 1000000000000000000n;
|
||||||
|
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
||||||
|
// The lines of a WRAP_ETH of 1 ETH whose Moved to line reads `to`.
|
||||||
|
const wrapLines = (to) => [
|
||||||
|
["Moved from the sender", "1.0000 ETH"],
|
||||||
|
["Token moved", "ETH"],
|
||||||
|
["Moved to", to],
|
||||||
|
];
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH to another address, then an EXECUTE_SUB_PLAN",
|
||||||
|
commands: [0x0b, 0x21],
|
||||||
|
inputs: [encodeWrapEth(OTHER_ADDR, ONE_ETH), SUB_PLAN],
|
||||||
|
lines: [
|
||||||
|
...wrapLines(OTHER_ADDR),
|
||||||
|
["Moved from the sender", UNDECODED],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH to address(1), then an EXECUTE_SUB_PLAN",
|
||||||
|
commands: [0x0b, 0x21],
|
||||||
|
inputs: [encodeWrapEth(MSG_SENDER, ONE_ETH), SUB_PLAN],
|
||||||
|
lines: [
|
||||||
|
...wrapLines("The sender"),
|
||||||
|
["Moved from the sender", UNDECODED],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH to another address, then a V3 exact-in of 600 USDT",
|
||||||
|
commands: [0x0a, 0x0b, 0x00],
|
||||||
|
inputs: [
|
||||||
|
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||||
|
encodeWrapEth(OTHER_ADDR, ONE_ETH),
|
||||||
|
V3_600,
|
||||||
|
],
|
||||||
|
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(OTHER_ADDR)],
|
||||||
|
},
|
||||||
|
])("$kind", ({ commands, inputs, lines }) => {
|
||||||
|
const result = decodeSteps(commands, inputs);
|
||||||
|
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/665 — V4Router pays ETH
|
||||||
|
// from the router's own ETH, whoever a SETTLE names as payer. ETH the
|
||||||
|
// router holds from an UNWRAP_WETH into it pays first, so the sender
|
||||||
|
// pays only the ETH beyond it. On next each row also took the V4
|
||||||
|
// step's whole ETH from the sender: 1 ETH more in the first two, and
|
||||||
|
// 1.5 ETH in the last.
|
||||||
|
const unwrapIntoRouter = encodeWrapEth(ROUTER_ADDR, 0n); // UNWRAP_WETH, same encoding
|
||||||
|
// A V4 step swapping `amount` of ETH for USDC, with a SETTLE of
|
||||||
|
// OPEN_DELTA by the user.
|
||||||
|
const v4EthForUsdc = (amount) =>
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_SETTLE_ID,
|
||||||
|
V4_TAKE_ALL_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
encodeV4ExactInSingle(ZERO_ADDR, USDC_ADDR, amount, 1n),
|
||||||
|
settle(ZERO_ADDR, 0n, true),
|
||||||
|
encodeV4TakeAll(USDC_ADDR, 0n),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
|
||||||
|
commands: [0x00, 0x0c, 0x10],
|
||||||
|
inputs: [
|
||||||
|
encodeV3SwapExactIn(ROUTER_ADDR, USDT_600, 1n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
]),
|
||||||
|
unwrapIntoRouter,
|
||||||
|
v4EthForUsdc(ONE_ETH),
|
||||||
|
],
|
||||||
|
lines: [["Amount", "600.0000 USDT"]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
|
||||||
|
commands: [0x0b, 0x0c, 0x10],
|
||||||
|
inputs: [
|
||||||
|
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
|
||||||
|
unwrapIntoRouter,
|
||||||
|
v4EthForUsdc(ONE_ETH),
|
||||||
|
],
|
||||||
|
lines: [["Amount", "1.0000 ETH"]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 swap of 1.5 ETH that the user settles",
|
||||||
|
commands: [0x0b, 0x0c, 0x10],
|
||||||
|
inputs: [
|
||||||
|
encodeWrapEth(ROUTER_ADDR, ONE_ETH),
|
||||||
|
unwrapIntoRouter,
|
||||||
|
v4EthForUsdc((ONE_ETH * 3n) / 2n),
|
||||||
|
],
|
||||||
|
lines: [["Amount", "1.5000 ETH"]],
|
||||||
|
},
|
||||||
|
])("$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