fix: the swap decoder reads a V2 already-paid zero and a zero balance check as the router does (closes #415)
A V2 exact-in amountIn of zero is the router's ALREADY_PAID marker: an earlier step sent the tokens to the pair and the swap spends all of them. Amount showed 0.0000 for it; it now reads "Whatever an earlier step sent to the pair (V2 already paid)", in the style of the V4 open delta line. A BALANCE_CHECK_ERC20 passes whenever the balance is at least minBalance, so a zero one guarantees nothing. It now sets the output side only when that side holds no minimum at the point the check is reached; a nonzero one sets the output side as before. README's Display Consistency text and TODO.md are updated to match. Model: opus-5-5
This commit was merged in pull request #440.
This commit is contained in:
+41
-14
@@ -84,17 +84,31 @@ function present(value) {
|
||||
//
|
||||
// `amountOutMinimum` gets no such mapping: V4Router compares it directly
|
||||
// (`if (amountOut < params.amountOutMinimum) revert V4TooLittleReceived`), so
|
||||
// a zero minimum is a literal zero slippage floor and is stated as one. Nor do
|
||||
// the V2/V3 paths have it — universal-router's `V3SwapRouter.v3SwapExactInput`
|
||||
// a zero minimum is a literal zero slippage floor and is stated as one. Nor
|
||||
// does the V3 path have it — universal-router's `V3SwapRouter.v3SwapExactInput`
|
||||
// special-cases only `ActionConstants.CONTRACT_BALANCE` (1<<255), never zero —
|
||||
// so a zero `amountIn` there is a literal zero and is displayed as one.
|
||||
// so a zero V3 `amountIn` is a literal zero and is displayed as one. The V2
|
||||
// exact-in path gives zero a meaning of its own: see ALREADY_PAID.
|
||||
const OPEN_DELTA = Symbol("v4-open-delta");
|
||||
|
||||
// The two amount lines that state a fact instead of a quantity. Same register
|
||||
// as UNNAMED_CURRENCY — a sentence in the value slot, so it cannot be misread
|
||||
// as a number — and deliberately not a third phrasing of "not named": these
|
||||
// say different things.
|
||||
// The Universal Router's V2 exact-in spells "the pair already holds the input
|
||||
// tokens" as an amount of zero: universal-router
|
||||
// `contracts/libraries/Constants.sol` declares
|
||||
// `uint256 internal constant ALREADY_PAID = 0` ("Used for identifying cases
|
||||
// when a v2 pair has already received input tokens"), and
|
||||
// `V2SwapRouter.v2SwapExactInput` makes no payment of its own when `amountIn`
|
||||
// equals it. The swap then spends whatever an earlier step sent to the pair.
|
||||
// As with OPEN_DELTA, the calldata states no quantity, and "0.0000" would say
|
||||
// that nothing is swapped.
|
||||
const ALREADY_PAID = Symbol("v2-already-paid");
|
||||
|
||||
// The amount lines that state a fact instead of a quantity. Same register as
|
||||
// UNNAMED_CURRENCY — a sentence in the value slot, so it cannot be misread as a
|
||||
// number — and deliberately not another phrasing of "not named": these say
|
||||
// different things.
|
||||
const OPEN_DELTA_AMOUNT = "All available (V4 open delta)";
|
||||
const ALREADY_PAID_AMOUNT =
|
||||
"Whatever an earlier step sent to the pair (V2 already paid)";
|
||||
const NO_MINIMUM = "None (no minimum guaranteed)";
|
||||
|
||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||
@@ -185,6 +199,7 @@ function decodeBalanceCheck(input) {
|
||||
// Decode V2_SWAP_EXACT_IN (command 0x08) input bytes.
|
||||
// ABI: (address recipient, uint256 amountIn, uint256 amountOutMin,
|
||||
// address[] path, bool payerIsUser)
|
||||
// A zero `amountIn` is read the way the router reads it, as ALREADY_PAID.
|
||||
function decodeV2SwapExactIn(input) {
|
||||
try {
|
||||
const d = coder.decode(
|
||||
@@ -192,7 +207,7 @@ function decodeV2SwapExactIn(input) {
|
||||
input,
|
||||
);
|
||||
return {
|
||||
amountIn: d[1],
|
||||
amountIn: d[1] === 0n ? ALREADY_PAID : d[1],
|
||||
amountOutMin: d[2],
|
||||
tokenIn: d[3][0],
|
||||
tokenOut: d[3][d[3].length - 1],
|
||||
@@ -502,7 +517,13 @@ function decode(data, toAddress, sources) {
|
||||
|
||||
if (cmdId === 0x0e) {
|
||||
const b = decodeBalanceCheck(inputs[i]);
|
||||
if (b) setOutput(b.token, b.minBalance);
|
||||
// The router passes this check whenever the owner holds at
|
||||
// least minBalance, so a zero one guarantees nothing and
|
||||
// does not replace a minimum an earlier step stated. Any
|
||||
// other minBalance sets the output side as a swap does.
|
||||
if (b && !(b.minBalance === 0n && present(minOutput))) {
|
||||
setOutput(b.token, b.minBalance);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdId === 0x00) {
|
||||
@@ -623,14 +644,20 @@ function decode(data, toAddress, sources) {
|
||||
}
|
||||
|
||||
if (present(inputAmount)) {
|
||||
// Two amounts need no scale to describe and are named rather than
|
||||
// formatted: V4's open delta, which is not a quantity at all (see
|
||||
// OPEN_DELTA), and an unbounded permit. The open-delta test comes
|
||||
// first — the sentinel is not a bigint and cannot be compared with
|
||||
// one.
|
||||
// Three amounts need no scale to describe and are named rather
|
||||
// than formatted: V4's open delta and 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 tests come
|
||||
// first — the sentinels are not bigints and cannot be compared
|
||||
// with one.
|
||||
let amount;
|
||||
if (inputAmount === OPEN_DELTA) {
|
||||
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||
} else if (inputAmount === ALREADY_PAID) {
|
||||
amount = {
|
||||
raw: ALREADY_PAID_AMOUNT,
|
||||
display: ALREADY_PAID_AMOUNT,
|
||||
};
|
||||
} else if (inputAmount >= MAX_UINT160) {
|
||||
amount = { raw: "Unlimited", display: "Unlimited" };
|
||||
} else if (hasV2ExactOut) {
|
||||
|
||||
Reference in New Issue
Block a user