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 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 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` `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 (zero) counts as spending what its step's swap spends. So the rest of a V4
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
`SETTLE` of more than its swap spends, taken to the router, the WETH of a `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 `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 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. // 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 // `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 // read. `settledFromRouter` is every SETTLE that the router pays from its own
// balance (`payerIsUser` false), as the currency, the amount as written and // balance (`payerIsUser` false), as the currency and the amount as written.
// 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).
function decodeV4Swap(input) { function decodeV4Swap(input) {
try { try {
const d = coder.decode(["bytes", "bytes[]"], input); const d = coder.decode(["bytes", "bytes[]"], input);
@@ -900,10 +898,6 @@ function decodeV4Swap(input) {
let takeToken = null; let takeToken = null;
const takes = []; const takes = [];
const settledFromRouter = []; 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; let recipientUnread = false;
// The step's swap actions that could be read, as decodeV4SwapAction() // The step's swap actions that could be read, as decodeV4SwapAction()
// reads them. // reads them.
@@ -923,19 +917,16 @@ function decodeV4Swap(input) {
params[i], params[i],
); );
settleToken = s[0]; settleToken = s[0];
settledCurrencies.push(s[0]);
if (!s[2]) { if (!s[2]) {
settledFromRouter.push({ settledFromRouter.push({
currency: s[0], currency: s[0],
amount: s[1], amount: s[1],
index: i,
}); });
} }
} else if (actionId === V4_SETTLE_ALL) { } else if (actionId === V4_SETTLE_ALL) {
// SETTLE_ALL: (address currency, uint256 maxAmount) // SETTLE_ALL: (address currency, uint256 maxAmount)
const s = coder.decode(["address", "uint256"], params[i]); const s = coder.decode(["address", "uint256"], params[i]);
settleToken = s[0]; settleToken = s[0];
settledCurrencies.push(s[0]);
} else if (actionId === V4_TAKE) { } else if (actionId === V4_TAKE) {
// TAKE: (address currency, address recipient, uint256 amount) // TAKE: (address currency, address recipient, uint256 amount)
const t = coder.decode( const t = coder.decode(
@@ -962,7 +953,6 @@ function decodeV4Swap(input) {
actionId === V4_SWAP_EXACT_OUT_SINGLE || actionId === V4_SWAP_EXACT_OUT_SINGLE ||
actionId === V4_SWAP_EXACT_OUT actionId === V4_SWAP_EXACT_OUT
) { ) {
lastSwapAction = i;
try { try {
swaps.push(decodeV4SwapAction(actionId, params[i])); swaps.push(decodeV4SwapAction(actionId, params[i]));
} catch { } 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. // Rule 3.
if (swapUndecoded) { if (swapUndecoded) {
return { return {
@@ -1457,10 +1429,10 @@ 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]);
if (v4) { if (v4) {
// A SETTLE of OPEN_DELTA (zero) that decodeV4Swap() // A SETTLE of OPEN_DELTA (zero) pays what the step's
// left at zero pays what the step's swap actions // swap actions spend of its currency, which the step
// spend of its currency, which the step states as its // states as its amount when that currency is its
// amount when that currency is its Token In. // Token In.
for (const s of v4.settledFromRouter) { for (const s of v4.settledFromRouter) {
let amount = s.amount; let amount = s.amount;
if (amount === 0n) { if (amount === 0n) {
-59
View File
@@ -650,65 +650,6 @@ describe("any token left in the router is not received", () => {
), ),
left: tokenLine(WETH, "WETH"), 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", 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( data: execute(