1 Commits
Author SHA1 Message Date
sneak 1af7c32b02 fix: any token a swap leaves in the router keeps it from reading as received (closes #606)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
The decoder checked only the swap's output for being left in the router.
It now records every token a step pays the router, the WETH of a
WRAP_ETH and the rest of a V4 SETTLE taken to the router among them, and
any token still there after the last step gets a Paid to line naming it,
so the minimum reads Min. paid out. A swap step or V4 SETTLE that spends
all the router holds of a token, a WRAP_ETH of all its ETH, and swap
steps from the router that together spend what a WRAP_ETH stated count
as spending it, so Uniswap's own routes read as before.

Model: opus-5-5
2026-10-19 16:32:20 +00:00
3 changed files with 6 additions and 97 deletions
+1 -5
View File
@@ -3061,11 +3061,7 @@ Currently supported:
token from the router (`payerIsUser` false). Such steps also spend all the
WETH a `WRAP_ETH` paid the router once together they spend the amount the
`WRAP_ETH` states, as a swap paid in ETH does; a `SETTLE` of `OPEN_DELTA`
(zero) from the router counts as spending what its step's swap spends only
when it is the step's only `SETTLE` or `SETTLE_ALL` of that token, comes after
every swap action in the step, and no swap action in the step buys that token,
because the router pays only what the step still owes of that token when the
`SETTLE` runs; otherwise it counts as spending nothing. So the rest of a V4
(zero) counts as spending what its step's swap spends. So the rest of a V4
`SETTLE` of more than its swap spends, taken to the router, the WETH of a
`WRAP_ETH`, and a token a swap pays the router on the way to the output, each
stays in the router when later steps do not spend all of it, where anyone can
+5 -33
View File
@@ -887,9 +887,7 @@ function totalAmount(amounts) {
// 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. `settledFromRouter` is every SETTLE that the router pays from its own
// balance (`payerIsUser` false), as the currency, the amount as written and
// its index among the step's actions, except that a SETTLE of OPEN_DELTA
// whose payment this decoder cannot follow has no amount (null; see below).
// balance (`payerIsUser` false), as the currency and the amount as written.
function decodeV4Swap(input) {
try {
const d = coder.decode(["bytes", "bytes[]"], input);
@@ -900,10 +898,6 @@ function decodeV4Swap(input) {
let takeToken = null;
const takes = [];
const settledFromRouter = [];
// The currency of every SETTLE and SETTLE_ALL, in order.
const settledCurrencies = [];
// The index of the step's last swap action, read or not, or -1.
let lastSwapAction = -1;
let recipientUnread = false;
// The step's swap actions that could be read, as decodeV4SwapAction()
// reads them.
@@ -923,19 +917,16 @@ function decodeV4Swap(input) {
params[i],
);
settleToken = s[0];
settledCurrencies.push(s[0]);
if (!s[2]) {
settledFromRouter.push({
currency: s[0],
amount: s[1],
index: i,
});
}
} else if (actionId === V4_SETTLE_ALL) {
// SETTLE_ALL: (address currency, uint256 maxAmount)
const s = coder.decode(["address", "uint256"], params[i]);
settleToken = s[0];
settledCurrencies.push(s[0]);
} else if (actionId === V4_TAKE) {
// TAKE: (address currency, address recipient, uint256 amount)
const t = coder.decode(
@@ -962,7 +953,6 @@ function decodeV4Swap(input) {
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT
) {
lastSwapAction = i;
try {
swaps.push(decodeV4SwapAction(actionId, params[i]));
} catch {
@@ -985,24 +975,6 @@ function decodeV4Swap(input) {
}
}
// A SETTLE of OPEN_DELTA (zero) paid from the router pays what the
// step still owes of its currency when it runs. That is what the
// step's swap actions spend of it only when the SETTLE is the step's
// one SETTLE or SETTLE_ALL of that currency, comes after every swap
// action, and no swap action buys that currency (an unread one may).
// Any other such SETTLE pays an amount this decoder cannot tell.
for (const s of settledFromRouter) {
const paysWhatSwapsSpend =
settledCurrencies.filter((c) => c === s.currency).length ===
1 &&
s.index > lastSwapAction &&
!swapUndecoded &&
!swaps.some((w) => w.tokenOut === s.currency);
if (s.amount === 0n && !paysWhatSwapsSpend) {
s.amount = null;
}
}
// Rule 3.
if (swapUndecoded) {
return {
@@ -1457,10 +1429,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]);
if (v4) {
// A SETTLE of OPEN_DELTA (zero) that decodeV4Swap()
// left at zero pays what the step's swap actions
// spend of its currency, which the step states as its
// amount when that currency is its Token In.
// A SETTLE of OPEN_DELTA (zero) pays what the step's
// swap actions spend of its currency, which the step
// states as its amount when that currency is its
// Token In.
for (const s of v4.settledFromRouter) {
let amount = s.amount;
if (amount === 0n) {
-59
View File
@@ -650,65 +650,6 @@ describe("any token left in the router is not received", () => {
),
left: tokenLine(WETH, "WETH"),
},
// A SETTLE of OPEN_DELTA from the router pays only what the step
// still owes of its currency when it runs.
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 0.5 WETH with two SETTLEs of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(WETH, USDC, ONE_WETH / 2n, USDC_1990),
settle(WETH, 0n, false),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 swap of 0.75 WETH with a SETTLE of 0.25 WETH and one of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(
WETH,
USDC,
(ONE_WETH * 3n) / 4n,
USDC_1990,
),
settle(WETH, ONE_WETH / 4n, false),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 step that also buys WETH back, with a SETTLE of OPEN_DELTA from the router",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
swapExactInSingle(USDC, WETH, 1000000n, 1n),
settle(WETH, 0n, false),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a WRAP_ETH of 1 ETH into the router, then a V4 SETTLE of OPEN_DELTA from the router before the swap, which the user settles",
data: execute(
wrapEth(ADDRESS_THIS, ONE_WETH),
v4Swap(
settle(WETH, 0n, false),
swapExactInSingle(WETH, USDC, ONE_WETH, USDC_1990),
settle(WETH, 0n, true),
takeAll(USDC),
),
),
left: tokenLine(WETH, "WETH"),
},
{
kind: "a swap paying WETH to the router, a TRANSFER of 1 wei of it to the WETH/USDC pair, then a V2 swap of what the pair holds",
data: execute(