fix: a V4 step of chained swap actions shows what the user receives (closes #563)
A V4 step's input side is what SETTLE or SETTLE_ALL names, else the one currency its swap actions spend and never buy; its output side is what TAKE or TAKE_ALL names, else the one they buy and never spend. Amount sums the amountIn of the actions spending the input side, shown only when each states one; Min. received likewise. An unread swap action leaves the sides to SETTLE, SETTLE_ALL, TAKE and TAKE_ALL, with no figure. A step with a swap action always sets the output side, sets the input side only if no earlier step did, and leaves a side its rules cannot name unknown, with no figure. SWAP_EXACT_OUT spends its first path entry. Model: opus-5-5
This commit was merged in pull request #566.
This commit is contained in:
+222
-131
@@ -290,7 +290,9 @@ const V4_SWAP_EXACT_IN = 0x07;
|
||||
const V4_SWAP_EXACT_OUT_SINGLE = 0x08;
|
||||
const V4_SWAP_EXACT_OUT = 0x09;
|
||||
const V4_SETTLE = 0x0b;
|
||||
const V4_SETTLE_ALL = 0x0c;
|
||||
const V4_TAKE = 0x0e;
|
||||
const V4_TAKE_ALL = 0x0f;
|
||||
|
||||
// A V4 exact-in `amountIn`, read the way V4Router reads it: zero is
|
||||
// ActionConstants.OPEN_DELTA, not a quantity of zero. See the OPEN_DELTA
|
||||
@@ -318,10 +320,133 @@ function decodeExactly(types, data) {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
// Decode one V4 swap action's parameters into the currency it spends
|
||||
// (`tokenIn`), the currency it buys (`tokenOut`) and, for an exact-in action,
|
||||
// its `amountIn` and `amountOutMin`. The exact-out actions decode no amounts.
|
||||
// A field the action does not state is null. Throws when the parameters are
|
||||
// not in the deployed shape (see decodeExactly()).
|
||||
function decodeV4SwapAction(actionId, param) {
|
||||
if (actionId === V4_SWAP_EXACT_IN) {
|
||||
// ExactInputParams: (address currencyIn,
|
||||
// tuple(address,uint24,int24,address,bytes)[] path,
|
||||
// uint128 amountIn, uint128 amountOutMin)
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
param,
|
||||
);
|
||||
const path = s[0][1];
|
||||
return {
|
||||
tokenIn: s[0][0],
|
||||
tokenOut: path.length > 0 ? path[path.length - 1][0] : null,
|
||||
amountIn: v4ExactInAmount(s[0][2]),
|
||||
amountOutMin: s[0][3],
|
||||
};
|
||||
}
|
||||
if (actionId === V4_SWAP_EXACT_IN_SINGLE) {
|
||||
// ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
|
||||
// bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
param,
|
||||
);
|
||||
const poolKey = s[0][0];
|
||||
const zeroForOne = s[0][1];
|
||||
return {
|
||||
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
|
||||
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
|
||||
amountIn: v4ExactInAmount(s[0][2]),
|
||||
amountOutMin: s[0][3],
|
||||
};
|
||||
}
|
||||
if (actionId === V4_SWAP_EXACT_OUT) {
|
||||
// ExactOutputParams: (address currencyOut,
|
||||
// tuple(address,uint24,int24,address,bytes)[] path,
|
||||
// uint128 amountOut, uint128 amountInMaximum)
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
param,
|
||||
);
|
||||
// The router works through the path from its last entry back to its
|
||||
// first, starting from currencyOut, so the first entry is what the
|
||||
// action spends.
|
||||
const path = s[0][1];
|
||||
return {
|
||||
tokenIn: path.length > 0 ? path[0][0] : null,
|
||||
tokenOut: s[0][0],
|
||||
amountIn: null,
|
||||
amountOutMin: null,
|
||||
};
|
||||
}
|
||||
// V4_SWAP_EXACT_OUT_SINGLE. ExactOutputSingleParams: (poolKey as above,
|
||||
// bool zeroForOne, uint128 amountOut, uint128 amountInMaximum,
|
||||
// bytes hookData)
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
param,
|
||||
);
|
||||
const poolKey = s[0][0];
|
||||
const zeroForOne = s[0][1];
|
||||
return {
|
||||
tokenIn: zeroForOne ? poolKey[0] : poolKey[1],
|
||||
tokenOut: zeroForOne ? poolKey[1] : poolKey[0],
|
||||
amountIn: null,
|
||||
amountOutMin: null,
|
||||
};
|
||||
}
|
||||
|
||||
// The currencies in `list` that `other` does not hold, each listed once.
|
||||
function currenciesNotIn(list, other) {
|
||||
return [...new Set(list.filter((c) => !other.includes(c)))];
|
||||
}
|
||||
|
||||
// The amount that the swap actions on one side of a V4 step state together:
|
||||
// the one action's own amount, or the sum when there are several and each
|
||||
// states a quantity. Exact-out actions state none here, and the open delta is
|
||||
// not a quantity. Otherwise null.
|
||||
function totalAmount(amounts) {
|
||||
if (amounts.length === 1) return amounts[0];
|
||||
if (amounts.length === 0) return null;
|
||||
if (!amounts.every((a) => typeof a === "bigint")) return null;
|
||||
return amounts.reduce((sum, a) => sum + a, 0n);
|
||||
}
|
||||
|
||||
// Decode V4_SWAP (command 0x10) input bytes.
|
||||
// The input is ABI-encoded as (bytes actions, bytes[] params).
|
||||
// We extract token addresses from SETTLE (input) and TAKE (output) sub-actions,
|
||||
// and swap amounts from the swap sub-actions.
|
||||
// The input is ABI-encoded as (bytes actions, bytes[] params). Each swap
|
||||
// action gives the currency it spends and the currency it buys (see
|
||||
// decodeV4SwapAction()). The step's sides then follow three rules:
|
||||
//
|
||||
// 1. Token In is the currency SETTLE or SETTLE_ALL names; otherwise the one
|
||||
// currency that some swap action spends and none buys. Token Out is the
|
||||
// currency TAKE or TAKE_ALL names; otherwise the one currency that some
|
||||
// swap action buys and none spends. If there is not exactly one such
|
||||
// currency, that side is unknown.
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// A currency a swap action does not name, such as what an exact-in action
|
||||
// with an empty path buys, is null and counts in rules 1 and 2 as a currency
|
||||
// like any other: a step of only that action has an unknown Token Out and
|
||||
// the action's amountOutMin as Min. received.
|
||||
//
|
||||
// `hasSwapAction` is true when the step has a swap action, read or not.
|
||||
// decode() then always sets the output side from this step, and sets the
|
||||
// input side from it only when no earlier step set it. A side it sets that
|
||||
// these rules leave unknown is set unknown, with no figure. A step with no
|
||||
// swap action sets only the sides SETTLE, SETTLE_ALL, TAKE and TAKE_ALL
|
||||
// name.
|
||||
function decodeV4Swap(input) {
|
||||
try {
|
||||
const d = coder.decode(["bytes", "bytes[]"], input);
|
||||
@@ -330,11 +455,12 @@ function decodeV4Swap(input) {
|
||||
|
||||
let settleToken = null;
|
||||
let takeToken = null;
|
||||
let amountIn = null;
|
||||
let amountOutMin = null;
|
||||
// The step's swap actions that could be read, as decodeV4SwapAction()
|
||||
// reads them.
|
||||
const swaps = [];
|
||||
// Set when a swap action's parameters are not in the deployed shape
|
||||
// (see decodeExactly()), so nothing was read from that action. SETTLE
|
||||
// and TAKE may still name both currencies.
|
||||
// (see decodeExactly()), so nothing was read from that action. SETTLE,
|
||||
// SETTLE_ALL, TAKE and TAKE_ALL may still name both currencies.
|
||||
let swapUndecoded = false;
|
||||
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
@@ -347,6 +473,10 @@ function decodeV4Swap(input) {
|
||||
params[i],
|
||||
);
|
||||
settleToken = s[0];
|
||||
} else if (actionId === V4_SETTLE_ALL) {
|
||||
// SETTLE_ALL: (address currency, uint256 maxAmount)
|
||||
const s = coder.decode(["address", "uint256"], params[i]);
|
||||
settleToken = s[0];
|
||||
} else if (actionId === V4_TAKE) {
|
||||
// TAKE: (address currency, address recipient, uint256 amount)
|
||||
const t = coder.decode(
|
||||
@@ -354,107 +484,20 @@ function decodeV4Swap(input) {
|
||||
params[i],
|
||||
);
|
||||
takeToken = t[0];
|
||||
} else if (actionId === V4_TAKE_ALL) {
|
||||
// TAKE_ALL: (address currency, uint256 minAmount)
|
||||
const t = coder.decode(["address", "uint256"], params[i]);
|
||||
takeToken = t[0];
|
||||
} else if (
|
||||
actionId === V4_SWAP_EXACT_IN_SINGLE ||
|
||||
actionId === V4_SWAP_EXACT_IN ||
|
||||
actionId === V4_SWAP_EXACT_IN_SINGLE
|
||||
actionId === V4_SWAP_EXACT_OUT_SINGLE ||
|
||||
actionId === V4_SWAP_EXACT_OUT
|
||||
) {
|
||||
// Extract amounts from exact-in swap actions
|
||||
if (actionId === V4_SWAP_EXACT_IN) {
|
||||
// ExactInputParams: (address currencyIn,
|
||||
// tuple(address,uint24,int24,address,bytes)[] path,
|
||||
// uint128 amountIn, uint128 amountOutMin)
|
||||
try {
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
params[i],
|
||||
);
|
||||
if (!present(settleToken)) settleToken = s[0][0];
|
||||
const path = s[0][1];
|
||||
if (path.length > 0 && !present(takeToken)) {
|
||||
takeToken = path[path.length - 1][0];
|
||||
}
|
||||
if (!present(amountIn)) {
|
||||
amountIn = v4ExactInAmount(s[0][2]);
|
||||
}
|
||||
if (!present(amountOutMin)) {
|
||||
amountOutMin = s[0][3];
|
||||
}
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
}
|
||||
} else {
|
||||
// ExactInputSingleParams: (tuple(address,address,uint24,int24,address) poolKey,
|
||||
// bool zeroForOne, uint128 amountIn, uint128 amountOutMin, bytes hookData)
|
||||
try {
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
params[i],
|
||||
);
|
||||
const poolKey = s[0][0];
|
||||
const zeroForOne = s[0][1];
|
||||
if (!present(settleToken))
|
||||
settleToken = zeroForOne
|
||||
? poolKey[0]
|
||||
: poolKey[1];
|
||||
if (!present(takeToken))
|
||||
takeToken = zeroForOne
|
||||
? poolKey[1]
|
||||
: poolKey[0];
|
||||
if (!present(amountIn)) {
|
||||
amountIn = v4ExactInAmount(s[0][2]);
|
||||
}
|
||||
if (!present(amountOutMin)) {
|
||||
amountOutMin = s[0][3];
|
||||
}
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
actionId === V4_SWAP_EXACT_OUT ||
|
||||
actionId === V4_SWAP_EXACT_OUT_SINGLE
|
||||
) {
|
||||
if (actionId === V4_SWAP_EXACT_OUT) {
|
||||
try {
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(address,tuple(address,uint24,int24,address,bytes)[],uint128,uint128)",
|
||||
],
|
||||
params[i],
|
||||
);
|
||||
if (!present(takeToken)) takeToken = s[0][0];
|
||||
const path = s[0][1];
|
||||
if (path.length > 0 && !present(settleToken)) {
|
||||
settleToken = path[path.length - 1][0];
|
||||
}
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const s = decodeExactly(
|
||||
[
|
||||
"tuple(tuple(address,address,uint24,int24,address),bool,uint128,uint128,bytes)",
|
||||
],
|
||||
params[i],
|
||||
);
|
||||
const poolKey = s[0][0];
|
||||
const zeroForOne = s[0][1];
|
||||
if (!present(settleToken))
|
||||
settleToken = zeroForOne
|
||||
? poolKey[0]
|
||||
: poolKey[1];
|
||||
if (!present(takeToken))
|
||||
takeToken = zeroForOne
|
||||
? poolKey[1]
|
||||
: poolKey[0];
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
}
|
||||
try {
|
||||
swaps.push(decodeV4SwapAction(actionId, params[i]));
|
||||
} catch {
|
||||
swapUndecoded = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -462,12 +505,54 @@ function decodeV4Swap(input) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 3.
|
||||
if (swapUndecoded) {
|
||||
return {
|
||||
tokenIn: settleToken,
|
||||
tokenOut: takeToken,
|
||||
amountIn: null,
|
||||
amountOutMin: null,
|
||||
swapUndecoded,
|
||||
hasSwapAction: true,
|
||||
};
|
||||
}
|
||||
|
||||
const spent = swaps.map((s) => s.tokenIn);
|
||||
const bought = swaps.map((s) => s.tokenOut);
|
||||
|
||||
// Rules 1 and 2 for Token In.
|
||||
let tokenIn = null;
|
||||
let amountIn = null;
|
||||
const spentOnly = currenciesNotIn(spent, bought);
|
||||
if (present(settleToken) || spentOnly.length === 1) {
|
||||
tokenIn = present(settleToken) ? settleToken : spentOnly[0];
|
||||
amountIn = totalAmount(
|
||||
swaps
|
||||
.filter((s) => s.tokenIn === tokenIn)
|
||||
.map((s) => s.amountIn),
|
||||
);
|
||||
}
|
||||
|
||||
// Rules 1 and 2 for Token Out.
|
||||
let tokenOut = null;
|
||||
let amountOutMin = null;
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
tokenIn: settleToken,
|
||||
tokenOut: takeToken,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
amountOutMin,
|
||||
swapUndecoded,
|
||||
hasSwapAction: swaps.length > 0,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -491,6 +576,7 @@ function decode(data, toAddress, sources) {
|
||||
let inputEstablished = false;
|
||||
let outputToken = null;
|
||||
let minOutput = null;
|
||||
let outputEstablished = false;
|
||||
let hasUnwrapWeth = false;
|
||||
let hasV2ExactOut = false;
|
||||
let swapUndecoded = false;
|
||||
@@ -510,8 +596,11 @@ function decode(data, toAddress, sources) {
|
||||
//
|
||||
// The input side is fixed by the first hop that states either half, the
|
||||
// output side by the last, because the final leg is what the user
|
||||
// receives. A half the establishing hop did not state stays null and
|
||||
// the line says so, rather than being filled in from a different hop.
|
||||
// receives. A V4 step with a swap action sets a side even when it
|
||||
// states neither half: the output side always, and the input side
|
||||
// when no earlier hop set it. A half the establishing hop did not
|
||||
// state stays null and the line says so, rather than being filled in
|
||||
// from a different hop.
|
||||
const setInput = (token, amount) => {
|
||||
inputToken = present(token) ? token : null;
|
||||
inputAmount = present(amount) ? amount : null;
|
||||
@@ -526,6 +615,7 @@ function decode(data, toAddress, sources) {
|
||||
if (!present(token) && !present(amount)) return;
|
||||
outputToken = present(token) ? token : null;
|
||||
minOutput = present(amount) ? amount : null;
|
||||
outputEstablished = true;
|
||||
};
|
||||
|
||||
for (let i = 0; i < commandsBytes.length; i++) {
|
||||
@@ -597,30 +687,33 @@ function decode(data, toAddress, sources) {
|
||||
if (cmdId === 0x10) {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
if (v4 && v4.swapUndecoded) {
|
||||
// The unread swap action named this step's currencies
|
||||
// and amounts, so the step is marked and still sets
|
||||
// both sides as a swap step does. A side its SETTLE or
|
||||
// TAKE did not name is set undetermined, not left to
|
||||
// an earlier or later step's token and figure.
|
||||
// A swap action of this step went unread, so the step
|
||||
// is marked.
|
||||
swapUndecoded = true;
|
||||
commandNames[i] += " (could not be decoded)";
|
||||
}
|
||||
if (v4 && v4.hasSwapAction) {
|
||||
// A step with a swap action always sets the output
|
||||
// side, as the last swap step wins, and sets the
|
||||
// input side only when no earlier step set it. Each
|
||||
// side it sets is set as a pair, even when the step
|
||||
// states neither half, so a side decodeV4Swap() left
|
||||
// unknown is set unknown with no figure. A token
|
||||
// stated without a figure, or a figure without a
|
||||
// token, likewise never sits beside an earlier step's
|
||||
// other half.
|
||||
if (!inputEstablished) {
|
||||
setInput(v4.tokenIn, v4.amountIn);
|
||||
}
|
||||
outputToken = v4.tokenOut;
|
||||
minOutput = v4.amountOutMin;
|
||||
outputEstablished = true;
|
||||
} 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, v4.amountIn);
|
||||
// Always update output: last swap step wins. A step
|
||||
// that carries the Min. received figure but decoded no
|
||||
// output currency makes the output *undetermined* — it
|
||||
// is neither ETH nor whatever an earlier step named,
|
||||
// and that figure is no longer counted in that token.
|
||||
// Equally, a step that names an output currency but no
|
||||
// minimum leaves Min. received unstated rather than
|
||||
// keeping an earlier step's figure beside the new
|
||||
// token. setOutput() is both rules; a step that states
|
||||
// neither half leaves the output alone.
|
||||
setOutput(v4.tokenOut, v4.amountOutMin);
|
||||
}
|
||||
}
|
||||
@@ -634,21 +727,19 @@ function decode(data, toAddress, sources) {
|
||||
}
|
||||
|
||||
// Resolve token info. A null token on either side means no step named
|
||||
// a currency for it, or a V4 step whose swap action went unread left
|
||||
// it undetermined; tokenInfo() refuses rather than calling it ETH.
|
||||
// a currency for it, or a V4 step with a swap action left it
|
||||
// unknown; 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 step
|
||||
// whose swap action went unread did set it, to undetermined. Any other
|
||||
// output side is WETH, or when no step set the output side. A V4 step
|
||||
// with a swap action 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());
|
||||
const outputUnset =
|
||||
!present(outputToken) && !present(minOutput) && !swapUndecoded;
|
||||
const inInfo = tokenInfo(inputToken, sources);
|
||||
const outInfo =
|
||||
hasUnwrapWeth && (outputIsWeth || outputUnset)
|
||||
hasUnwrapWeth && (outputIsWeth || !outputEstablished)
|
||||
? { symbol: "ETH", decimals: 18, address: null }
|
||||
: tokenInfo(outputToken, sources);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user