fix: the swap decoder reads who the router's transfer commands pay (closes #599)
TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION (0x07, on the routers with the 0x7f mask), PERMIT2_TRANSFER_FROM and V4 TAKE_PORTION were payouts the decoder did not read, so a swap using one read Min. paid out with an unknown payee. Each recipient is now read under the rule swap steps follow: the sender, the router, a hop into the next V2 pair, or another address, which is named. Paying all the router holds of a token pays it out as SWEEP does; a part leaves the rest in the router. UniversalRouterV1 pays a PERMIT2_TRANSFER_FROM to address(1) or address(2) as written. 0x07 and ACROSS_V4_DEPOSIT_V3 (0x40) are named in Steps; the deposit stays unread. Model: opus-5-5
This commit is contained in:
@@ -2955,45 +2955,58 @@ Currently supported:
|
|||||||
address uses: `0x1f` for `UniversalRouterV1`, `0x3f` for
|
address uses: `0x1f` for `UniversalRouterV1`, `0x3f` for
|
||||||
`UniversalRouterV1_2_V2Support` and `UniversalRouterV2`, and `0x7f` for
|
`UniversalRouterV1_2_V2Support` and `UniversalRouterV2`, and `0x7f` for
|
||||||
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`. The allow-revert flag
|
`UniversalRouterV2_1_1` and `UniversalRouterV2_1_2`. The allow-revert flag
|
||||||
(`0x80`) therefore does not change a step's name. The decoder does not read
|
(`0x80`) therefore does not change a step's name. Only the routers with the
|
||||||
the commands inside an `EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that
|
`0x7f` mask have `PAY_PORTION_FULL_PRECISION` (`0x07`), listed as
|
||||||
step stands they may spend the user's tokens or pay the user. That step is
|
`Pay Portion (Full Precision)`, and `ACROSS_V4_DEPOSIT_V3` (`0x40`), listed as
|
||||||
listed as `Execute Sub-Plan (its steps are not shown)`, and whatever the other
|
`Across Bridge Deposit`; the other routers revert on `0x07`, and it is listed
|
||||||
steps state, the summary names no swap direction, `Token In` and `Token Out`
|
there as `Command 0x07`. The decoder does not read the commands inside an
|
||||||
read `Unknown (could not be decoded)`, and there is no `Amount` or
|
`EXECUTE_SUB_PLAN` (`0x21`) step, and wherever that step stands they may spend
|
||||||
`Min. received` line. Each swap step, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH` and
|
the user's tokens or pay the user. That step is listed as
|
||||||
V4 `TAKE` names the address it pays, and `TAKE_ALL` pays the transaction's
|
`Execute Sub-Plan (its steps are not shown)`, and whatever the other steps
|
||||||
sender. The router reads `address(1)` as the transaction's sender, and
|
state, the summary names no swap direction, `Token In` and `Token Out` read
|
||||||
`address(2)` as the router itself, which holds what it is paid until a later
|
`Unknown (could not be decoded)`, and there is no `Amount` or `Min. received`
|
||||||
step pays it out; a payment to the router, as `address(2)` or by its own
|
line. Each swap step, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH`, `TRANSFER`,
|
||||||
address, is not counted as paid out. When a swap step or `TAKE` pays the
|
`PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, `PERMIT2_TRANSFER_FROM` (which
|
||||||
router the swap's output token, the output stays in the router unless a later
|
moves the user's own tokens) and V4 `TAKE` and `TAKE_PORTION` names the
|
||||||
`SWEEP` of that token, or a later `UNWRAP_WETH` when it is WETH, pays it out.
|
address it pays, and `TAKE_ALL` pays the transaction's sender. The router
|
||||||
A swap step or `TAKE` paying a token to the V2 pair that the very next step
|
reads `address(1)` as the transaction's sender, and `address(2)` as the router
|
||||||
spends, as a V2 exact-in of that token whose `amountIn` is `ALREADY_PAID`
|
itself, which holds what it is paid until a later step pays it out; a payment
|
||||||
(zero) or `CONTRACT_BALANCE`, is a hop, not a payout: Uniswap's mixed routes
|
to the router, as `address(2)` or by its own address, is not counted as paid
|
||||||
pay the next V2 pair directly, and the router enforces that swap's own
|
out. `UniversalRouterV1` alone pays a `PERMIT2_TRANSFER_FROM` to `address(1)`
|
||||||
minimum. The decoder checks that the address paid is that pair, deriving it
|
or `address(2)` as those addresses, and the decoder names them. When a swap
|
||||||
from the V2 factory and pair init code hash the network's routers were
|
step, `TAKE` or `TAKE_PORTION` pays the router the swap's output token, the
|
||||||
deployed with and the first two tokens of that step's path; a payment to any
|
output stays in the router unless a later step pays all the router holds of it
|
||||||
other address is named. (Sepolia's `UniversalRouterV1_2_V2Support` was
|
to anyone but the router: a `SWEEP` of that token, an `UNWRAP_WETH` when it is
|
||||||
deployed with placeholders for both values, so no V2 swap step can succeed on
|
WETH, or a `TRANSFER` of `CONTRACT_BALANCE`, a `PAY_PORTION` of 10,000 bips or
|
||||||
that router.) Every other payment counts, in whichever step and of whatever
|
a `PAY_PORTION_FULL_PRECISION` of 1e18 of that token. A step paying a token to
|
||||||
token. The minimum reads `Min. received` only when some step pays the
|
the V2 pair that the very next step spends, as a V2 exact-in of that token
|
||||||
transaction's sender (the From address on TxApproval, the sender the explorer
|
whose `amountIn` is `ALREADY_PAID` (zero) or `CONTRACT_BALANCE`, is a hop, not
|
||||||
reports on TransactionDetail), no step pays any other address, no step that
|
a payout: Uniswap's mixed routes pay the next V2 pair directly, and the router
|
||||||
may pay an address went unread, and the output does not stay in the router.
|
enforces that swap's own minimum. The decoder checks that the address paid is
|
||||||
Otherwise it reads `Min. paid out`, a `Paid to` line after `Token Out` names
|
that pair, deriving it from the V2 factory and pair init code hash the
|
||||||
each other address paid, one reads
|
network's routers were deployed with and the first two tokens of that step's
|
||||||
|
path; a payment to any other address is named. (Sepolia's
|
||||||
|
`UniversalRouterV1_2_V2Support` was deployed with placeholders for both
|
||||||
|
values, so no V2 swap step can succeed on that router.) Every other payment
|
||||||
|
counts, in whichever step and of whatever token, a share of the output
|
||||||
|
included: an interface fee paid to another address by `PAY_PORTION`,
|
||||||
|
`TRANSFER` or `TAKE_PORTION` is named, and the minimum reads `Min. paid out`,
|
||||||
|
because the swap's minimum includes the fee. The minimum reads `Min. received`
|
||||||
|
only when some step pays the transaction's sender (the From address on
|
||||||
|
TxApproval, the sender the explorer reports on TransactionDetail), no step
|
||||||
|
pays any other address, no step that may pay an address went unread, and the
|
||||||
|
output does not stay in the router. Otherwise it reads `Min. paid out`, a
|
||||||
|
`Paid to` line after `Token Out` names each other address paid, one reads
|
||||||
`The router, where it stays (no later step pays it out)` when the output stays
|
`The router, where it stays (no later step pays it out)` when the output stays
|
||||||
in the router, and one more reads `Unknown (could not be decoded)` when such a
|
in the router, and one more reads `Unknown (could not be decoded)` when such a
|
||||||
step went unread, or `Unknown (not named in the calldata)` when no step pays
|
step went unread, or `Unknown (not named in the calldata)` when no step pays
|
||||||
anyone. A step goes unread when its recipient does not decode as an address,
|
anyone. A step goes unread when its recipient does not decode as an address,
|
||||||
or when it is a command or V4 action the decoder does not read: every command
|
or when it is a command the decoder does not read: every command other than
|
||||||
other than `PERMIT2_PERMIT`, `V2_SWAP_EXACT_IN`, `V2_SWAP_EXACT_OUT`,
|
`PERMIT2_PERMIT`, `PERMIT2_TRANSFER_FROM`, `V2_SWAP_EXACT_IN`,
|
||||||
`V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `WRAP_ETH`, `UNWRAP_WETH` and
|
`V2_SWAP_EXACT_OUT`, `V3_SWAP_EXACT_IN`, `V4_SWAP`, `SWEEP`, `TRANSFER`,
|
||||||
`BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`, `TRANSFER`, `PAY_PORTION` and
|
`PAY_PORTION`, `PAY_PORTION_FULL_PRECISION`, `WRAP_ETH`, `UNWRAP_WETH` and
|
||||||
`EXECUTE_SUB_PLAN` among them), and V4 `TAKE_PORTION`.
|
`BALANCE_CHECK_ERC20` (`V3_SWAP_EXACT_OUT`, `PERMIT2_TRANSFER_FROM_BATCH`,
|
||||||
|
`EXECUTE_SUB_PLAN` and `ACROSS_V4_DEPOSIT_V3` among them).
|
||||||
|
|
||||||
### Non-Goals Forever
|
### Non-Goals Forever
|
||||||
|
|
||||||
|
|||||||
+146
-32
@@ -123,7 +123,10 @@ function v2PairFor(chainId, tokenA, tokenB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Universal Router command types, each read from its command byte with the
|
// Universal Router command types, each read from its command byte with the
|
||||||
// router's mask (see ROUTER_ADDRESSES)
|
// router's mask (see ROUTER_ADDRESSES). Only the routers with the 0x7f mask
|
||||||
|
// have 0x07 and 0x40: 0x40 is above the other masks, and the other routers
|
||||||
|
// revert on 0x07, so decode() names it there as it names a command not
|
||||||
|
// listed here.
|
||||||
const COMMAND_NAMES = {
|
const COMMAND_NAMES = {
|
||||||
0x00: "V3 Swap (Exact In)",
|
0x00: "V3 Swap (Exact In)",
|
||||||
0x01: "V3 Swap (Exact Out)",
|
0x01: "V3 Swap (Exact Out)",
|
||||||
@@ -132,6 +135,7 @@ const COMMAND_NAMES = {
|
|||||||
0x04: "Sweep",
|
0x04: "Sweep",
|
||||||
0x05: "Transfer",
|
0x05: "Transfer",
|
||||||
0x06: "Pay Portion",
|
0x06: "Pay Portion",
|
||||||
|
0x07: "Pay Portion (Full Precision)",
|
||||||
0x08: "V2 Swap (Exact In)",
|
0x08: "V2 Swap (Exact In)",
|
||||||
0x09: "V2 Swap (Exact Out)",
|
0x09: "V2 Swap (Exact Out)",
|
||||||
0x0a: "Permit2 Permit",
|
0x0a: "Permit2 Permit",
|
||||||
@@ -145,6 +149,7 @@ const COMMAND_NAMES = {
|
|||||||
0x13: "V4 Initialize Pool",
|
0x13: "V4 Initialize Pool",
|
||||||
0x14: "V4 Position Mgr Call",
|
0x14: "V4 Position Mgr Call",
|
||||||
0x21: "Execute Sub-Plan",
|
0x21: "Execute Sub-Plan",
|
||||||
|
0x40: "Across Bridge Deposit",
|
||||||
};
|
};
|
||||||
|
|
||||||
// The swap's Amount and Min. received lines land on the same approval screen,
|
// The swap's Amount and Min. received lines land on the same approval screen,
|
||||||
@@ -251,9 +256,11 @@ const WETH_ADDRESSES = [
|
|||||||
// Two recipient values the router reads as placeholders, not as addresses:
|
// Two recipient values the router reads as placeholders, not as addresses:
|
||||||
// MSG_SENDER pays the caller of execute(), which is the transaction's sender,
|
// MSG_SENDER pays the caller of execute(), which is the transaction's sender,
|
||||||
// and ADDRESS_THIS pays the router itself, which holds what it is paid until a
|
// and ADDRESS_THIS pays the router itself, which holds what it is paid until a
|
||||||
// later step pays it out. The swap commands, SWEEP, WRAP_ETH and UNWRAP_WETH
|
// later step pays it out. The swap commands, SWEEP, WRAP_ETH, UNWRAP_WETH,
|
||||||
// map their recipient through the dispatcher's map(), and a V4 TAKE through
|
// TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION and, on every router but
|
||||||
// V4Router's _mapRecipient(); TAKE_ALL always pays the caller.
|
// UniversalRouterV1, PERMIT2_TRANSFER_FROM map their recipient through the
|
||||||
|
// dispatcher's map(), and a V4 TAKE and TAKE_PORTION through V4Router's
|
||||||
|
// _mapRecipient(); TAKE_ALL always pays the caller.
|
||||||
// - constants: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Constants.sol
|
// - constants: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Constants.sol
|
||||||
// - map(), UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Recipient.sol
|
// - map(), UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/libraries/Recipient.sol
|
||||||
// - map(), UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
// - map(), UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
||||||
@@ -426,6 +433,46 @@ function decodeSweep(input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRANSFER, PAY_PORTION, PAY_PORTION_FULL_PRECISION and PERMIT2_TRANSFER_FROM
|
||||||
|
// read their parameters as each release's dispatcher reads them:
|
||||||
|
// - UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/base/Dispatcher.sol
|
||||||
|
// - UniversalRouterV1_2_V2Support: https://github.com/Uniswap/universal-router/blob/41183d6eb154f0ab0e74a0e911a5ef9ea51fc4bd/contracts/base/Dispatcher.sol
|
||||||
|
// - UniversalRouterV2: https://github.com/Uniswap/universal-router/blob/3663f6db6e2fe121753cd2d899699c2dc75dca86/contracts/base/Dispatcher.sol
|
||||||
|
// - UniversalRouterV2_1_1: https://github.com/Uniswap/universal-router/blob/999d561c3ad58fb5cab91b602911f3c75591a9c7/contracts/base/Dispatcher.sol
|
||||||
|
// - UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/base/Dispatcher.sol
|
||||||
|
|
||||||
|
// Decode TRANSFER (command 0x05) input bytes, and those of PAY_PORTION (0x06)
|
||||||
|
// and PAY_PORTION_FULL_PRECISION (0x07), which share its layout. Each pays
|
||||||
|
// `token` out of the router to the recipient. TRANSFER pays `amount`, or all
|
||||||
|
// the router holds when `amount` is CONTRACT_BALANCE. PAY_PORTION pays
|
||||||
|
// `amount` ten-thousandths of what the router holds, and
|
||||||
|
// PAY_PORTION_FULL_PRECISION `amount` 1e18ths of it.
|
||||||
|
// - UniversalRouterV1: https://github.com/Uniswap/universal-router/blob/cda697da221e24ca143bc4f55023694a2a16796f/contracts/modules/Payments.sol
|
||||||
|
// - UniversalRouterV2_1_2: https://github.com/Uniswap/universal-router/blob/802fe4c18f47300e0f183e2a42e9146ec2ea9fc3/contracts/modules/Payments.sol
|
||||||
|
// ABI: (address token, address recipient, uint256 amount)
|
||||||
|
function decodeTransfer(input) {
|
||||||
|
try {
|
||||||
|
const d = coder.decode(["address", "address", "uint256"], input);
|
||||||
|
return { token: d[0], recipient: d[1], amount: d[2] };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode PERMIT2_TRANSFER_FROM (command 0x02) input bytes. Permit2 moves
|
||||||
|
// `token` from the transaction's sender to the recipient. UniversalRouterV1
|
||||||
|
// hands Permit2 the recipient as written, without map(), so on that router
|
||||||
|
// address(1) and address(2) are paid as those addresses.
|
||||||
|
// ABI: (address token, address recipient, uint160 amount)
|
||||||
|
function decodePermit2TransferFrom(input) {
|
||||||
|
try {
|
||||||
|
const d = coder.decode(["address", "address", "uint160"], input);
|
||||||
|
return { token: d[0], recipient: d[1] };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decode WRAP_ETH (command 0x0b) input bytes. The router pays the WETH to the
|
// Decode WRAP_ETH (command 0x0b) input bytes. The router pays the WETH to the
|
||||||
// recipient.
|
// recipient.
|
||||||
// ABI: (address recipient, uint256 amount)
|
// ABI: (address recipient, uint256 amount)
|
||||||
@@ -615,9 +662,11 @@ function totalAmount(amounts) {
|
|||||||
// swap action sets only the sides SETTLE, SETTLE_ALL, TAKE and TAKE_ALL
|
// swap action sets only the sides SETTLE, SETTLE_ALL, TAKE and TAKE_ALL
|
||||||
// name.
|
// name.
|
||||||
//
|
//
|
||||||
// `takes` is every TAKE and TAKE_ALL, as the currency it takes and its
|
// `takes` is every TAKE, TAKE_ALL and TAKE_PORTION, as the currency it takes
|
||||||
// recipient as written, MSG_SENDER for a TAKE_ALL. `recipientUnread` is set
|
// and its recipient as written, MSG_SENDER for a TAKE_ALL. TAKE_PORTION takes
|
||||||
// when the step has a payout this decoder did not read.
|
// a share of the currency, so it does not name the output side as TAKE does.
|
||||||
|
// `recipientUnread` is set when the step has a payout this decoder did not
|
||||||
|
// read.
|
||||||
function decodeV4Swap(input) {
|
function decodeV4Swap(input) {
|
||||||
try {
|
try {
|
||||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||||
@@ -663,6 +712,13 @@ function decodeV4Swap(input) {
|
|||||||
const t = coder.decode(["address", "uint256"], params[i]);
|
const t = coder.decode(["address", "uint256"], params[i]);
|
||||||
takeToken = t[0];
|
takeToken = t[0];
|
||||||
takes.push({ currency: t[0], recipient: MSG_SENDER });
|
takes.push({ currency: t[0], recipient: MSG_SENDER });
|
||||||
|
} else if (actionId === V4_TAKE_PORTION) {
|
||||||
|
// TAKE_PORTION: (address currency, address recipient, uint256 bips)
|
||||||
|
const t = coder.decode(
|
||||||
|
["address", "address", "uint256"],
|
||||||
|
params[i],
|
||||||
|
);
|
||||||
|
takes.push({ currency: t[0], recipient: t[1] });
|
||||||
} else if (
|
} else if (
|
||||||
actionId === V4_SWAP_EXACT_IN_SINGLE ||
|
actionId === V4_SWAP_EXACT_IN_SINGLE ||
|
||||||
actionId === V4_SWAP_EXACT_IN ||
|
actionId === V4_SWAP_EXACT_IN ||
|
||||||
@@ -674,17 +730,18 @@ function decodeV4Swap(input) {
|
|||||||
} catch {
|
} catch {
|
||||||
swapUndecoded = true;
|
swapUndecoded = true;
|
||||||
}
|
}
|
||||||
} else if (actionId === V4_TAKE_PORTION) {
|
|
||||||
// It pays a share of a currency to an address this decoder
|
|
||||||
// does not read
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/599).
|
|
||||||
// V4Router reverts on any action not named here.
|
|
||||||
recipientUnread = true;
|
|
||||||
}
|
}
|
||||||
|
// V4Router reverts on any action not named above, so none
|
||||||
|
// of them pays anyone.
|
||||||
} catch {
|
} catch {
|
||||||
// Skip sub-actions we can't decode. A TAKE or TAKE_ALL among
|
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
|
||||||
// them is a payout this decoder did not read.
|
// TAKE_PORTION among them is a payout this decoder did not
|
||||||
if (actionId === V4_TAKE || actionId === V4_TAKE_ALL) {
|
// read.
|
||||||
|
if (
|
||||||
|
actionId === V4_TAKE ||
|
||||||
|
actionId === V4_TAKE_ALL ||
|
||||||
|
actionId === V4_TAKE_PORTION
|
||||||
|
) {
|
||||||
recipientUnread = true;
|
recipientUnread = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -824,31 +881,41 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
const paidElsewhere = [];
|
const paidElsewhere = [];
|
||||||
// Set when a step may pay an address this decoder did not read.
|
// Set when a step may pay an address this decoder did not read.
|
||||||
let payeeUnread = false;
|
let payeeUnread = false;
|
||||||
// The tokens, lowercased, that a swap step or a V4 TAKE paid to the
|
// The tokens, lowercased, that a swap step or a V4 TAKE or
|
||||||
// router and no later SWEEP of that token, or UNWRAP_WETH of WETH,
|
// TAKE_PORTION paid to the router and no later step paid out of it in
|
||||||
// paid out.
|
// full: a SWEEP of that token, an UNWRAP_WETH of WETH, or a TRANSFER,
|
||||||
|
// PAY_PORTION or PAY_PORTION_FULL_PRECISION of all the router holds of
|
||||||
|
// that token.
|
||||||
const heldByRouter = new Set();
|
const heldByRouter = new Set();
|
||||||
const isRouter = (recipient) => {
|
const isRouter = (recipient) => {
|
||||||
const r = recipient.toLowerCase();
|
const r = recipient.toLowerCase();
|
||||||
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
return r === ADDRESS_THIS || r === toAddress.toLowerCase();
|
||||||
};
|
};
|
||||||
|
// A step pays `recipient`, an address other than the sender's or the
|
||||||
|
// router's, as written.
|
||||||
|
const addPaidElsewhere = (recipient) => {
|
||||||
|
const r = recipient.toLowerCase();
|
||||||
|
if (!paidElsewhere.some((a) => a.toLowerCase() === r)) {
|
||||||
|
paidElsewhere.push(recipient);
|
||||||
|
}
|
||||||
|
};
|
||||||
// A step pays `recipient`. `output` is the token a swap step or a V4
|
// A step pays `recipient`. `output` is the token a swap step or a V4
|
||||||
// TAKE pays; other steps pass none.
|
// TAKE or TAKE_PORTION pays; other steps pass none.
|
||||||
const addPayee = (recipient, output) => {
|
const addPayee = (recipient, output) => {
|
||||||
const r = recipient.toLowerCase();
|
const r = recipient.toLowerCase();
|
||||||
if (isRouter(recipient)) {
|
if (isRouter(recipient)) {
|
||||||
if (present(output)) heldByRouter.add(output.toLowerCase());
|
if (present(output)) heldByRouter.add(output.toLowerCase());
|
||||||
} else if (r === MSG_SENDER || r === sender) {
|
} else if (r === MSG_SENDER || r === sender) {
|
||||||
paidToSender = true;
|
paidToSender = true;
|
||||||
} else if (!paidElsewhere.some((a) => a.toLowerCase() === r)) {
|
} else {
|
||||||
paidElsewhere.push(recipient);
|
addPaidElsewhere(recipient);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// A swap step or V4 TAKE that pays `token` to `recipient` is a hop,
|
// A step that pays `token` to `recipient` is a hop, not a payout,
|
||||||
// not a payout, when the very next step is a V2 exact-in whose input
|
// when the very next step is a V2 exact-in whose input is already
|
||||||
// is already paid, whose path starts at `token`, and whose pair (see
|
// paid, whose path starts at `token`, and whose pair (see v2PairFor())
|
||||||
// v2PairFor()) is `recipient`: Uniswap's mixed routes pay the next V2
|
// is `recipient`: Uniswap's mixed routes pay the next V2 pair
|
||||||
// pair directly, and the router enforces that next swap's own minimum.
|
// directly, and the router enforces that next swap's own minimum.
|
||||||
// Its input is already paid when its amountIn is ALREADY_PAID, or
|
// Its input is already paid when its amountIn is ALREADY_PAID, or
|
||||||
// CONTRACT_BALANCE, which Uniswap's routes send and with which the
|
// CONTRACT_BALANCE, which Uniswap's routes send and with which the
|
||||||
// router adds only what it holds.
|
// router adds only what it holds.
|
||||||
@@ -870,9 +937,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
|
|
||||||
for (let i = 0; i < commandsBytes.length; i++) {
|
for (let i = 0; i < commandsBytes.length; i++) {
|
||||||
const cmdId = commandsBytes[i] & commandTypeMask;
|
const cmdId = commandsBytes[i] & commandTypeMask;
|
||||||
|
// Only the routers with the 0x7f mask have command 0x07 (see
|
||||||
|
// COMMAND_NAMES).
|
||||||
|
const name =
|
||||||
|
cmdId === 0x07 && commandTypeMask !== 0x7f
|
||||||
|
? null
|
||||||
|
: COMMAND_NAMES[cmdId];
|
||||||
commandNames.push(
|
commandNames.push(
|
||||||
COMMAND_NAMES[cmdId] ||
|
name || "Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
||||||
"Command 0x" + cmdId.toString(16).padStart(2, "0"),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -890,6 +962,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// 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);
|
||||||
|
} else if (cmdId === 0x02) {
|
||||||
|
const p = decodePermit2TransferFrom(inputs[i]);
|
||||||
|
if (!p) {
|
||||||
|
payeeUnread = true;
|
||||||
|
} else if (
|
||||||
|
commandTypeMask === 0x1f &&
|
||||||
|
[MSG_SENDER, ADDRESS_THIS].includes(
|
||||||
|
p.recipient.toLowerCase(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// UniversalRouterV1, the router with the 0x1f mask,
|
||||||
|
// pays address(1) and address(2) as written (see
|
||||||
|
// decodePermit2TransferFrom()).
|
||||||
|
addPaidElsewhere(p.recipient);
|
||||||
|
} else if (!isHop(i, p.token, p.recipient)) {
|
||||||
|
addPayee(p.recipient);
|
||||||
|
}
|
||||||
} else if (cmdId === 0x0e) {
|
} else if (cmdId === 0x0e) {
|
||||||
const b = decodeBalanceCheck(inputs[i]);
|
const b = decodeBalanceCheck(inputs[i]);
|
||||||
// The router passes this check whenever the owner holds at
|
// The router passes this check whenever the owner holds at
|
||||||
@@ -948,6 +1037,31 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else {
|
} else {
|
||||||
payeeUnread = true;
|
payeeUnread = true;
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
cmdId === 0x05 ||
|
||||||
|
cmdId === 0x06 ||
|
||||||
|
(cmdId === 0x07 && commandTypeMask === 0x7f)
|
||||||
|
) {
|
||||||
|
const t = decodeTransfer(inputs[i]);
|
||||||
|
if (t) {
|
||||||
|
if (!isHop(i, t.token, t.recipient)) {
|
||||||
|
addPayee(t.recipient);
|
||||||
|
}
|
||||||
|
// The amount with which TRANSFER, PAY_PORTION or
|
||||||
|
// PAY_PORTION_FULL_PRECISION pays all the router
|
||||||
|
// holds of the token, as SWEEP does.
|
||||||
|
const all =
|
||||||
|
cmdId === 0x05
|
||||||
|
? CONTRACT_BALANCE
|
||||||
|
: cmdId === 0x06
|
||||||
|
? 10000n
|
||||||
|
: 10n ** 18n;
|
||||||
|
if (t.amount === all && !isRouter(t.recipient)) {
|
||||||
|
heldByRouter.delete(t.token.toLowerCase());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payeeUnread = true;
|
||||||
|
}
|
||||||
} else if (cmdId === 0x0b) {
|
} else if (cmdId === 0x0b) {
|
||||||
const w = decodeWrapEth(inputs[i]);
|
const w = decodeWrapEth(inputs[i]);
|
||||||
if (w) {
|
if (w) {
|
||||||
@@ -1016,9 +1130,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Any other command may pay an address this decoder does
|
// Any other command may pay an address this decoder does
|
||||||
// not read: V3_SWAP_EXACT_OUT does, and so do TRANSFER,
|
// not read: V3_SWAP_EXACT_OUT,
|
||||||
// PAY_PORTION and the others
|
// PERMIT2_TRANSFER_FROM_BATCH and ACROSS_V4_DEPOSIT_V3
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/599 lists.
|
// among them.
|
||||||
payeeUnread = true;
|
payeeUnread = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+22
-1
@@ -954,7 +954,28 @@ describe("uniswap decoder", () => {
|
|||||||
router: "UniversalRouterV2_1_2",
|
router: "UniversalRouterV2_1_2",
|
||||||
address: ROUTER_V2_1_2_ADDR,
|
address: ROUTER_V2_1_2_ADDR,
|
||||||
command: "0x40",
|
command: "0x40",
|
||||||
steps: "Command 0x40",
|
steps: "Across Bridge Deposit",
|
||||||
|
},
|
||||||
|
// Only the routers with the 0x7f mask have 0x07, as
|
||||||
|
// PAY_PORTION_FULL_PRECISION; the others revert on it
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/599).
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV1",
|
||||||
|
address: ROUTER_V1_ADDR,
|
||||||
|
command: "0x07",
|
||||||
|
steps: "Command 0x07",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2",
|
||||||
|
address: ROUTER_ADDR,
|
||||||
|
command: "0x07",
|
||||||
|
steps: "Command 0x07",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
router: "UniversalRouterV2_1_2",
|
||||||
|
address: ROUTER_V2_1_2_ADDR,
|
||||||
|
command: "0x07",
|
||||||
|
steps: "Pay Portion (Full Precision)",
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
"command $command sent to $router is listed as $steps",
|
"command $command sent to $router is listed as $steps",
|
||||||
|
|||||||
+323
-33
@@ -1,12 +1,14 @@
|
|||||||
// The address a swap's output is paid to
|
// The address a swap's output is paid to
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/594).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/594 and
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/599).
|
||||||
//
|
//
|
||||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH and V4 TAKE names the
|
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||||
// address it pays, and TAKE_ALL pays the sender. The decoder read none of
|
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM and V4 TAKE and
|
||||||
// them: a swap whose output went to another address still read "Min.
|
// TAKE_PORTION names the address it pays, and TAKE_ALL pays the sender. The
|
||||||
// received", as if the user received it. The router reads address(1) as the
|
// decoder read none of them: a swap whose output went to another address
|
||||||
// sender and address(2) as the router itself, which holds what it is paid
|
// still read "Min. received", as if the user received it. The router reads
|
||||||
// until a later step pays it out.
|
// address(1) as the sender and address(2) as the router itself, which holds
|
||||||
|
// what it is paid until a later step pays it out.
|
||||||
//
|
//
|
||||||
// The screens are driven against a minimal DOM stub in the shape
|
// The screens are driven against a minimal DOM stub in the shape
|
||||||
// tests/uniswapRouterAddress.test.js uses.
|
// tests/uniswapRouterAddress.test.js uses.
|
||||||
@@ -41,6 +43,8 @@ const transactionDetail = require("../src/popup/views/transactionDetail");
|
|||||||
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
// UniversalRouterV2_1_2 on Ethereum Mainnet, whose mask reads 0x07 and 0x40.
|
// UniversalRouterV2_1_2 on Ethereum Mainnet, whose mask reads 0x07 and 0x40.
|
||||||
const ROUTER_V2_1_2 = "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85";
|
const ROUTER_V2_1_2 = "0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85";
|
||||||
|
// UniversalRouterV1 on Ethereum Mainnet.
|
||||||
|
const ROUTER_V1 = "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B";
|
||||||
const USER = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
const USER = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
const OTHER = "0x7777777777777777777777777777777777777777";
|
const OTHER = "0x7777777777777777777777777777777777777777";
|
||||||
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
const MSG_SENDER = "0x0000000000000000000000000000000000000001";
|
||||||
@@ -53,6 +57,8 @@ const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
|||||||
const ONE_WETH = 1000000000000000000n;
|
const ONE_WETH = 1000000000000000000n;
|
||||||
const USDT_2000 = 2000000000n;
|
const USDT_2000 = 2000000000n;
|
||||||
const USDC_1990 = 1990000000n;
|
const USDC_1990 = 1990000000n;
|
||||||
|
// The router's whole-balance amount.
|
||||||
|
const CONTRACT_BALANCE = 2n ** 255n;
|
||||||
|
|
||||||
// The Paid to line's value when no address can be named.
|
// The Paid to line's value when no address can be named.
|
||||||
const UNDECODED = "Unknown (could not be decoded)";
|
const UNDECODED = "Unknown (could not be decoded)";
|
||||||
@@ -89,7 +95,14 @@ function v2Swap(command, recipient, amount, limit, path, payerIsUser = true) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function v3SwapExactIn(recipient, amountIn, amountOutMin, tokenIn, tokenOut) {
|
function v3SwapExactIn(
|
||||||
|
recipient,
|
||||||
|
amountIn,
|
||||||
|
amountOutMin,
|
||||||
|
tokenIn,
|
||||||
|
tokenOut,
|
||||||
|
payerIsUser = true,
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
0x00,
|
0x00,
|
||||||
coder.encode(
|
coder.encode(
|
||||||
@@ -102,7 +115,7 @@ function v3SwapExactIn(recipient, amountIn, amountOutMin, tokenIn, tokenOut) {
|
|||||||
["address", "uint24", "address"],
|
["address", "uint24", "address"],
|
||||||
[tokenIn, 3000, tokenOut],
|
[tokenIn, 3000, tokenOut],
|
||||||
),
|
),
|
||||||
true,
|
payerIsUser,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -115,6 +128,28 @@ function sweep(token, recipient) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRANSFER (0x05), PAY_PORTION (0x06) and PAY_PORTION_FULL_PRECISION (0x07)
|
||||||
|
// share one layout. `amount` is a quantity, bips, or 1e18ths respectively.
|
||||||
|
function transfer(command, token, recipient, amount) {
|
||||||
|
return [
|
||||||
|
command,
|
||||||
|
coder.encode(
|
||||||
|
["address", "address", "uint256"],
|
||||||
|
[token, recipient, amount],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function permit2TransferFrom(token, recipient, amount) {
|
||||||
|
return [
|
||||||
|
0x02,
|
||||||
|
coder.encode(
|
||||||
|
["address", "address", "uint160"],
|
||||||
|
[token, recipient, amount],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function wrapEth(recipient, amount) {
|
function wrapEth(recipient, amount) {
|
||||||
return [0x0b, coder.encode(["address", "uint256"], [recipient, amount])];
|
return [0x0b, coder.encode(["address", "uint256"], [recipient, amount])];
|
||||||
}
|
}
|
||||||
@@ -168,6 +203,15 @@ function take(currency, recipient) {
|
|||||||
function takeAll(currency) {
|
function takeAll(currency) {
|
||||||
return [0x0f, coder.encode(["address", "uint256"], [currency, 0n])];
|
return [0x0f, coder.encode(["address", "uint256"], [currency, 0n])];
|
||||||
}
|
}
|
||||||
|
function takePortion(currency, recipient, bips) {
|
||||||
|
return [
|
||||||
|
0x10,
|
||||||
|
coder.encode(
|
||||||
|
["address", "address", "uint256"],
|
||||||
|
[currency, recipient, bips],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// A V4_SWAP step of `actions`, each [action, params].
|
// A V4_SWAP step of `actions`, each [action, params].
|
||||||
function v4Swap(...actions) {
|
function v4Swap(...actions) {
|
||||||
@@ -422,6 +466,216 @@ describe("an output left in the router is not received", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/599: TRANSFER, PAY_PORTION,
|
||||||
|
// PAY_PORTION_FULL_PRECISION and V4 TAKE_PORTION pay a token to an address,
|
||||||
|
// and PERMIT2_TRANSFER_FROM moves the user's own tokens to one.
|
||||||
|
describe("each transfer command's recipient", () => {
|
||||||
|
// Each pays out all of a swap's output. Paid to another address, this is
|
||||||
|
// the first shape the issue reports.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "TRANSFER of CONTRACT_BALANCE",
|
||||||
|
data: (r) =>
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x05, WETH, r, CONTRACT_BALANCE),
|
||||||
|
),
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION of 10,000 bips",
|
||||||
|
data: (r) =>
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x06, WETH, r, 10000n),
|
||||||
|
),
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION_FULL_PRECISION of 1e18",
|
||||||
|
data: (r) =>
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x07, WETH, r, 10n ** 18n),
|
||||||
|
),
|
||||||
|
router: ROUTER_V2_1_2,
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "V4 TAKE_PORTION of 10,000 bips",
|
||||||
|
data: (r) =>
|
||||||
|
execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
SETTLE_USDT,
|
||||||
|
takePortion(USDC, r, 10000n),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$kind: paid to the user it is received, to the router it stays there, to another address that address is named",
|
||||||
|
({ data, router = ROUTER, min }) => {
|
||||||
|
const toUser = decode(data(MSG_SENDER), USER, router);
|
||||||
|
expect(labels(toUser)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(toUser, "Min. received").value).toBe(min);
|
||||||
|
expect(decode(data(USER), USER, router).details).toEqual(
|
||||||
|
toUser.details,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toRouter = decode(data(ADDRESS_THIS), USER, router);
|
||||||
|
expect(labels(toRouter)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(toRouter)).toEqual([STAYS_IN_ROUTER]);
|
||||||
|
expect(detail(toRouter, "Min. paid out").value).toBe(min);
|
||||||
|
|
||||||
|
const toOther = decode(data(OTHER), USER, router);
|
||||||
|
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||||
|
expect(detail(toOther, "Min. paid out").value).toBe(min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The user's USDT, moved to `r`, then a swap of it paid to the user.
|
||||||
|
// The router spends what it is moved, so a move to the router pays no
|
||||||
|
// one. A move to another address is the issue's second shape.
|
||||||
|
const movedThenSwapped = (r) =>
|
||||||
|
execute(
|
||||||
|
permit2TransferFrom(USDT, r, USDT_2000),
|
||||||
|
v3SwapExactIn(MSG_SENDER, USDT_2000, ONE_WETH, USDT, WETH, false),
|
||||||
|
);
|
||||||
|
|
||||||
|
test("PERMIT2_TRANSFER_FROM: to the user or the router it pays no one else, to another address that address is named", () => {
|
||||||
|
for (const r of [MSG_SENDER, USER, ADDRESS_THIS, ROUTER]) {
|
||||||
|
const result = decode(movedThenSwapped(r));
|
||||||
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||||
|
}
|
||||||
|
|
||||||
|
const toOther = decode(movedThenSwapped(OTHER));
|
||||||
|
expect(labels(toOther)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||||
|
expect(detail(toOther, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
|
|
||||||
|
const alone = decode(
|
||||||
|
execute(permit2TransferFrom(USDT, OTHER, USDT_2000)),
|
||||||
|
);
|
||||||
|
expect(detail(alone, "Steps").value).toBe("Permit2 Transfer");
|
||||||
|
expect(paidTo(alone)).toEqual([OTHER]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("UniversalRouterV1 pays a PERMIT2_TRANSFER_FROM to address(1) or address(2) as written, and maps a TRANSFER's", () => {
|
||||||
|
for (const r of [MSG_SENDER, ADDRESS_THIS]) {
|
||||||
|
const result = decode(movedThenSwapped(r), USER, ROUTER_V1);
|
||||||
|
expect(paidTo(result)).toEqual([r]);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferred = decode(
|
||||||
|
execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x05, WETH, MSG_SENDER, CONTRACT_BALANCE),
|
||||||
|
),
|
||||||
|
USER,
|
||||||
|
ROUTER_V1,
|
||||||
|
);
|
||||||
|
expect(labels(transferred)).toEqual(PAID_TO_USER);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a router without command 0x07 does not read it as PAY_PORTION_FULL_PRECISION", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
|
swapToWeth(MSG_SENDER),
|
||||||
|
transfer(0x07, WETH, MSG_SENDER, 10n ** 18n),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(detail(result, "Steps").value).toBe(
|
||||||
|
"V3 Swap (Exact In) → Command 0x07",
|
||||||
|
);
|
||||||
|
expect(paidTo(result)).toEqual([UNDECODED]);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// A payout of only part of what the router holds leaves the rest there.
|
||||||
|
describe("a payout of part of the router's output", () => {
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "a TRANSFER of 1 base unit",
|
||||||
|
step: transfer(0x05, WETH, MSG_SENDER, 1n),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PAY_PORTION of 5,000 bips",
|
||||||
|
step: transfer(0x06, WETH, MSG_SENDER, 5000n),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PAY_PORTION_FULL_PRECISION of 1e18 less 1",
|
||||||
|
step: transfer(0x07, WETH, MSG_SENDER, 10n ** 18n - 1n),
|
||||||
|
},
|
||||||
|
])("$kind to the user leaves the rest in the router", ({ step }) => {
|
||||||
|
const result = decode(
|
||||||
|
execute(swapToWeth(ADDRESS_THIS), step),
|
||||||
|
USER,
|
||||||
|
ROUTER_V2_1_2,
|
||||||
|
);
|
||||||
|
expect(labels(result)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
// An interface fee: a share of the output to another address, the rest
|
||||||
|
// to the user. The swap's minimum includes the fee, so it is not what the
|
||||||
|
// user receives.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION of 25 bips, then SWEEP",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x06, WETH, OTHER, 25n),
|
||||||
|
sweep(WETH, MSG_SENDER),
|
||||||
|
),
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "TRANSFER of a flat fee, then SWEEP",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x05, WETH, OTHER, ONE_WETH / 400n),
|
||||||
|
sweep(WETH, MSG_SENDER),
|
||||||
|
),
|
||||||
|
min: "1.0000 WETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION of 25 bips, then UNWRAP_WETH",
|
||||||
|
data: execute(
|
||||||
|
swapToWeth(ADDRESS_THIS),
|
||||||
|
transfer(0x06, WETH, OTHER, 25n),
|
||||||
|
unwrapWeth(MSG_SENDER),
|
||||||
|
),
|
||||||
|
min: "1.0000 ETH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "V4 TAKE_PORTION of 25 bips, then TAKE_ALL",
|
||||||
|
data: execute(
|
||||||
|
v4Swap(
|
||||||
|
SWAP_USDT_FOR_USDC,
|
||||||
|
SETTLE_USDT,
|
||||||
|
takePortion(USDC, OTHER, 25n),
|
||||||
|
takeAll(USDC),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
min: "1990.0000 USDC",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"a fee by $kind names the fee's recipient and calls nothing received",
|
||||||
|
({ data, min }) => {
|
||||||
|
const result = decode(data);
|
||||||
|
expect(labels(result)).toEqual(PAID_ELSEWHERE);
|
||||||
|
expect(paidTo(result)).toEqual([OTHER]);
|
||||||
|
expect(detail(result, "Min. paid out").value).toBe(min);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Uniswap's mixed routes pay a V2 pair directly when the next step swaps
|
// Uniswap's mixed routes pay a V2 pair directly when the next step swaps
|
||||||
// through it. That V2 swap spends what the pair holds: its amountIn is
|
// through it. That V2 swap spends what the pair holds: its amountIn is
|
||||||
// CONTRACT_BALANCE, paid from the router, or ALREADY_PAID. The pairs below
|
// CONTRACT_BALANCE, paid from the router, or ALREADY_PAID. The pairs below
|
||||||
@@ -430,7 +684,6 @@ describe("an output left in the router is not received", () => {
|
|||||||
describe("a swap paid to the pair the next V2 swap spends", () => {
|
describe("a swap paid to the pair the next V2 swap spends", () => {
|
||||||
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
// The WETH/USDC pair on Ethereum Mainnet, as its V2 factory reports it.
|
||||||
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
const PAIR = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc";
|
||||||
const CONTRACT_BALANCE = 2n ** 255n;
|
|
||||||
const AMOUNTS_IN = [
|
const AMOUNTS_IN = [
|
||||||
{ kind: "CONTRACT_BALANCE", amountIn: CONTRACT_BALANCE },
|
{ kind: "CONTRACT_BALANCE", amountIn: CONTRACT_BALANCE },
|
||||||
{ kind: "ALREADY_PAID", amountIn: 0n },
|
{ kind: "ALREADY_PAID", amountIn: 0n },
|
||||||
@@ -498,6 +751,36 @@ describe("a swap paid to the pair the next V2 swap spends", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
kind: "ETH wrapped into the router, then a TRANSFER of all of it",
|
||||||
|
pay: (r) => [
|
||||||
|
wrapEth(ADDRESS_THIS, ONE_WETH),
|
||||||
|
transfer(0x05, WETH, r, CONTRACT_BALANCE),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "a PERMIT2_TRANSFER_FROM of the user's WETH",
|
||||||
|
pay: (r) => [permit2TransferFrom(WETH, r, ONE_WETH)],
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$kind, paid to the pair, is a hop; paid to another address, it is named",
|
||||||
|
({ pay }) => {
|
||||||
|
const route = (r) =>
|
||||||
|
execute(...pay(r), pairSpends(0n, [WETH, USDC]));
|
||||||
|
|
||||||
|
const toPair = decode(route(PAIR));
|
||||||
|
expect(paidTo(toPair)).toEqual([]);
|
||||||
|
expect(detail(toPair, "Min. received").value).toBe(
|
||||||
|
"1990.0000 USDC",
|
||||||
|
);
|
||||||
|
|
||||||
|
const toOther = decode(route(OTHER));
|
||||||
|
expect(paidTo(toOther)).toEqual([OTHER]);
|
||||||
|
expect(detail(toOther, "Min. received")).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
describe("on Sepolia", () => {
|
describe("on Sepolia", () => {
|
||||||
const SEPOLIA = "0xaa36a7";
|
const SEPOLIA = "0xaa36a7";
|
||||||
// UniversalRouterV2_1_2 on Sepolia.
|
// UniversalRouterV2_1_2 on Sepolia.
|
||||||
@@ -661,13 +944,17 @@ describe("every step's payee counts, whichever step sets the output", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("a payout that cannot be read is never said to be received", () => {
|
describe("a payout that cannot be read is never said to be received", () => {
|
||||||
test("a V4 TAKE whose recipient word has its top byte set", () => {
|
test.each([
|
||||||
|
{ kind: "TAKE", action: take(USDC, OTHER) },
|
||||||
|
{ kind: "TAKE_PORTION", action: takePortion(USDC, OTHER, 25n) },
|
||||||
|
])("a V4 $kind whose recipient word has its top byte set", ({ action }) => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
execute(
|
execute(
|
||||||
v4Swap(
|
v4Swap(
|
||||||
SWAP_USDT_FOR_USDC,
|
SWAP_USDT_FOR_USDC,
|
||||||
SETTLE_USDT,
|
SETTLE_USDT,
|
||||||
withDirtyWord(take(USDC, OTHER), 1),
|
withDirtyWord(action, 1),
|
||||||
|
takeAll(USDC),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -689,11 +976,33 @@ describe("a payout that cannot be read is never said to be received", () => {
|
|||||||
{ kind: "SWEEP", step: sweep(WETH, OTHER), word: 1 },
|
{ kind: "SWEEP", step: sweep(WETH, OTHER), word: 1 },
|
||||||
{ kind: "WRAP_ETH", step: wrapEth(OTHER, 1n) },
|
{ kind: "WRAP_ETH", step: wrapEth(OTHER, 1n) },
|
||||||
{ kind: "UNWRAP_WETH", step: unwrapWeth(OTHER) },
|
{ kind: "UNWRAP_WETH", step: unwrapWeth(OTHER) },
|
||||||
|
{
|
||||||
|
kind: "TRANSFER",
|
||||||
|
step: transfer(0x05, WETH, OTHER, 1n),
|
||||||
|
word: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION",
|
||||||
|
step: transfer(0x06, WETH, OTHER, 1n),
|
||||||
|
word: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PAY_PORTION_FULL_PRECISION",
|
||||||
|
step: transfer(0x07, WETH, OTHER, 1n),
|
||||||
|
word: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "PERMIT2_TRANSFER_FROM",
|
||||||
|
step: permit2TransferFrom(WETH, OTHER, 1n),
|
||||||
|
word: 1,
|
||||||
|
},
|
||||||
])(
|
])(
|
||||||
"a $kind whose recipient word has its top byte set",
|
"a $kind whose recipient word has its top byte set",
|
||||||
({ step, word = 0 }) => {
|
({ step, word = 0 }) => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
execute(swapToWeth(MSG_SENDER), withDirtyWord(step, word)),
|
execute(swapToWeth(MSG_SENDER), withDirtyWord(step, word)),
|
||||||
|
USER,
|
||||||
|
ROUTER_V2_1_2,
|
||||||
);
|
);
|
||||||
expect(paidTo(result)).toEqual([UNDECODED]);
|
expect(paidTo(result)).toEqual([UNDECODED]);
|
||||||
expect(detail(result, "Min. received")).toBeUndefined();
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
@@ -719,17 +1028,8 @@ describe("a payout that cannot be read is never said to be received", () => {
|
|||||||
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
expect(detail(result, "Min. paid out").value).toBe("1.0000 WETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/599 is to read these. Until
|
// Each is a payout this decoder does not read.
|
||||||
// then each is a payout this decoder does not read.
|
|
||||||
const transferShape = (command) => [
|
|
||||||
command,
|
|
||||||
coder.encode(["address", "address", "uint256"], [WETH, OTHER, 1n]),
|
|
||||||
];
|
|
||||||
test.each([
|
test.each([
|
||||||
{ kind: "PERMIT2_TRANSFER_FROM", step: transferShape(0x02) },
|
|
||||||
{ kind: "TRANSFER", step: transferShape(0x05) },
|
|
||||||
{ kind: "PAY_PORTION", step: transferShape(0x06) },
|
|
||||||
{ kind: "PAY_PORTION_FULL_PRECISION", step: transferShape(0x07) },
|
|
||||||
{
|
{
|
||||||
kind: "PERMIT2_TRANSFER_FROM_BATCH",
|
kind: "PERMIT2_TRANSFER_FROM_BATCH",
|
||||||
step: [
|
step: [
|
||||||
@@ -750,7 +1050,7 @@ describe("a payout that cannot be read is never said to be received", () => {
|
|||||||
0x21,
|
0x21,
|
||||||
coder.encode(
|
coder.encode(
|
||||||
["bytes", "bytes[]"],
|
["bytes", "bytes[]"],
|
||||||
["0x05", [transferShape(0x05)[1]]],
|
["0x05", [transfer(0x05, WETH, OTHER, 1n)[1]]],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -758,16 +1058,6 @@ describe("a payout that cannot be read is never said to be received", () => {
|
|||||||
kind: "ACROSS_V4_DEPOSIT_V3",
|
kind: "ACROSS_V4_DEPOSIT_V3",
|
||||||
step: [0x40, coder.encode(["address"], [OTHER])],
|
step: [0x40, coder.encode(["address"], [OTHER])],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
kind: "V4 TAKE_PORTION",
|
|
||||||
step: v4Swap([
|
|
||||||
0x10,
|
|
||||||
coder.encode(
|
|
||||||
["address", "address", "uint256"],
|
|
||||||
[WETH, OTHER, 10000n],
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
])("a swap paid to the user, then $kind", ({ step }) => {
|
])("a swap paid to the user, then $kind", ({ step }) => {
|
||||||
const result = decode(
|
const result = decode(
|
||||||
execute(swapToWeth(MSG_SENDER), step),
|
execute(swapToWeth(MSG_SENDER), step),
|
||||||
|
|||||||
Reference in New Issue
Block a user