fix: a V4 step that spends some of its output currency shows no Min. received (closes #574) #667
@@ -1243,7 +1243,11 @@ only when no earlier step set it. It finds each side by three rules:
|
|||||||
is their sum; in any other case the step gives no `Amount`. The decoder reads
|
is their sum; in any other case the step gives no `Amount`. The decoder reads
|
||||||
no amount from an exact-out action, and an open delta is not a stated amount.
|
no amount from an exact-out action, and an open delta is not a stated amount.
|
||||||
Its `Min. received` is found the same way, from the `amountOutMin` of the
|
Its `Min. received` is found the same way, from the `amountOutMin` of the
|
||||||
actions that buy the output currency.
|
actions that buy the output currency, except that the step gives none when a
|
||||||
|
swap action in it also spends the output currency, because the step then does
|
||||||
|
not pay out all that those actions buy. A step that buys at least 0.5 WETH
|
||||||
|
with USDT and swaps 0.3 WETH of it for USDC, with WETH as its output side,
|
||||||
|
gives no `Min. received`.
|
||||||
3. If any swap action in the step could not be read, only `SETTLE`,
|
3. If any swap action in the step could not be read, only `SETTLE`,
|
||||||
`SETTLE_ALL`, `TAKE` and `TAKE_ALL` name its sides, and the step gives no
|
`SETTLE_ALL`, `TAKE` and `TAKE_ALL` name its sides, and the step gives no
|
||||||
`Amount` or `Min. received`. A side they did not name is unknown, and the
|
`Amount` or `Min. received`. A side they did not name is unknown, and the
|
||||||
|
|||||||
@@ -953,7 +953,9 @@ function totalAmount(amounts) {
|
|||||||
// 2. Amount is the amountIn of the swap action that spends Token In. If
|
// 2. Amount is the amountIn of the swap action that spends Token In. If
|
||||||
// several exact-in actions spend it, each with a stated amount, it is
|
// several exact-in actions spend it, each with a stated amount, it is
|
||||||
// their sum; in any other case there is none. Min. received is the same
|
// their sum; in any other case there is none. Min. received is the same
|
||||||
// for amountOutMin and the actions that buy Token Out.
|
// for amountOutMin and the actions that buy Token Out, except that there
|
||||||
|
// is none when a swap action in the step also spends Token Out: the step
|
||||||
|
// then does not pay out all that those actions buy.
|
||||||
// 3. If any swap action in the step cannot be read, only SETTLE, SETTLE_ALL,
|
// 3. If any swap action in the step cannot be read, only SETTLE, SETTLE_ALL,
|
||||||
// TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
|
// TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
|
||||||
// received.
|
// received.
|
||||||
@@ -1187,12 +1189,14 @@ function decodeV4Swap(input) {
|
|||||||
const boughtOnly = currenciesNotIn(bought, spent);
|
const boughtOnly = currenciesNotIn(bought, spent);
|
||||||
if (present(takeToken) || boughtOnly.length === 1) {
|
if (present(takeToken) || boughtOnly.length === 1) {
|
||||||
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
|
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
|
||||||
|
if (!spent.includes(tokenOut)) {
|
||||||
amountOutMin = totalAmount(
|
amountOutMin = totalAmount(
|
||||||
swaps
|
swaps
|
||||||
.filter((s) => s.tokenOut === tokenOut)
|
.filter((s) => s.tokenOut === tokenOut)
|
||||||
.map((s) => s.amountOutMin),
|
.map((s) => s.amountOutMin),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokenIn,
|
tokenIn,
|
||||||
|
|||||||
@@ -3730,6 +3730,88 @@ describe("uniswap decoder", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/574 — a V4 step that buys
|
||||||
|
// at least 0.5 WETH with 1,000 USDT, spends some of it on USDC, and takes
|
||||||
|
// the USDC and then the WETH, so its output side is WETH. Only what is
|
||||||
|
// left of the 0.5 WETH is sure to reach the user, and the screen read
|
||||||
|
// Min. received 0.5000 WETH.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
spend: "0.3 WETH for at least 594 USDC",
|
||||||
|
swap: {
|
||||||
|
id: V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
param: encodeV4ExactInSingle(
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
300000000000000000n,
|
||||||
|
594000000n,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spend: "0.3 WETH through DAI for at least 594 USDC",
|
||||||
|
swap: {
|
||||||
|
id: V4_SWAP_EXACT_IN,
|
||||||
|
param: encodeV4ExactIn(
|
||||||
|
WETH_ADDR,
|
||||||
|
[DAI_ADDR, USDC_ADDR],
|
||||||
|
300000000000000000n,
|
||||||
|
594000000n,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spend: "at most 0.3 WETH for exactly 594 USDC",
|
||||||
|
swap: {
|
||||||
|
id: V4_SWAP_EXACT_OUT_SINGLE_ID,
|
||||||
|
param: encodeV4ExactOutSingle(
|
||||||
|
WETH_ADDR,
|
||||||
|
USDC_ADDR,
|
||||||
|
594000000n,
|
||||||
|
300000000000000000n,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"a V4 step that buys WETH and spends $spend, with WETH as its output side, shows no Min. received",
|
||||||
|
({ swap }) => {
|
||||||
|
const data = buildExecute(
|
||||||
|
"0x10",
|
||||||
|
[
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
USDT_FOR_WETH_IN.id,
|
||||||
|
swap.id,
|
||||||
|
V4_SETTLE_ALL_ID,
|
||||||
|
V4_TAKE_ID,
|
||||||
|
V4_TAKE_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
USDT_FOR_WETH_IN.param,
|
||||||
|
swap.param,
|
||||||
|
encodeV4SettleAll(USDT_ADDR, 1000000000n),
|
||||||
|
encodeV4Take(USDC_ADDR),
|
||||||
|
encodeV4Take(WETH_ADDR),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
9999999999n,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = uniswap.decode(
|
||||||
|
data,
|
||||||
|
ROUTER_ADDR,
|
||||||
|
"0x1",
|
||||||
|
{},
|
||||||
|
USER_ADDR,
|
||||||
|
);
|
||||||
|
expect(result.name).toBe("Swap USDT → WETH");
|
||||||
|
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
|
||||||
|
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
|
||||||
|
expect(detail(result, "Min. received")).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// A V4 step that spends WETH on USDC and on DAI. Its swap actions buy two
|
// A V4 step that spends WETH on USDC and on DAI. Its swap actions buy two
|
||||||
// currencies that none of them spends, and CLOSE_CURRENCY, which the
|
// currencies that none of them spends, and CLOSE_CURRENCY, which the
|
||||||
// decoder does not read, names no side, so its output side is unknown.
|
// decoder does not read, names no side, so its output side is unknown.
|
||||||
|
|||||||
@@ -1332,6 +1332,10 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
left: tokenLine(WETH, "WETH"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
// The second swap action spends 1 USDC of what the first buys,
|
||||||
|
// so the step states no minimum
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/574).
|
||||||
|
noMin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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",
|
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",
|
||||||
@@ -1432,7 +1436,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
left: { label: "Token moved into the router", value: "ETH" },
|
left: { label: "Token moved into the router", value: "ETH" },
|
||||||
},
|
},
|
||||||
])("$kind", ({ data, left, spent, unread }) => {
|
])("$kind", ({ data, left, spent, unread, noMin }) => {
|
||||||
const result = decode(data);
|
const result = decode(data);
|
||||||
expect(labels(result)).toEqual([
|
expect(labels(result)).toEqual([
|
||||||
"Protocol",
|
"Protocol",
|
||||||
@@ -1458,7 +1462,7 @@ describe("any token left in the router is not received", () => {
|
|||||||
"Token Out",
|
"Token Out",
|
||||||
"Paid to",
|
"Paid to",
|
||||||
"Token moved into the router",
|
"Token moved into the router",
|
||||||
"Min. paid out",
|
...(noMin ? [] : ["Min. paid out"]),
|
||||||
"Steps",
|
"Steps",
|
||||||
"Deadline",
|
"Deadline",
|
||||||
]);
|
]);
|
||||||
@@ -1474,7 +1478,9 @@ describe("any token left in the router is not received", () => {
|
|||||||
}
|
}
|
||||||
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||||
expect(detail(result, "Token moved into the router")).toEqual(left);
|
expect(detail(result, "Token moved into the router")).toEqual(left);
|
||||||
expect(detail(result, "Min. paid out").value).toBe("1990.0000 USDC");
|
expect(detail(result, "Min. paid out")?.value).toBe(
|
||||||
|
noMin ? undefined : "1990.0000 USDC",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Uniswap's own routes: a swap paid in ETH wraps into the router what its
|
// Uniswap's own routes: a swap paid in ETH wraps into the router what its
|
||||||
|
|||||||
Reference in New Issue
Block a user