Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182742152d |
@@ -3473,8 +3473,11 @@ Currently supported:
|
|||||||
the user's wallet. Every V4 step, with a swap action or without, takes from
|
the user's wallet. Every V4 step, with a swap action or without, takes from
|
||||||
the user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and
|
the user's wallet what its `SETTLE`s paid by the user (`payerIsUser` true) and
|
||||||
its `SETTLE_ALL`s pay, for each token on its own; what the step that sets the
|
its `SETTLE_ALL`s pay, for each token on its own; what the step that sets the
|
||||||
input side takes of any other token counts as a later step's take. A `SETTLE`
|
input side takes of any other token counts as a later step's take. A V4
|
||||||
of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
|
`SETTLE` of ETH paid from the router still takes from the user's wallet, since
|
||||||
|
the router holds the transaction's value. When an earlier step left ETH in the
|
||||||
|
router, such a `SETTLE` gets lines of its own, with `Up to` before its amount.
|
||||||
|
A `SETTLE` of any amount but `OPEN_DELTA` pays that amount, and a `SETTLE` of
|
||||||
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
|
`OPEN_DELTA` or a `SETTLE_ALL` pays what the step owes, as above. So a V4 step
|
||||||
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
|
that swaps 400 USDT, with a `SETTLE` of 5,000 USDT paid by the user and the
|
||||||
rest taken to another address, reads `5000.0000 USDT`. A later step paid from
|
rest taken to another address, reads `5000.0000 USDT`. A later step paid from
|
||||||
|
|||||||
+61
-18
@@ -1338,7 +1338,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// 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
|
||||||
// that sets the input side, as { token, amount }, in step order.
|
// that sets the input side, as { token, amount }, in step order.
|
||||||
// After the loop each joins the Amount line or gets lines of its own
|
// After the loop each joins the Amount line or gets lines of its own
|
||||||
// (see permitStands).
|
// (see permitStands). A take with `atMost` set may take less than
|
||||||
|
// `amount`: it never joins the Amount line, and its line reads
|
||||||
|
// "Up to" and the amount.
|
||||||
const userPaid = [];
|
const userPaid = [];
|
||||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
||||||
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes
|
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes
|
||||||
@@ -1815,6 +1817,37 @@ 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]);
|
||||||
const inputSetBefore = inputEstablished;
|
const inputSetBefore = inputEstablished;
|
||||||
|
// V4Router pays a SETTLE of ETH from the router's own
|
||||||
|
// ETH, whichever payer the SETTLE names. The router holds
|
||||||
|
// the transaction's value, and any ETH an earlier step
|
||||||
|
// left it (see heldByRouter). So the user also pays a
|
||||||
|
// SETTLE of ETH that names the router as payer. The user
|
||||||
|
// pays all of its amount when the router holds no other
|
||||||
|
// ETH, and up to its amount otherwise. CONTRACT_BALANCE
|
||||||
|
// pays all the router holds, an amount the calldata does
|
||||||
|
// not state.
|
||||||
|
const routerHeldEth = heldByRouter.has(
|
||||||
|
"0x0000000000000000000000000000000000000000",
|
||||||
|
);
|
||||||
|
const ethFromRouter = (v4?.settledFromRouter ?? [])
|
||||||
|
.filter(
|
||||||
|
(s) =>
|
||||||
|
s.currency ===
|
||||||
|
"0x0000000000000000000000000000000000000000",
|
||||||
|
)
|
||||||
|
.map((s) => ({
|
||||||
|
currency: s.currency,
|
||||||
|
amount:
|
||||||
|
s.amount === CONTRACT_BALANCE ? null : s.amount,
|
||||||
|
}));
|
||||||
|
// What the user pays the step: what decodeV4Swap() says,
|
||||||
|
// and the ETH of each SETTLE from the router when the
|
||||||
|
// router holds no other ETH.
|
||||||
|
const paidByUser = v4
|
||||||
|
? v4.paidByUser.concat(
|
||||||
|
routerHeldEth ? [] : ethFromRouter,
|
||||||
|
)
|
||||||
|
: [];
|
||||||
if (v4) {
|
if (v4) {
|
||||||
// Each SETTLE paid from the router spends the amount
|
// Each SETTLE paid from the router spends the amount
|
||||||
// decodeV4Swap() says it pays.
|
// decodeV4Swap() says it pays.
|
||||||
@@ -1845,10 +1878,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
commandNames[i] += " (could not be decoded)";
|
commandNames[i] += " (could not be decoded)";
|
||||||
}
|
}
|
||||||
// The step's amount, if it sets the input side: what the
|
// The step's amount, if it sets the input side: what the
|
||||||
// user pays it of its input currency (see
|
// user pays it of its input currency, or, when the user
|
||||||
// decodeV4Swap()), or, when the user pays none of it,
|
// pays none of it, what its swap actions spend of it,
|
||||||
// what its swap actions spend of it, whoever pays that.
|
// whoever pays that.
|
||||||
const paidIn = v4?.paidByUser.find(
|
const paidIn = paidByUser.find(
|
||||||
(p) => p.currency === v4.tokenIn,
|
(p) => p.currency === v4.tokenIn,
|
||||||
);
|
);
|
||||||
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
const amountIn = paidIn ? paidIn.amount : v4?.amountIn;
|
||||||
@@ -1902,10 +1935,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// router still runs it.
|
// router still runs it.
|
||||||
const setInputHere = inputEstablished && !inputSetBefore;
|
const setInputHere = inputEstablished && !inputSetBefore;
|
||||||
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
if (setInputHere) inputPaidByUser = paidIn !== undefined;
|
||||||
if (v4) {
|
for (const p of paidByUser) {
|
||||||
for (const p of v4.paidByUser) {
|
if (setInputHere && p === paidIn) continue;
|
||||||
if (setInputHere && p === paidIn) continue;
|
addUserPaid(p.currency, p.amount);
|
||||||
addUserPaid(p.currency, p.amount);
|
}
|
||||||
|
if (routerHeldEth) {
|
||||||
|
for (const p of ethFromRouter) {
|
||||||
|
userPaid.push({
|
||||||
|
token: p.currency,
|
||||||
|
amount: p.amount,
|
||||||
|
atMost: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!v4 || v4.settleUnread) {
|
if (!v4 || v4.settleUnread) {
|
||||||
@@ -2013,10 +2053,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// The total is what the steps take of Token In: the amount of the
|
// The total is what the steps take of Token In: the amount of the
|
||||||
// step that set the input side, with each later take of that token
|
// step that set the input side, with each later take of that token
|
||||||
// added to it while both are quantities. A take that cannot be added,
|
// added to it while both are quantities. A take that cannot be added,
|
||||||
// being of another token or coming after an amount that is not a
|
// being of another token, coming after an amount that is not a
|
||||||
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
|
// quantity (a V4 open delta, ALREADY_PAID, or none stated), or having
|
||||||
// lines of its own. One whose amount is not known also leaves the
|
// `atMost` set, gets lines of its own. One whose amount is not known
|
||||||
// total unknown, with no figure.
|
// also leaves the total unknown, with no figure.
|
||||||
//
|
//
|
||||||
// A PERMIT2_PERMIT names Token In only when its amount stands as the
|
// A PERMIT2_PERMIT names Token In only when its amount stands as the
|
||||||
// Amount figure in place of the total, which it does only in the
|
// Amount figure in place of the total, which it does only in the
|
||||||
@@ -2093,7 +2133,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (
|
if (
|
||||||
ofTokenIn &&
|
ofTokenIn &&
|
||||||
typeof total === "bigint" &&
|
typeof total === "bigint" &&
|
||||||
typeof t.amount === "bigint"
|
typeof t.amount === "bigint" &&
|
||||||
|
!t.atMost
|
||||||
) {
|
) {
|
||||||
total += t.amount;
|
total += t.amount;
|
||||||
inTotal.push(t);
|
inTotal.push(t);
|
||||||
@@ -2196,8 +2237,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// V2's already-paid zero, neither of which is a quantity at all (see
|
// V2's already-paid zero, neither of which is a quantity at all (see
|
||||||
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
||||||
// tests come first — the sentinels are not bigints and cannot be
|
// tests come first — the sentinels are not bigints and cannot be
|
||||||
// compared with one.
|
// compared with one. `atMost` marks an amount that is only the most a
|
||||||
const spentText = (amount, info) => {
|
// take may be (see userPaid).
|
||||||
|
const spentText = (amount, info, atMost) => {
|
||||||
if (amount === OPEN_DELTA) {
|
if (amount === OPEN_DELTA) {
|
||||||
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||||
}
|
}
|
||||||
@@ -2210,7 +2252,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
if (amount >= MAX_UINT160) {
|
if (amount >= MAX_UINT160) {
|
||||||
return { raw: "Unlimited", display: "Unlimited" };
|
return { raw: "Unlimited", display: "Unlimited" };
|
||||||
}
|
}
|
||||||
if (hasV2OrV3ExactOut) {
|
if (hasV2OrV3ExactOut || atMost) {
|
||||||
// A V2 or V3 exact-out swap spends at most this figure,
|
// A V2 or V3 exact-out swap spends at most this figure,
|
||||||
// whichever steps gave it (an amountInMax, the WRAP_ETH of a
|
// whichever steps gave it (an amountInMax, the WRAP_ETH of a
|
||||||
// swap paid in ETH, a permit, or several steps' total), so it
|
// swap paid in ETH, a permit, or several steps' total), so it
|
||||||
@@ -2270,7 +2312,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
details.push({
|
details.push({
|
||||||
label: "Moved from the sender",
|
label: "Moved from the sender",
|
||||||
value: present(s.amount)
|
value: present(s.amount)
|
||||||
? spentText(s.amount, tokenInfo(s.token, sources)).display
|
? spentText(s.amount, tokenInfo(s.token, sources), s.atMost)
|
||||||
|
.display
|
||||||
: UNDECODED_CURRENCY,
|
: UNDECODED_CURRENCY,
|
||||||
});
|
});
|
||||||
if (present(s.token)) {
|
if (present(s.token)) {
|
||||||
|
|||||||
@@ -3439,6 +3439,77 @@ describe("uniswap decoder", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/682 — V4 pays a SETTLE
|
||||||
|
// of ETH from the router's own ETH, whichever payer the SETTLE names,
|
||||||
|
// and the router holds the transaction's value. On next at 36b2002 a
|
||||||
|
// SETTLE of 1 ETH that names the router as payer gave no ETH line.
|
||||||
|
describe("a V4 SETTLE of ETH that names the router as payer", () => {
|
||||||
|
const ONE_ETH = 1000000000000000000n;
|
||||||
|
// A V4 swap of `amount` of ETH for USDC. The SETTLE names the
|
||||||
|
// router as payer.
|
||||||
|
const v4EthFromRouter = (amount) =>
|
||||||
|
encodeV4Swap(
|
||||||
|
new Uint8Array([
|
||||||
|
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||||
|
V4_SETTLE_ID,
|
||||||
|
V4_TAKE_ALL_ID,
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
encodeV4ExactInSingle(ZERO_ADDR, USDC_ADDR, amount, 1n),
|
||||||
|
settle(ZERO_ADDR, amount, false),
|
||||||
|
encodeV4TakeAll(USDC_ADDR, 1n),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
test("after a V3 exact-in of 600 USDT, shows the 1 ETH moved from the sender", () => {
|
||||||
|
const result = decodeSteps(
|
||||||
|
[0x00, 0x10],
|
||||||
|
[V3_600, v4EthFromRouter(ONE_ETH)],
|
||||||
|
);
|
||||||
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
|
["Amount", "600.0000 USDT"],
|
||||||
|
["Moved from the sender", "1.0000 ETH"],
|
||||||
|
["Token moved", "ETH"],
|
||||||
|
["Moved to", "The swap step that spends it"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The UNWRAP_WETH leaves ETH in the router. The SETTLE may spend
|
||||||
|
// that ETH before the transaction's value.
|
||||||
|
test("after an UNWRAP_WETH into the router, shows at most 1 ETH moved from the sender", () => {
|
||||||
|
const result = decodeSteps(
|
||||||
|
[0x00, 0x0c, 0x10],
|
||||||
|
[
|
||||||
|
encodeV3SwapExactIn(ROUTER_ADDR, USDT_600, 1n, [
|
||||||
|
USDT_ADDR,
|
||||||
|
WETH_ADDR,
|
||||||
|
]),
|
||||||
|
encodeWrapEth(ROUTER_ADDR, 0n), // UNWRAP_WETH, same encoding
|
||||||
|
v4EthFromRouter(ONE_ETH),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
|
["Amount", "600.0000 USDT"],
|
||||||
|
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||||
|
["Token moved", "ETH"],
|
||||||
|
["Moved to", "The swap step that spends it"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("after a WRAP_ETH of 0.5 ETH, adds the SETTLE's 0.5 ETH to the Amount line", () => {
|
||||||
|
const result = decodeSteps(
|
||||||
|
[0x0b, 0x10],
|
||||||
|
[
|
||||||
|
encodeWrapEth(ROUTER_ADDR, ONE_ETH / 2n),
|
||||||
|
v4EthFromRouter(ONE_ETH / 2n),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(linesBetweenTokens(result)).toEqual([
|
||||||
|
["Amount", "1.0000 ETH"],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
// https://git.eeqj.de/sneak/AutistMask/issues/359 — the output half, in
|
||||||
|
|||||||
@@ -1661,9 +1661,18 @@ describe("any token left in the router is not received", () => {
|
|||||||
),
|
),
|
||||||
min: "1990.0000 USDC",
|
min: "1990.0000 USDC",
|
||||||
},
|
},
|
||||||
{
|
])("$kind reads as a swap paid to the user", ({ data, min }) => {
|
||||||
kind: "a swap paying WETH to the router, an UNWRAP_WETH into the router, then a V4 swap settling all the ETH from the router",
|
const result = decode(data);
|
||||||
data: execute(
|
expect(labels(result)).toEqual(PAID_TO_USER);
|
||||||
|
expect(detail(result, "Min. received").value).toBe(min);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/682 — the SETTLE spends all
|
||||||
|
// the router's ETH, the transaction's value included, so the sender pays
|
||||||
|
// an amount the calldata does not state.
|
||||||
|
test("a swap paying WETH to the router, an UNWRAP_WETH into the router, then a V4 swap settling all the ETH from the router reads as a swap paid to the user", () => {
|
||||||
|
const result = decode(
|
||||||
|
execute(
|
||||||
swapToWeth(ADDRESS_THIS),
|
swapToWeth(ADDRESS_THIS),
|
||||||
unwrapWeth(ADDRESS_THIS),
|
unwrapWeth(ADDRESS_THIS),
|
||||||
v4Swap(
|
v4Swap(
|
||||||
@@ -1672,12 +1681,23 @@ describe("any token left in the router is not received", () => {
|
|||||||
takeAll(USDC),
|
takeAll(USDC),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
min: "1990.0000 USDC",
|
);
|
||||||
},
|
expect(labels(result)).toEqual([
|
||||||
])("$kind reads as a swap paid to the user", ({ data, min }) => {
|
"Protocol",
|
||||||
const result = decode(data);
|
"Token In",
|
||||||
expect(labels(result)).toEqual(PAID_TO_USER);
|
"Amount",
|
||||||
expect(detail(result, "Min. received").value).toBe(min);
|
"Moved from the sender",
|
||||||
|
"Token moved",
|
||||||
|
"Moved to",
|
||||||
|
"Token Out",
|
||||||
|
"Min. received",
|
||||||
|
"Steps",
|
||||||
|
"Deadline",
|
||||||
|
]);
|
||||||
|
expect(detail(result, "Moved from the sender").value).toBe(
|
||||||
|
"Unknown (could not be decoded)",
|
||||||
|
);
|
||||||
|
expect(detail(result, "Min. received").value).toBe("1990.0000 USDC");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user