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
|
||||
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
|
||||
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`,
|
||||
`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
|
||||
|
||||
+10
-6
@@ -953,7 +953,9 @@ function totalAmount(amounts) {
|
||||
// 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
|
||||
// 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,
|
||||
// TAKE and TAKE_ALL name the sides, and there is no Amount or Min.
|
||||
// received.
|
||||
@@ -1187,11 +1189,13 @@ function decodeV4Swap(input) {
|
||||
const boughtOnly = currenciesNotIn(bought, spent);
|
||||
if (present(takeToken) || boughtOnly.length === 1) {
|
||||
tokenOut = present(takeToken) ? takeToken : boughtOnly[0];
|
||||
amountOutMin = totalAmount(
|
||||
swaps
|
||||
.filter((s) => s.tokenOut === tokenOut)
|
||||
.map((s) => s.amountOutMin),
|
||||
);
|
||||
if (!spent.includes(tokenOut)) {
|
||||
amountOutMin = totalAmount(
|
||||
swaps
|
||||
.filter((s) => s.tokenOut === tokenOut)
|
||||
.map((s) => s.amountOutMin),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -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
|
||||
// currencies that none of them spends, and CLOSE_CURRENCY, which the
|
||||
// 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"),
|
||||
// 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",
|
||||
@@ -1432,7 +1436,7 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
left: { label: "Token moved into the router", value: "ETH" },
|
||||
},
|
||||
])("$kind", ({ data, left, spent, unread }) => {
|
||||
])("$kind", ({ data, left, spent, unread, noMin }) => {
|
||||
const result = decode(data);
|
||||
expect(labels(result)).toEqual([
|
||||
"Protocol",
|
||||
@@ -1458,7 +1462,7 @@ describe("any token left in the router is not received", () => {
|
||||
"Token Out",
|
||||
"Paid to",
|
||||
"Token moved into the router",
|
||||
"Min. paid out",
|
||||
...(noMin ? [] : ["Min. paid out"]),
|
||||
"Steps",
|
||||
"Deadline",
|
||||
]);
|
||||
@@ -1474,7 +1478,9 @@ describe("any token left in the router is not received", () => {
|
||||
}
|
||||
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user