fix: WETH paid to the user before an UNWRAP_WETH reads as WETH (closes #607)
An UNWRAP_WETH made Token Out ETH whenever the output side was WETH, though the router unwraps only the WETH it holds. decode() now records where the step that set the output side paid its output, and an UNWRAP_WETH makes the output ETH only when that step paid WETH to the router and the router still holds WETH. When that step paid only part of its WETH to the router, or made a payout the decoder cannot read, the output reads ETH with no minimum: the decoder does not count how much of that step's minimum reaches the router, nor read the UNWRAP_WETH's own minimum. A BALANCE_CHECK_ERC20 pays nobody, so it is never read as unwrapped. README Display Consistency says the same. Model: opus-5-5
This commit was merged in pull request #640.
This commit is contained in:
@@ -1253,14 +1253,24 @@ Decoding). So a step whose USDT-for-USDC swap action could not be read, with a
|
||||
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||
`TAKE` or `TAKE_ALL` names.
|
||||
|
||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
|
||||
on mainnet or Sepolia, or when no step set the output side; a WETH
|
||||
`Min. received` figure then reads in ETH. Otherwise `Token Out` and
|
||||
`Min. received` keep the output side's own token and figure, whether the swap
|
||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
||||
the signature screen has its own amount wording, including `Unknown`; the
|
||||
SignApproval section below describes it.
|
||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when no step set the output
|
||||
side, or when the output side is WETH, on mainnet or Sepolia, that the step that
|
||||
set it paid to the router and that the router still holds when the `UNWRAP_WETH`
|
||||
runs (Transaction Decoding below gives what the router holds). The router
|
||||
unwraps only the WETH it holds, so WETH that step paid to the user, or to anyone
|
||||
else, stays WETH: a swap that pays the router WETH, then a swap that pays the
|
||||
user WETH, then an unwrap, shows WETH with the second swap's minimum. The
|
||||
minimum reads in ETH when that step paid all its WETH to the router. When it
|
||||
paid only part of it to the router, as a V4 step does that `TAKE`s some of its
|
||||
WETH to the user and the rest to the router, or paid the router WETH beside a
|
||||
payout the decoder cannot read, the swap shows ETH with no minimum: the decoder
|
||||
does not count how much of that step's minimum reaches the router, nor read the
|
||||
`UNWRAP_WETH`'s own minimum. A later step that sets the output side sets it
|
||||
anew. Otherwise `Token Out` and `Min. received` keep the output side's own token
|
||||
and figure, whether the swap was paid in ETH or in a token: a V2 exact-out swap
|
||||
that buys USDC and then unwraps the WETH it did not spend shows USDC. The token
|
||||
permission warning on the signature screen has its own amount wording, including
|
||||
`Unknown`; the SignApproval section below describes it.
|
||||
|
||||
The swap's `Deadline` line is the router's deadline as a UTC date and time, e.g.
|
||||
`2026-02-27 08:25:51`. A JavaScript date reaches only to 275760-09-13 00:00:00
|
||||
|
||||
+59
-15
@@ -1219,6 +1219,16 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
let hasUnwrapWeth = false;
|
||||
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
||||
// which the router holds; cleared whenever the output side is set
|
||||
// again.
|
||||
let outputUnwrapped = false;
|
||||
// Where the step that set the output side paid its output: the
|
||||
// recipient of a swap step, as written, or of each TAKE, TAKE_ALL and
|
||||
// TAKE_PORTION of it in a V4 step, a TAKE_ALL as paid to the sender,
|
||||
// with null for a payout of that step this decoder did not read. A
|
||||
// BALANCE_CHECK_ERC20 pays nobody.
|
||||
let outputRecipients = [];
|
||||
let hasV2OrV3ExactOut = false;
|
||||
let swapUndecoded = false;
|
||||
let hasSubPlan = false;
|
||||
@@ -1255,11 +1265,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
setInput(token, amount);
|
||||
};
|
||||
const setOutput = (token, amount) => {
|
||||
const setOutput = (token, amount, recipients) => {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
outputToken = present(token) ? token : null;
|
||||
minOutput = present(amount) ? amount : null;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
outputRecipients = recipients;
|
||||
};
|
||||
|
||||
// What the steps take from the sender beyond the amount of the step
|
||||
@@ -1505,7 +1517,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// does. It pays nobody.
|
||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||
if (!(minBalance === 0n && present(minOutput))) {
|
||||
setOutput(b.token, minBalance);
|
||||
setOutput(b.token, minBalance, []);
|
||||
}
|
||||
}
|
||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
||||
@@ -1519,7 +1531,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// Always update output: in multi-step swaps (V3 → V4),
|
||||
// the last swap step determines the final output token
|
||||
// and minimum received amount.
|
||||
setOutput(s.tokenOut, s.amountOutMin);
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1539,7 +1551,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountIn);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOutMin);
|
||||
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||
if (!s.payerIsUser) {
|
||||
spendFromRouter(s.tokenIn, s.amountIn);
|
||||
}
|
||||
@@ -1570,7 +1582,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else if (s.payerIsUser) {
|
||||
addUserPaid(s.tokenIn, s.amountInMax);
|
||||
}
|
||||
setOutput(s.tokenOut, s.amountOut);
|
||||
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||
addPayee(s.recipient, s.tokenOut);
|
||||
}
|
||||
@@ -1728,6 +1740,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
(p) => p.currency === v4.tokenIn,
|
||||
);
|
||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
||||
// Where the step pays its output, if it sets the output
|
||||
// side (see outputRecipients).
|
||||
const takeRecipients = v4
|
||||
? v4.takes
|
||||
.filter((t) => t.currency === v4.tokenOut)
|
||||
.map((t) => t.recipient)
|
||||
.concat(v4.recipientUnread ? [null] : [])
|
||||
: [];
|
||||
if (v4 && v4.hasSwapAction) {
|
||||
// A step with a swap action always sets the output
|
||||
// side, as the last swap step wins, and sets the
|
||||
@@ -1744,13 +1764,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputToken = v4.tokenOut;
|
||||
minOutput = v4.amountOutMin;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
outputRecipients = takeRecipients;
|
||||
} else if (v4) {
|
||||
// A step with no swap action sets only the sides its
|
||||
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||
// setInputOnce() and setOutput() leave alone a side
|
||||
// whose two halves are both unstated.
|
||||
setInputOnce(v4.tokenIn, amountIn);
|
||||
setOutput(v4.tokenOut, v4.amountOutMin);
|
||||
setOutput(v4.tokenOut, v4.amountOutMin, takeRecipients);
|
||||
}
|
||||
// Every V4 step, with a swap action or without, takes
|
||||
// from the sender what the user pays it (an amount an
|
||||
@@ -1772,6 +1794,29 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
}
|
||||
} else if (command === "UNWRAP_WETH") {
|
||||
hasUnwrapWeth = true;
|
||||
// The router unwraps all the WETH it holds, even when the
|
||||
// recipient cannot be read. The output side's WETH is
|
||||
// among it when the step that set the output side paid it
|
||||
// to the router and the router still holds WETH. WETH
|
||||
// that step paid to the user, or to anyone else, stays
|
||||
// WETH, so when that step also paid some of its output
|
||||
// elsewhere, or made a payout this decoder did not read,
|
||||
// only part of it may be unwrapped. This decoder does not
|
||||
// count how much of that step's minimum then reaches the
|
||||
// router, nor read the UNWRAP_WETH's own minimum, so the
|
||||
// output side keeps no figure.
|
||||
const toRouter = (r) => present(r) && isRouter(r);
|
||||
if (
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase()) &&
|
||||
heldByRouter.has(outputToken.toLowerCase()) &&
|
||||
outputRecipients.some(toRouter)
|
||||
) {
|
||||
outputUnwrapped = true;
|
||||
if (!outputRecipients.every(toRouter)) {
|
||||
minOutput = null;
|
||||
}
|
||||
}
|
||||
const u = decodeUnwrapWeth(inputs[i]);
|
||||
if (u) {
|
||||
addPayee(u.recipient);
|
||||
@@ -1912,23 +1957,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
outputToken = null;
|
||||
minOutput = null;
|
||||
outputEstablished = true;
|
||||
outputUnwrapped = false;
|
||||
}
|
||||
|
||||
// Resolve token info. A null token on either side means no step named
|
||||
// a currency for it, a V4 step with a swap action left it unknown, or
|
||||
// an EXECUTE_SUB_PLAN step did; tokenInfo() refuses rather than calling
|
||||
// it ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||
// when the output side is WETH, or when no step set the output side. A
|
||||
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
||||
// even when it set it unknown. Any other output keeps its own token
|
||||
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
||||
// not spend receives USDC.
|
||||
const outputIsWeth =
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||
// when it unwraps the output side's WETH (`outputUnwrapped`), or when
|
||||
// no step set the output side. A V4 step with a swap action, or
|
||||
// an EXECUTE_SUB_PLAN step, did set it, even when it set it unknown.
|
||||
// Any other output keeps its own token and figure: a swap that pays
|
||||
// its WETH to the user and then unwraps receives WETH, and one that
|
||||
// buys USDC and then unwraps the WETH it did not spend receives USDC.
|
||||
const inInfo = tokenInfo(inputToken, sources);
|
||||
const outInfo =
|
||||
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
||||
outputUnwrapped || (hasUnwrapWeth && !outputEstablished)
|
||||
? { symbol: "ETH", decimals: 18, address: null }
|
||||
: tokenInfo(outputToken, sources);
|
||||
|
||||
|
||||
@@ -875,7 +875,7 @@ describe("uniswap decoder", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
||||
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||
USDC_ADDR,
|
||||
SEPOLIA_WETH_ADDR,
|
||||
]),
|
||||
@@ -958,7 +958,7 @@ describe("uniswap decoder", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
||||
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
]),
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
||||
// balance a BALANCE_CHECK_ERC20 checks
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600), any token a swap
|
||||
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606), and
|
||||
// leaves in the router (https://git.eeqj.de/sneak/AutistMask/issues/606),
|
||||
// what each PERMIT2_TRANSFER_FROM moves
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/611).
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/611), and when an
|
||||
// UNWRAP_WETH makes the output ETH
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/607).
|
||||
//
|
||||
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
||||
@@ -268,18 +270,28 @@ const SWAP_USDT_FOR_ETH = [
|
||||
[[[ZERO, USDT, 500, 10, ZERO], false, USDT_2000, ONE_WETH, "0x"]],
|
||||
),
|
||||
];
|
||||
const SWAP_USDT_FOR_WETH = [
|
||||
0x06, // SWAP_EXACT_IN_SINGLE
|
||||
coder.encode(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
[[[USDT, WETH, 500, 10, ZERO], true, USDT_2000, ONE_WETH, "0x"]],
|
||||
),
|
||||
];
|
||||
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||
const SETTLE_USDT = [
|
||||
0x0b,
|
||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
||||
];
|
||||
function take(currency, recipient) {
|
||||
// A TAKE of `amount`, by default zero: the open delta, all the step owes.
|
||||
function take(currency, recipient, amount = 0n) {
|
||||
return [
|
||||
0x0e,
|
||||
coder.encode(
|
||||
["address", "address", "uint256"],
|
||||
[currency, recipient, 0n],
|
||||
[currency, recipient, amount],
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -531,7 +543,7 @@ describe("an output left in the router is not received", () => {
|
||||
{
|
||||
kind: "an UNWRAP_WETH to the user, then the swap",
|
||||
data: execute(unwrapWeth(MSG_SENDER), toRouter),
|
||||
min: "1.0000 ETH",
|
||||
min: "1.0000 WETH",
|
||||
},
|
||||
{
|
||||
kind: "the swap, then a WRAP_ETH of zero to the user",
|
||||
@@ -568,6 +580,171 @@ describe("an output left in the router is not received", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/607: an UNWRAP_WETH pays out,
|
||||
// as ETH, the WETH the router holds when it runs. Token Out read ETH whenever
|
||||
// the output side was WETH, so a swap that paid its WETH to the user before
|
||||
// the UNWRAP_WETH read as ETH, though the user receives WETH.
|
||||
describe("an UNWRAP_WETH makes the output ETH only when the router holds it", () => {
|
||||
test("a swap paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||
const result = decode(
|
||||
execute(swapToWeth(MSG_SENDER), unwrapWeth(MSG_SENDER)),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
test("a swap paying its WETH to the router, then an UNWRAP_WETH to the user, reads as ETH", () => {
|
||||
const result = decode(
|
||||
execute(swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER)),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 ETH");
|
||||
});
|
||||
|
||||
// The SWEEP pays the user all the WETH the router holds, so the
|
||||
// UNWRAP_WETH finds none of it.
|
||||
test("a swap paying its WETH to the router, then a SWEEP of it to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
sweep(WETH, MSG_SENDER),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
// The user receives the first step's WETH as WETH and the second's as
|
||||
// ETH. The second step sets the output side and pays its WETH to the
|
||||
// router, so the output reads as ETH with the second step's minimum.
|
||||
test("two swaps, the second paying its WETH to the router, then an UNWRAP_WETH, reads as ETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(MSG_SENDER),
|
||||
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
|
||||
USDC,
|
||||
WETH,
|
||||
]),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
||||
});
|
||||
|
||||
// The same two swaps the other way round. The user receives the first
|
||||
// step's WETH as ETH and the second's as WETH. The second step sets the
|
||||
// output side and pays its WETH to the user, so the output reads as WETH
|
||||
// with the second step's minimum, though the router holds the first's.
|
||||
test("two swaps, the second paying its WETH to the user, then an UNWRAP_WETH, reads as WETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v2Swap(0x08, ADDRESS_THIS, USDC_1990, ONE_WETH / 2n, [
|
||||
USDC,
|
||||
WETH,
|
||||
]),
|
||||
swapToWeth(MSG_SENDER),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDC → WETH");
|
||||
expect(detail(result, "Min. received").value).toBe("1.0000 WETH");
|
||||
});
|
||||
|
||||
// One V4 step buys at least 1 WETH and TAKEs 0.6 WETH to the user and the
|
||||
// rest to the router. The user receives 0.6 WETH and the rest as ETH.
|
||||
// Neither minimum is shown: the decoder does not count how much of the
|
||||
// step's minimum reaches the router, nor read the UNWRAP_WETH's own.
|
||||
test("one V4 step paying part of its WETH to the user and the rest to the router, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_WETH,
|
||||
SETTLE_USDT,
|
||||
take(WETH, MSG_SENDER, (ONE_WETH * 6n) / 10n),
|
||||
take(WETH, ADDRESS_THIS),
|
||||
),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(labels(result)).toEqual([
|
||||
"Protocol",
|
||||
"Token In",
|
||||
"Amount",
|
||||
"Token Out",
|
||||
"Steps",
|
||||
"Deadline",
|
||||
]);
|
||||
});
|
||||
|
||||
// A TAKE the decoder cannot read may pay part of the WETH to the user.
|
||||
test("one V4 step paying WETH to the router beside a TAKE that cannot be read, then an UNWRAP_WETH, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
v4Swap(
|
||||
SWAP_USDT_FOR_WETH,
|
||||
SETTLE_USDT,
|
||||
withDirtyWord(take(WETH, MSG_SENDER), 1),
|
||||
take(WETH, ADDRESS_THIS),
|
||||
),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
expect(detail(result, "Min. paid out")).toBeUndefined();
|
||||
});
|
||||
|
||||
// A step after the UNWRAP_WETH that sets the output side sets it anew.
|
||||
test("a swap paying WETH to the user after an UNWRAP_WETH of an earlier swap's WETH reads as WETH", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
swapToWeth(ADDRESS_THIS),
|
||||
unwrapWeth(MSG_SENDER),
|
||||
v2Swap(0x08, MSG_SENDER, USDC_1990, ONE_WETH / 2n, [
|
||||
USDC,
|
||||
WETH,
|
||||
]),
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 WETH");
|
||||
});
|
||||
|
||||
// The EXECUTE_SUB_PLAN's own steps, which the decoder does not read, buy
|
||||
// the user USDC as well, so Token Out is not named.
|
||||
test("a swap paying its WETH to the router, then an UNWRAP_WETH, then an EXECUTE_SUB_PLAN, names no Token Out", () => {
|
||||
const subPlan = [
|
||||
0x21,
|
||||
coder.encode(
|
||||
["bytes", "bytes[]"],
|
||||
[
|
||||
"0x00",
|
||||
[
|
||||
v3SwapExactIn(
|
||||
MSG_SENDER,
|
||||
USDT_2000,
|
||||
USDC_1990,
|
||||
USDT,
|
||||
USDC,
|
||||
)[1],
|
||||
],
|
||||
],
|
||||
),
|
||||
];
|
||||
const result = decode(
|
||||
execute(swapToWeth(ADDRESS_THIS), unwrapWeth(MSG_SENDER), subPlan),
|
||||
);
|
||||
expect(result.name).toBe("Uniswap Swap");
|
||||
expect(detail(result, "Token Out").value).toBe(UNDECODED);
|
||||
expect(detail(result, "Min. received")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/606: any token a step pays the
|
||||
// router stays there unless a later step pays out or spends all of it, not
|
||||
// only the swap's output. A V4 SETTLE can take more of the user's tokens than
|
||||
|
||||
Reference in New Issue
Block a user