Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edfd01014d |
@@ -1250,14 +1250,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`,
|
A V4 step with no swap action sets only the sides its `SETTLE`, `SETTLE_ALL`,
|
||||||
`TAKE` or `TAKE_ALL` names.
|
`TAKE` or `TAKE_ALL` names.
|
||||||
|
|
||||||
An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side is WETH,
|
An `UNWRAP_WETH` step makes `Token Out` ETH only when no step set the output
|
||||||
on mainnet or Sepolia, or when no step set the output side; a WETH
|
side, or when the output side is WETH, on mainnet or Sepolia, that the step that
|
||||||
`Min. received` figure then reads in ETH. Otherwise `Token Out` and
|
set it paid to the router and that the router still holds when the `UNWRAP_WETH`
|
||||||
`Min. received` keep the output side's own token and figure, whether the swap
|
runs (Transaction Decoding below gives what the router holds). The router
|
||||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
unwraps only the WETH it holds, so WETH that step paid to the user, or to anyone
|
||||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
else, stays WETH: a swap that pays the router WETH, then a swap that pays the
|
||||||
the signature screen has its own amount wording, including `Unknown`; the
|
user WETH, then an unwrap, shows WETH with the second swap's minimum. The
|
||||||
SignApproval section below describes it.
|
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.
|
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
|
`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 minOutput = null;
|
||||||
let outputEstablished = false;
|
let outputEstablished = false;
|
||||||
let hasUnwrapWeth = 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 hasV2OrV3ExactOut = false;
|
||||||
let swapUndecoded = false;
|
let swapUndecoded = false;
|
||||||
let hasSubPlan = false;
|
let hasSubPlan = false;
|
||||||
@@ -1255,11 +1265,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
setInput(token, amount);
|
setInput(token, amount);
|
||||||
};
|
};
|
||||||
const setOutput = (token, amount) => {
|
const setOutput = (token, amount, recipients) => {
|
||||||
if (!present(token) && !present(amount)) return;
|
if (!present(token) && !present(amount)) return;
|
||||||
outputToken = present(token) ? token : null;
|
outputToken = present(token) ? token : null;
|
||||||
minOutput = present(amount) ? amount : null;
|
minOutput = present(amount) ? amount : null;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
|
outputRecipients = recipients;
|
||||||
};
|
};
|
||||||
|
|
||||||
// What the steps take from the sender beyond the amount of the step
|
// 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.
|
// does. It pays nobody.
|
||||||
const minBalance = allowedToFail ? 0n : b.minBalance;
|
const minBalance = allowedToFail ? 0n : b.minBalance;
|
||||||
if (!(minBalance === 0n && present(minOutput))) {
|
if (!(minBalance === 0n && present(minOutput))) {
|
||||||
setOutput(b.token, minBalance);
|
setOutput(b.token, minBalance, []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (command === "V3_SWAP_EXACT_IN") {
|
} 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),
|
// Always update output: in multi-step swaps (V3 → V4),
|
||||||
// the last swap step determines the final output token
|
// the last swap step determines the final output token
|
||||||
// and minimum received amount.
|
// and minimum received amount.
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1539,7 +1551,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountIn);
|
addUserPaid(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOutMin);
|
setOutput(s.tokenOut, s.amountOutMin, [s.recipient]);
|
||||||
if (!s.payerIsUser) {
|
if (!s.payerIsUser) {
|
||||||
spendFromRouter(s.tokenIn, s.amountIn);
|
spendFromRouter(s.tokenIn, s.amountIn);
|
||||||
}
|
}
|
||||||
@@ -1570,7 +1582,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else if (s.payerIsUser) {
|
} else if (s.payerIsUser) {
|
||||||
addUserPaid(s.tokenIn, s.amountInMax);
|
addUserPaid(s.tokenIn, s.amountInMax);
|
||||||
}
|
}
|
||||||
setOutput(s.tokenOut, s.amountOut);
|
setOutput(s.tokenOut, s.amountOut, [s.recipient]);
|
||||||
if (!isHop(i, s.tokenOut, s.recipient)) {
|
if (!isHop(i, s.tokenOut, s.recipient)) {
|
||||||
addPayee(s.recipient, s.tokenOut);
|
addPayee(s.recipient, s.tokenOut);
|
||||||
}
|
}
|
||||||
@@ -1728,6 +1740,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
(p) => p.currency === v4.tokenIn,
|
(p) => p.currency === v4.tokenIn,
|
||||||
);
|
);
|
||||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
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) {
|
if (v4 && v4.hasSwapAction) {
|
||||||
// A step with a swap action always sets the output
|
// A step with a swap action always sets the output
|
||||||
// side, as the last swap step wins, and sets the
|
// side, as the last swap step wins, and sets the
|
||||||
@@ -1744,13 +1764,15 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
outputToken = v4.tokenOut;
|
outputToken = v4.tokenOut;
|
||||||
minOutput = v4.amountOutMin;
|
minOutput = v4.amountOutMin;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
|
outputRecipients = takeRecipients;
|
||||||
} else if (v4) {
|
} else if (v4) {
|
||||||
// A step with no swap action sets only the sides its
|
// A step with no swap action sets only the sides its
|
||||||
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
// SETTLE, SETTLE_ALL, TAKE or TAKE_ALL names:
|
||||||
// setInputOnce() and setOutput() leave alone a side
|
// setInputOnce() and setOutput() leave alone a side
|
||||||
// whose two halves are both unstated.
|
// whose two halves are both unstated.
|
||||||
setInputOnce(v4.tokenIn, amountIn);
|
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
|
// Every V4 step, with a swap action or without, takes
|
||||||
// from the sender what the user pays it (an amount an
|
// 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") {
|
} else if (command === "UNWRAP_WETH") {
|
||||||
hasUnwrapWeth = true;
|
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]);
|
const u = decodeUnwrapWeth(inputs[i]);
|
||||||
if (u) {
|
if (u) {
|
||||||
addPayee(u.recipient);
|
addPayee(u.recipient);
|
||||||
@@ -1912,23 +1957,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
outputToken = null;
|
outputToken = null;
|
||||||
minOutput = null;
|
minOutput = null;
|
||||||
outputEstablished = true;
|
outputEstablished = true;
|
||||||
|
outputUnwrapped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve token info. A null token on either side means no step named
|
// 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
|
// 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
|
// 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
|
// 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
|
// when it unwraps the output side's WETH (`outputUnwrapped`), or when
|
||||||
// V4 step with a swap action, or an EXECUTE_SUB_PLAN step, did set it,
|
// no step set the output side. A V4 step with a swap action, or
|
||||||
// even when it set it unknown. Any other output keeps its own token
|
// an EXECUTE_SUB_PLAN step, did set it, even when it set it unknown.
|
||||||
// and figure: a swap that buys USDC and then unwraps the WETH it did
|
// Any other output keeps its own token and figure: a swap that pays
|
||||||
// not spend receives USDC.
|
// its WETH to the user and then unwraps receives WETH, and one that
|
||||||
const outputIsWeth =
|
// buys USDC and then unwraps the WETH it did not spend receives USDC.
|
||||||
present(outputToken) &&
|
|
||||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
|
||||||
const inInfo = tokenInfo(inputToken, sources);
|
const inInfo = tokenInfo(inputToken, sources);
|
||||||
const outInfo =
|
const outInfo =
|
||||||
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
outputUnwrapped || (hasUnwrapWeth && !outputEstablished)
|
||||||
? { symbol: "ETH", decimals: 18, address: null }
|
? { symbol: "ETH", decimals: 18, address: null }
|
||||||
: tokenInfo(outputToken, sources);
|
: tokenInfo(outputToken, sources);
|
||||||
|
|
||||||
|
|||||||
@@ -875,7 +875,7 @@ describe("uniswap decoder", () => {
|
|||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||||
[
|
[
|
||||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||||
USDC_ADDR,
|
USDC_ADDR,
|
||||||
SEPOLIA_WETH_ADDR,
|
SEPOLIA_WETH_ADDR,
|
||||||
]),
|
]),
|
||||||
@@ -958,7 +958,7 @@ describe("uniswap decoder", () => {
|
|||||||
const data = buildExecute(
|
const data = buildExecute(
|
||||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||||
[
|
[
|
||||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
encodeV2SwapExactIn(ROUTER_ADDR, 1000000n, 500000000000000n, [
|
||||||
USDT_ADDR,
|
USDT_ADDR,
|
||||||
WETH_ADDR,
|
WETH_ADDR,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
// (https://git.eeqj.de/sneak/AutistMask/issues/592), the address whose
|
||||||
// balance a BALANCE_CHECK_ERC20 checks
|
// balance a BALANCE_CHECK_ERC20 checks
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/600), any token a swap
|
// (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
|
// 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,
|
// Every swap command, SWEEP, WRAP_ETH, UNWRAP_WETH, TRANSFER, PAY_PORTION,
|
||||||
// PAY_PORTION_FULL_PRECISION, PERMIT2_TRANSFER_FROM, ACROSS_V4_DEPOSIT_V3 and
|
// 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"]],
|
[[[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.
|
// SWAP_EXACT_IN_SINGLE with no parameters, which cannot be read.
|
||||||
const UNREADABLE_SWAP = [0x06, "0x"];
|
const UNREADABLE_SWAP = [0x06, "0x"];
|
||||||
const SETTLE_USDT = [
|
const SETTLE_USDT = [
|
||||||
0x0b,
|
0x0b,
|
||||||
coder.encode(["address", "uint256", "bool"], [USDT, 0n, true]),
|
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 [
|
return [
|
||||||
0x0e,
|
0x0e,
|
||||||
coder.encode(
|
coder.encode(
|
||||||
["address", "address", "uint256"],
|
["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",
|
kind: "an UNWRAP_WETH to the user, then the swap",
|
||||||
data: execute(unwrapWeth(MSG_SENDER), toRouter),
|
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",
|
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
|
// 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
|
// 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
|
// only the swap's output. A V4 SETTLE can take more of the user's tokens than
|
||||||
|
|||||||
Reference in New Issue
Block a user