From f1e602d4142cfdf7723afaf789b857d686a02aea Mon Sep 17 00:00:00 2001 From: tim Date: Wed, 10 Dec 2025 15:53:45 -0400 Subject: [PATCH] Liquidity Party adapter --- evm/src/interfaces/ISwapAdapterTypes.sol | 15 +- evm/src/libraries/FractionMath.sol | 261 +++++++++++++++- evm/src/liquidityparty/Funding.sol | 18 ++ evm/src/liquidityparty/IPartyInfo.sol | 24 ++ evm/src/liquidityparty/IPartyPlanner.sol | 18 ++ evm/src/liquidityparty/IPartyPool.sol | 86 +++++ .../LiquidityPartySwapAdapter.sol | 258 +++++++++++++++ evm/src/liquidityparty/manifest.yaml | 25 ++ evm/test/AdapterTest.sol | 295 ++++++++++++++---- evm/test/LiquidityPartySwapAdapter.t.sol | 224 +++++++++++++ substreams/ethereum-liquidityparty/Makefile | 11 +- ...ol_viewer.abi.json => party_info.abi.json} | 70 ++--- .../abi/party_planner.abi.json | 196 +++++++++++- .../abi/party_pool.abi.json | 243 ++++++++------- .../ethereum-liquidityparty/bin/generate_abi | 11 + ...arty.yaml => ethereum-liquidityparty.yaml} | 13 +- .../integration_test.tycho.yaml | 95 ++---- .../src/abi/.gitignore | 2 +- .../ethereum-liquidityparty/src/modules.rs | 9 + .../ethereum-liquidityparty/src/params.rs | 13 +- .../src/pool_factories.rs | 23 +- 21 files changed, 1589 insertions(+), 321 deletions(-) create mode 100644 evm/src/liquidityparty/Funding.sol create mode 100644 evm/src/liquidityparty/IPartyInfo.sol create mode 100644 evm/src/liquidityparty/IPartyPlanner.sol create mode 100644 evm/src/liquidityparty/IPartyPool.sol create mode 100644 evm/src/liquidityparty/LiquidityPartySwapAdapter.sol create mode 100644 evm/src/liquidityparty/manifest.yaml create mode 100644 evm/test/LiquidityPartySwapAdapter.t.sol rename substreams/ethereum-liquidityparty/abi/{party_pool_viewer.abi.json => party_info.abi.json} (93%) create mode 100755 substreams/ethereum-liquidityparty/bin/generate_abi rename substreams/ethereum-liquidityparty/{sepolia-liquidityparty.yaml => ethereum-liquidityparty.yaml} (73%) diff --git a/evm/src/interfaces/ISwapAdapterTypes.sol b/evm/src/interfaces/ISwapAdapterTypes.sol index 323ac0a..41abbc0 100644 --- a/evm/src/interfaces/ISwapAdapterTypes.sol +++ b/evm/src/interfaces/ISwapAdapterTypes.sol @@ -40,7 +40,6 @@ interface ISwapAdapterTypes { } /// @dev Representation used for rational numbers such as prices. - // TODO: Use only uint128 for numerator and denominator. struct Fraction { uint256 numerator; uint256 denominator; @@ -61,6 +60,20 @@ interface ISwapAdapterTypes { /// available for unexpected reason. E.g. it was paused due to a bug. error Unavailable(string reason); + /// @dev The InvalidOrder error is thrown when the input to a swap is + /// not valid: e.g. if the limit price is negative, or below the + /// current price; the request amount is 0; the requested swap tokens + /// are not part of the pool; etc. + error InvalidOrder(string reason); + + /// @dev The TooSmall error is thrown when the requested trade amount + /// is too small, causing either zero output or a numerical imprecision + /// problem. If lowerLimit is not zero, then it specifies the minimum + /// trade size required. If lowerLimit is zero, then the lower bound + /// cannot be easily computed, in which case solvers can binary search + /// for a precise lower bound. + error TooSmall(uint256 lowerLimit); + /// @dev The LimitExceeded error is thrown when a limit has been exceeded. /// E.g. the specified amount can't be traded safely. error LimitExceeded(uint256 limit); diff --git a/evm/src/libraries/FractionMath.sol b/evm/src/libraries/FractionMath.sol index 43f2ef7..061b3d3 100644 --- a/evm/src/libraries/FractionMath.sol +++ b/evm/src/libraries/FractionMath.sol @@ -13,14 +13,267 @@ library FractionMath { ISwapAdapterTypes.Fraction memory frac1, ISwapAdapterTypes.Fraction memory frac2 ) internal pure returns (int8) { - uint256 crossProduct1 = frac1.numerator * frac2.denominator; - uint256 crossProduct2 = frac2.numerator * frac1.denominator; + uint256 fixed1 = toQ128x128(frac1.numerator, frac1.denominator); + uint256 fixed2 = toQ128x128(frac2.numerator, frac2.denominator); // fractions are equal - if (crossProduct1 == crossProduct2) return 0; + if (fixed1 == fixed2) return 0; // frac1 is greater than frac2 - else if (crossProduct1 > crossProduct2) return 1; + else if (fixed1 > fixed2) return 1; // frac1 is less than frac2 else return -1; } + + /// @notice Converts a Fraction into unsigned Q128.128 fixed point + function toQ128x128(ISwapAdapterTypes.Fraction memory rational) + internal + pure + returns (uint256 result) + { + return toQ128x128(rational.numerator, rational.denominator); + } + + /// @notice Converts an unsigned rational `numerator / denominator` + /// into Q128.128 (unsigned 128.128 fixed point), + /// rounding toward zero (floor for positive inputs). + /// + /// see https://github.com/Liquidity-Party/toQ128x128 + /// + /// @dev Reverts if: + /// - `denominator == 0`, or + /// - the exact result >= 2^256 (i.e. overflow of uint256). + /// + /// This computes floor(numerator * 2^128 / denominator) + /// using a full 512-bit intermediate to avoid precision loss. + /// + function toQ128x128(uint256 numerator, uint256 denominator) + internal + pure + returns (uint256 result) + { + require(denominator != 0, "toQ128x128: div by zero"); + + // We want (numerator * 2^128) / denominator using full precision, + // so we implement a 512-bit muldiv. + // + // Let: + // prod = numerator * 2^128 + // + // Since 2^128 is a power of two, the 512-bit product is easy: + // prod0 = (numerator << 128) mod 2^256 (low 256 bits) + // prod1 = (numerator >> 128) (high 256 bits) + // + // So prod = (prod1 * 2^256 + prod0). + uint256 prod0; + uint256 prod1; + unchecked { + prod0 = numerator << 128; + prod1 = numerator >> 128; + } + + // If the high 256 bits are zero, the product fits in 256 bits. + // This is the cheap path: just do a normal 256-bit division. + if (prod1 == 0) { + unchecked { + // denominator was already checked for 0. + return prod0 / denominator; + } + } + + // At this point prod1 > 0, so the 512-bit product does not fit in a + // uint256. We need a full-precision 512/256 division: + // + // result = floor((prod1 * 2^256 + prod0) / denominator) + // + // and we must ensure the final result fits in uint256. + + // Ensure result < 2^256. This is equivalent to requiring: + // denominator > prod1 + // because if denominator <= prod1, then: + // (prod1 * 2^256) / denominator >= 2^256. + require(denominator > prod1, "Q128x128: overflow"); + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // remainder = (prod1 * 2^256 + prod0) % denominator + // Since we can only directly mod 256-bit values, we first mod + // `prod0`, then adjust using the high word. + remainder := mulmod(numerator, shl(128, 1), denominator) + } + + // Now subtract `remainder` from the 512-bit product [prod1 prod0]. + assembly { + // Subtract remainder from the low part; if it underflows, borrow + // 1 from the high part. + let borrow := lt(prod0, remainder) + prod0 := sub(prod0, remainder) + prod1 := sub(prod1, borrow) + } + + // Factor powers of two out of denominator to simplify the division. + // + // Let denominator = d * 2^shift, with d odd. + // We can divide prod0 by 2^shift cheaply (bit shift), + // then do an exact division by the odd d using modular inverse. + uint256 twos; + unchecked { + // largest power of two divisor of denominator + twos = denominator & (~denominator + 1); + } + + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide the low word by twos. + prod0 := div(prod0, twos) + + // Adjust the high word so that the full 512-bit number is shifted + // by `twos`. + // twos = 2^k, so: + // combined = prod1 * 2^256 + prod0 + // combined / twos = + // prod1 * 2^256 / twos + prod0 / twos + // and 2^256 / twos = 2^(256-k). + // + // Here we compute: + // twos = 2^256 / twos + twos := add(div(sub(0, twos), twos), 1) + + // Now add the shifted high bits into prod0: + prod0 := or(prod0, mul(prod1, twos)) + } + + // At this point, denominator is odd and the 512-bit value + // has been squeezed into prod0 (prod1 is effectively 0). + + // Compute the modular inverse of denominator modulo 2^256. + // This uses Newton-Raphson iteration: + // + // inv ≡ denominator^{-1} (mod 2^256) + // + // Starting from a seed for odd denominator: + // All operations must be unchecked as they rely on modular arithmetic. + unchecked { + uint256 inv = (3 * denominator) ^ 2; + + // Perform Newton-Raphson iterations to refine the inverse. + // Starting from inv which is correct modulo 2^4, then each + // Newton-Raphson step doubles the number of correct bits: + // 2⁴ → 2⁸ → 2¹⁶ → 2³² → 2⁶⁴ → + // 2¹²⁸ → 2²⁵⁶ + // Requiring six iterations for 256-bit precision: + inv *= 2 - denominator * inv; + inv *= 2 - denominator * inv; + inv *= 2 - denominator * inv; + inv *= 2 - denominator * inv; + inv *= 2 - denominator * inv; + inv *= 2 - denominator * inv; + + // Now inv is the modular inverse of denominator mod 2^256. + // The exact division result is then: + // + // result = (prod0 * inv) mod 2^256 + // + // which is just ordinary 256-bit multiplication. + result = prod0 * inv; + } + } + + /// @notice Multiply a Fraction and a uint256 using full precision + function mul(ISwapAdapterTypes.Fraction memory rational, uint256 y) + internal + pure + returns (uint256 result) + { + return mulDiv(rational.numerator, y, rational.denominator); + } + + /// @notice Full-precision mulDiv: computes floor(x * y / denominator) + /// with 512-bit intermediate precision to avoid overflow. + /// + /// @dev Reverts if `denominator == 0` or the exact result >= 2^256. + /// The implementation mirrors the 512/256 division flow used by + /// `toQ128x128(uint256,uint256)`, but with a general multiplicand `y` + /// instead of the fixed 2^128 shift. + function mulDiv(uint256 x, uint256 y, uint256 denominator) + internal + pure + returns (uint256 result) + { + require(denominator != 0, "mulDiv: div by zero"); + + // Compute the 512-bit product [prod1 prod0] = x * y. + // mm = (x * y) mod (2^256 - 1) + // prod0 = (x * y) mod 2^256 + // prod1 = (x * y - prod0 - (mm < prod0 ? 1 : 0)) / 2^256 + uint256 prod0; + uint256 prod1; + assembly { + let mm := mulmod(x, y, not(0)) + prod0 := mul(x, y) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // If the high 256 bits are zero, we can do a simple 256-bit division. + if (prod1 == 0) { + unchecked { + return prod0 / denominator; + } + } + + // Ensure result < 2^256. This is equivalent to requiring + // denominator > prod1. + require(denominator > prod1, "mulDiv: overflow"); + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + remainder := mulmod(x, y, denominator) + // Subtract remainder from the low part; if it underflows, borrow 1 + // from the high part. + let borrow := lt(prod0, remainder) + prod0 := sub(prod0, remainder) + prod1 := sub(prod1, borrow) + } + + // Factor powers of two out of denominator to simplify the division. + uint256 twos; + unchecked { + // largest power of two divisor of denominator + twos = denominator & (~denominator + 1); + } + + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide the low word by twos. + prod0 := div(prod0, twos) + + // Compute twos = 2^256 / twos. + twos := add(div(sub(0, twos), twos), 1) + + // Shift bits from the high word into the low word. + prod0 := or(prod0, mul(prod1, twos)) + } + + // Compute modular inverse of the (now odd) denominator modulo 2^256 + // via Newton-Raphson iterations. + // `inv` is correct to four bits, so we require six iterations + // to achieve 256-bit precision. + unchecked { + uint256 inv = (3 * denominator) ^ 2; + inv *= 2 - denominator * inv; // 2^8 + inv *= 2 - denominator * inv; // 2^16 + inv *= 2 - denominator * inv; // 2^32 + inv *= 2 - denominator * inv; // 2^64 + inv *= 2 - denominator * inv; // 2^128 + inv *= 2 - denominator * inv; // 2^256 + + // Exact division: result = prod0 * inv mod 2^256 + result = prod0 * inv; + } + } } diff --git a/evm/src/liquidityparty/Funding.sol b/evm/src/liquidityparty/Funding.sol new file mode 100644 index 0000000..442464e --- /dev/null +++ b/evm/src/liquidityparty/Funding.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +library Funding { + /// @notice a constant passed to swap as the fundingSelector to indicate + /// that the payer has used regular ERC20 approvals to allow the pool to + /// move the necessary input tokens. + // Slither analysis of this line is literally wrong and broken. The extra zero digits are REQUIRED by Solidity since it is a bytes4 literal. + // slither-disable-next-line too-many-digits + bytes4 internal constant APPROVALS = 0x00000000; + + /// @notice a constant passed to swap as the fundingSelector to indicate + /// that the payer has already sent sufficient input tokens to the pool + /// before calling swap, so no movement of input tokens is required. + // Slither analysis of this line is literally wrong and broken. The extra zero digits are REQUIRED by Solidity since it is a bytes4 literal. + // slither-disable-next-line too-many-digits + bytes4 internal constant PREFUNDING = 0x00000001; +} diff --git a/evm/src/liquidityparty/IPartyInfo.sol b/evm/src/liquidityparty/IPartyInfo.sol new file mode 100644 index 0000000..8db9a0c --- /dev/null +++ b/evm/src/liquidityparty/IPartyInfo.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IPartyPool} from "./IPartyPool.sol"; + +interface IPartyInfo { + /// @notice returns true iff the pool is not killed and has been initialized + /// with liquidity. + function working(IPartyPool pool) external view returns (bool); + + /// @notice Infinitesimal out-per-in marginal price for swap base->quote as + /// Q128.128, not adjusted for token decimals. + /// @dev Returns p_base / p_quote in Q128.128 format, scaled to external + /// units by (denom_quote / denom_base). This aligns with the swap kernel so + /// that, fee-free, avg(out/in) ≤ price(base, quote) for exact-in trades. + /// @param baseTokenIndex index of the input (base) asset + /// @param quoteTokenIndex index of the output (quote) asset + /// @return price Q128.128 value equal to out-per-in (j per i) + function price( + IPartyPool pool, + uint256 baseTokenIndex, + uint256 quoteTokenIndex + ) external view returns (uint256); +} diff --git a/evm/src/liquidityparty/IPartyPlanner.sol b/evm/src/liquidityparty/IPartyPlanner.sol new file mode 100644 index 0000000..d33f248 --- /dev/null +++ b/evm/src/liquidityparty/IPartyPlanner.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {IPartyPool} from "./IPartyPool.sol"; + +/// @title IPartyPlanner +/// @notice Interface for factory contract for creating and tracking PartyPool +/// instances +interface IPartyPlanner { + /// @notice Retrieves a page of pool addresses + /// @param offset Starting index for pagination + /// @param limit Maximum number of items to return + /// @return pools Array of pool addresses for the requested page + function getAllPools(uint256 offset, uint256 limit) + external + view + returns (IPartyPool[] memory pools); +} diff --git a/evm/src/liquidityparty/IPartyPool.sol b/evm/src/liquidityparty/IPartyPool.sol new file mode 100644 index 0000000..60f6fe7 --- /dev/null +++ b/evm/src/liquidityparty/IPartyPool.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.8.27; + +/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token +/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing +/// model. The pool issues an ERC20 LP token representing proportional +/// ownership. +/// It supports: +/// - Proportional minting and burning of LP _tokens, +/// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap), +/// - Exact-input swaps and swaps-to-price-limits, +/// - Flash loans via a callback interface. +interface IPartyPool { + /// @notice If a security problem is found, the vault owner may call this + /// function to permanently disable swap and mint functionality, leaving + /// only burns (withdrawals) working. + function killed() external view returns (bool); + + /// @notice Returns the number of tokens (n) in the pool. + function numTokens() external view returns (uint256); + + /// @notice Returns the list of all token addresses in the pool (copy). + function allTokens() external view returns (address[] memory); + + /// @notice External view to quote exact-in swap amounts (gross input incl. + /// fee and output), matching swap() computations @param inputTokenIndex + /// index of input token + /// @param outputTokenIndex index of output token + /// @param maxAmountIn maximum gross input allowed (inclusive of fee) + /// @param limitPrice maximum acceptable marginal price (pass 0 to ignore) + /// @return amountIn gross input amount to transfer (includes fee), + /// amountOut output amount user would receive, inFee fee taken from input + /// amount + function swapAmounts( + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice + ) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee); + + /// @notice Swap input token inputTokenIndex -> token outputTokenIndex. + /// Payer must approve token inputTokenIndex. @param payer address of the + /// account that pays for the swap + /// @param fundingSelector If set to USE_APPROVALS, then the payer must use + /// regular ERC20 approvals to authorize the pool to move the required input + /// amount. If this fundingSelector is USE_PREFUNDING, then all of the input + /// amount is expected to have already been sent to the pool and no + /// additional transfers are needed. Refunds of excess input amount are NOT + /// provided and it is illegal to use this funding method with a limit + /// price. Otherwise, for any other fundingSelector value, a callback style + /// funding mechanism is used where the given selector is invoked on the + /// payer, passing the arguments of (address inputToken, uint256 + /// inputAmount). The callback function must send the given amount of input + /// coin to the pool in order to continue the swap transaction, otherwise + /// "Insufficient funds" is thrown. @param receiver address that will + /// receive the output tokens + /// @param inputTokenIndex index of input asset + /// @param outputTokenIndex index of output asset + /// @param maxAmountIn maximum amount of token inputTokenIndex (uint256) to + /// transfer in (inclusive of fees) @param limitPrice maximum acceptable + /// marginal price (64.64 fixed point). Pass 0 to ignore. + /// @param deadline timestamp after which the transaction will revert. Pass + /// 0 to ignore. @param unwrap If true, then any output of wrapper token + /// will be unwrapped and native ETH sent to the receiver. + /// @param cbData callback data if fundingSelector is of the callback type. + /// @return amountIn actual input used (uint256), amountOut actual output + /// sent (uint256), inFee fee taken from the input (uint256) + function swap( + address payer, + bytes4 fundingSelector, + address receiver, + uint256 inputTokenIndex, + uint256 outputTokenIndex, + uint256 maxAmountIn, + int128 limitPrice, + uint256 deadline, + bool unwrap, + bytes memory cbData + ) + external + payable + returns (uint256 amountIn, uint256 amountOut, uint256 inFee); + + /// @notice Effective combined fee in ppm for the given asset pair (i as + /// input, j as output). + function fee(uint256 i, uint256 j) external view returns (uint256); +} diff --git a/evm/src/liquidityparty/LiquidityPartySwapAdapter.sol b/evm/src/liquidityparty/LiquidityPartySwapAdapter.sol new file mode 100644 index 0000000..27a9b1d --- /dev/null +++ b/evm/src/liquidityparty/LiquidityPartySwapAdapter.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity ^0.8.27; + +import { + IERC20 +} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { + SafeERC20 +} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISwapAdapter} from "../interfaces/ISwapAdapter.sol"; +import {Funding} from "./Funding.sol"; +import {IPartyInfo} from "./IPartyInfo.sol"; +import {IPartyPlanner} from "./IPartyPlanner.sol"; +import {IPartyPool} from "./IPartyPool.sol"; +import {console2} from "../../lib/forge-std/src/console2.sol"; + +contract LiquidityPartySwapAdapter is ISwapAdapter { + using SafeERC20 for IERC20; + + // Forge lint wants immutables to be all caps. Slither wants them to be + // mixed case. Why do we care about pedantic linters? The Solidity style + // guide mentions "constants" but never "immutables." Faced with an + // irresolvable linter conflict, I chose to disable the slither linter, + // since its detection of immutables as constants seems to be broken. + // slither-disable-next-line naming-convention + IPartyPlanner public immutable PLANNER; + // slither-disable-next-line naming-convention + IPartyInfo public immutable INFO; + + constructor(IPartyPlanner planner, IPartyInfo info) { + PLANNER = planner; + INFO = info; + } + + function price( + bytes32 poolId, + address sellToken, + address buyToken, + uint256[] memory specifiedAmounts + ) external view override returns (Fraction[] memory prices) { + IPartyPool pool = _poolFromId(poolId); + (uint256 indexIn, uint256 indexOut) = + _tokenIndexes(pool, sellToken, buyToken); + prices = new Fraction[](specifiedAmounts.length); + for (uint256 i = 0; i < specifiedAmounts.length; i++) { + uint256 amount = specifiedAmounts[i]; + if (amount == 0) { + // Marginal price support + prices[i] = _marginalPrice(pool, indexIn, indexOut); + } else { + // Regular slippage calculation + // slither-disable-next-line unused-return calls-loop + ( + uint256 amountIn, + uint256 amountOut, /*uint256 inFee*/ + ) = pool.swapAmounts(indexIn, indexOut, amount, 0); + prices[i].numerator = amountOut; + prices[i].denominator = amountIn; + } + } + } + + function swap( + bytes32 poolId, + address sellToken, + address buyToken, + OrderSide, + /*side*/ + uint256 specifiedAmount + ) external returns (Trade memory trade) { + // Setup + address swapper = msg.sender; + IPartyPool pool = _poolFromId(poolId); + // This require should never trigger if the substreams module correctly + // removes components that were killed. + if (!INFO.working(pool)) { + revert Unavailable("LiqP pool not working"); + } + (uint256 indexIn, uint256 indexOut) = + _tokenIndexes(pool, sellToken, buyToken); + + // Transfer and Swap + uint256 startingGas = gasleft(); + IERC20(sellToken) + .safeTransferFrom(swapper, address(pool), specifiedAmount); + // slither-disable-next-line unused-return + try pool.swap( + address(0), + Funding.PREFUNDING, + swapper, + indexIn, + indexOut, + specifiedAmount, + 0, + 0, + false, + "" + ) returns ( + uint256 amountIn, uint256 amountOut, uint256 inFee + ) { + uint256 endingGas = gasleft(); + uint256 gasUsed = startingGas - endingGas; + Fraction memory poolPrice = _marginalPrice(pool, indexIn, indexOut); + console2.log("Successfully swapped", amountOut); + // forge-lint: disable-next-line(named-struct-fields) + return Trade(amountOut, gasUsed, poolPrice); + } catch (bytes memory reason) { + bytes32 hash = keccak256(reason); + if (hash == keccak256("swap: input too small after fee")) { + revert TooSmall(0); + } else if ( + hash == keccak256("swap: transfer exceeds max") + || hash + == keccak256("LMSR: a/b too large (would overflow exp)") + || hash == keccak256("swap: transfer exceeds max") + ) { + revert LimitExceeded(0); // todo size + } else if (hash == keccak256("killed")) { + revert Unavailable("pool has been permanently killed"); + } else if (hash == keccak256("LMSR: size metric zero")) { + revert Unavailable("pool currently has no LP assets"); + } else if (hash == keccak256("LMSR: limitPrice <= current price")) { + revert InvalidOrder("limit price is below current price"); + } else if ( + hash == keccak256("LMSR: ratio<=0") // invalid limit price + ) { + revert InvalidOrder("limit price cannot be negative"); + } else { + console2.log("Unhandled error", string(reason)); + // re-raise + assembly { + revert(add(reason, 0x20), mload(reason)) + } + } + } + } + + function getLimits(bytes32 poolId, address sellToken, address buyToken) + external + view + returns (uint256[] memory limits) + { + // We arbitrarily limit the amounts like Uniswap V2 does, to make the + // test cases work. There is no theoretical limit on the input amount. + // forge-lint: disable-next-line(unsafe-typecast) + address pool = address(bytes20(poolId)); + limits = new uint256[](2); + + // input token limit: Theoretically unlimited, but artificially limited + // here to appease Tycho's test cases. Instead of estimating actual + // input limits based on a maximum target slippage, we merely return the + // current + // inventory of input token. Even for large stablecoin pools with a + // kappa near 1, this input amount should result in an "unreasonably" + // high slippage: + // Pool Size => Slippage for inputAmount=reserveBalance and kappa=1 + // 2 => 33.7% + // 10 => 9.2% + // 25 => 3.9% + // 50 => 2.1% + // See the commented-out method below for an exact computation of the + // maximum input amount for a given pool configuration and target + // slippage. + limits[0] = IERC20(sellToken).balanceOf(pool); + + // output token limit: the pool's current balance (an overestimate) + limits[1] = IERC20(buyToken).balanceOf(pool); + } + + function getCapabilities( + bytes32, + /*poolId*/ + address, + /*sellToken*/ + address /*buyToken*/ + ) + external + pure + returns (Capability[] memory capabilities) + { + capabilities = new Capability[](3); + capabilities[0] = Capability.SellOrder; + capabilities[1] = Capability.PriceFunction; + capabilities[2] = Capability.MarginalPrice; + return capabilities; + } + + function getTokens(bytes32 poolId) + external + view + returns (address[] memory tokens) + { + IPartyPool pool = _poolFromId(poolId); + return pool.allTokens(); + } + + function getPoolIds(uint256 offset, uint256 limit) + external + view + returns (bytes32[] memory ids) + { + IPartyPool[] memory pools = PLANNER.getAllPools(offset, limit); + ids = new bytes32[](pools.length); + for (uint256 i = 0; i < pools.length; i++) { + ids[i] = bytes32(uint256(uint160(address(pools[i])))); + } + } + + // + // Internal Helpers + // + + uint256 private constant NONE = type(uint256).max; + + /// @dev Liquidity Party pools identify tokens by index rather than address, + /// saving 5200 gas per swap. + function _tokenIndexes(IPartyPool pool, address sellToken, address buyToken) + internal + view + returns (uint256 indexIn, uint256 indexOut) + { + indexIn = NONE; + indexOut = NONE; + address[] memory tokens = pool.allTokens(); + uint256 numTokens = pool.numTokens(); + for (uint256 i = 0; i < numTokens; i++) { + if (tokens[i] == sellToken) { + indexIn = i; + } else if (tokens[i] == buyToken) { + indexOut = i; + } + } + // This should never happen if the token metadata was correctly loaded + // by substreams + require(indexIn != NONE && indexOut != NONE, "tokens not in pool"); + } + + function _marginalPrice(IPartyPool pool, uint256 indexIn, uint256 indexOut) + internal + view + returns (Fraction memory poolPrice) + { + // Liquidity Party prices are Q128.128 fixed point format + // slither-disable-next-line calls-loop + uint256 price128x128 = INFO.price(pool, indexIn, indexOut); + uint256 feePpm = pool.fee(indexIn, indexOut); + // price128x128 *= 1_000_000 - feePpm; + // price128x128 /= 1_000_000; + // forge-lint: disable-next-line(unsafe-typecast,named-struct-fields) + return Fraction(price128x128, 1 << 128); + } + + function _poolFromId(bytes32 poolId) internal pure returns (IPartyPool) { + // forge-lint: disable-next-line(unsafe-typecast) + return IPartyPool(address(bytes20(poolId))); + } +} + diff --git a/evm/src/liquidityparty/manifest.yaml b/evm/src/liquidityparty/manifest.yaml new file mode 100644 index 0000000..ba0f67e --- /dev/null +++ b/evm/src/liquidityparty/manifest.yaml @@ -0,0 +1,25 @@ +author: + name: Tim Olson + email: tim@dexorder.com + +constants: + # This is our median gas cost for a 20-asset pool. Gas varies by pool size from 120k to 200k. + protocol_gas: 147238 + capabilities: + - SellSide + - PriceFunction + - MarginalPrice + +contract: LiquidityPartySwapAdapter.sol + +# Deployment instances used to generate chain specific bytecode. +instances: + - chain: + name: sepolia + id: 11155111 + arguments: + - "0x77C29B1790D18A3AD269BcE09b7dB1074911Dcb6" # PartyPlanner + - "0x784BA6cD19B484bEE9Cee880B18b57fC6e8b2D5c" # PartyInfo + +# We do implement getPoolIds() and getTokens(), so explicit swap tests are not needed. +tests: {} diff --git a/evm/test/AdapterTest.sol b/evm/test/AdapterTest.sol index 22e491e..cf3b32a 100644 --- a/evm/test/AdapterTest.sol +++ b/evm/test/AdapterTest.sol @@ -12,7 +12,6 @@ contract AdapterTest is Test, ISwapAdapterTypes { using FractionMath for Fraction; using EfficientERC20 for IERC20; - uint256 constant pricePrecision = 10e24; string[] public stringPctgs = ["0%", "0.1%", "50%", "100%"]; // @notice Test the behavior of a swap adapter for a list of pools @@ -44,9 +43,10 @@ contract AdapterTest is Test, ISwapAdapterTypes { } // Prices should: - // 1. Be monotonic decreasing + // 1. Be monotonic decreasing (within rounding tolerance) // 2. Be positive // 3. Always be >= the executed price and >= the price after the swap + // (within rounding tolerance) function testPricesForPair( ISwapAdapter adapter, bytes32 poolId, @@ -76,7 +76,10 @@ contract AdapterTest is Test, ISwapAdapterTypes { Fraction[] memory prices = adapter.price(poolId, tokenIn, tokenOut, amounts); assertGt( - fractionToInt(prices[0]), + fractionToInt(prices[0]) + // within rounding tolerance + * (amounts[amounts.length - 1] + 1) + / amounts[amounts.length - 1], fractionToInt(prices[prices.length - 1]), "Price at limit should be smaller than price at 0" ); @@ -92,7 +95,6 @@ contract AdapterTest is Test, ISwapAdapterTypes { uint256 priceAtZero = fractionToInt(prices[0]); console2.log("TEST: Price at 0: %d", priceAtZero); - Trade memory trade; deal(tokenIn, address(this), 5 * amounts[amounts.length - 1]); uint256 initialState = vm.snapshot(); @@ -104,50 +106,93 @@ contract AdapterTest is Test, ISwapAdapterTypes { amounts[j] ); uint256 priceAtAmount = fractionToInt(prices[j]); + // We allow the assertions to tolerate rounding errors + // not greater than `1/amounts[j]` + uint256 toleranceDenominator = amounts[j]; console2.log("TEST: Swapping %d of %s", amounts[j], tokenIn); - trade = adapter.swap( + try adapter.swap( poolId, tokenIn, tokenOut, OrderSide.Sell, amounts[j] - ); - uint256 executedPrice = - trade.calculatedAmount * pricePrecision / amounts[j]; - uint256 priceAfterSwap = fractionToInt(trade.price); - console2.log("TEST: - Executed price: %d", executedPrice); - console2.log("TEST: - Price at amount: %d", priceAtAmount); - console2.log("TEST: - Price after swap: %d", priceAfterSwap); + ) returns ( + Trade memory trade + ) { + uint256 executedPrice = Fraction( + trade.calculatedAmount, amounts[j] + ).toQ128x128(); + uint256 priceAfterSwap = fractionToInt(trade.price); + console2.log("TEST: - Executed price: %d", executedPrice); + console2.log("TEST: - Price at amount: %d", priceAtAmount); + console2.log("TEST: - Price after swap: %d", priceAfterSwap); - if (hasPriceImpact) { - assertGe( - executedPrice, - priceAtAmount, - "Price should be greated than executed price." - ); - assertGt( - executedPrice, - priceAfterSwap, - "Executed price should be greater than price after swap." - ); - assertGt( - priceAtZero, - executedPrice, - "Price should be greated than price after swap." - ); - } else { - assertGe( - priceAtZero, - priceAfterSwap, - "Executed price should be or equal to price after swap." - ); - assertGe( - priceAtZero, - priceAtAmount, - "Executed price should be or equal to price after swap." - ); - assertGe( - priceAtZero, - executedPrice, - "Price should be or equal to price after swap." - ); + if (hasPriceImpact) { + assertGeTol( + executedPrice, + priceAtAmount, + toleranceDenominator, + "Price should be greater than executed price." + ); + assertGtTol( + executedPrice, + priceAfterSwap, + toleranceDenominator, + "Executed price should be greater than price after swap." + ); + assertGtTol( + priceAtZero, + executedPrice, + toleranceDenominator, + "Price should be greater than price after swap." + ); + } else { + assertGeTol( + priceAtZero, + priceAfterSwap, + toleranceDenominator, + "Executed price should be or equal to price after swap." + ); + assertGeTol( + priceAtZero, + priceAtAmount, + toleranceDenominator, + "Executed price should be or equal to price after swap." + ); + assertGeTol( + priceAtZero, + executedPrice, + toleranceDenominator, + "Price should be or equal to price after swap." + ); + } + } catch (bytes memory reason) { + (bool isTooSmall, uint256 lowerLimit) = + decodeTooSmallError(reason); + (bool isLimitExceeded, uint256 limit) = + decodeLimitExceededError(reason); + + if (isTooSmall) { + // We allow a TooSmall exception to occur for the smallest + // amount only. + if (j == 1) { + console2.log( + "TEST: TooSmall exception tolerated for smallest amount" + ); + } else { + revert( + "TEST: TooSmall thrown for a significantly sized amount" + ); + } + } else if (isLimitExceeded) { + // We never allow LimitExceeded to be thrown, since all + // amounts should be within the stated limits. + revert( + "TEST: LimitExceeded thrown for an amount within limits" + ); + } else { + // any other revert reason bubbles up + assembly { + revert(add(reason, 32), mload(reason)) + } + } } vm.revertTo(initialState); @@ -185,24 +230,87 @@ contract AdapterTest is Test, ISwapAdapterTypes { ); uint256[] memory aboveLimitArray = new uint256[](1); aboveLimitArray[0] = amountAboveLimit; + bool supportsLimitExceeded = false; try adapter.price(poolId, tokenIn, tokenOut, aboveLimitArray) { revert( "Pool shouldn't be able to fetch prices above the sell limit" ); - } catch Error(string memory s) { - console2.log( - "TEST: Expected error when fetching price above limit: %s", s - ); + } catch (bytes memory reason) { + (bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason); + (bool isLimitExceeded, uint256 limit) = + decodeLimitExceededError(reason); + + if (isLimitExceeded) { + supportsLimitExceeded = true; + console2.log( + "TEST: LimitExceeded supported! Thrown when fetching price above limit: %i", + limit + ); + } else if (isTooSmall) { + console2.log( + "TEST: UNEXPECTED TooSmall error when fetching price below limit: %i", + lowerLimit + ); + revert TooSmall(lowerLimit); + } else if ( + reason.length >= 4 + && bytes4(reason) == bytes4(keccak256("Error(string)")) + ) { + string memory s = abi.decode( + sliceBytes(reason, 4, reason.length - 4), (string) + ); + console2.log( + "TEST: Expected error when fetching price above limit: %s", + s + ); + } else { + // Unexpected error type: re-raise. + assembly { + revert(add(reason, 32), mload(reason)) + } + } } try adapter.swap( poolId, tokenIn, tokenOut, OrderSide.Sell, aboveLimitArray[0] ) { revert("Pool shouldn't be able to swap above the sell limit"); - } catch Error(string memory s) { - console2.log( - "TEST: Expected error when swapping above limit: %s", s - ); + } catch (bytes memory reason) { + (bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason); + (bool isLimitExceeded, uint256 limit) = + decodeLimitExceededError(reason); + + if (isLimitExceeded) { + supportsLimitExceeded = true; + console2.log( + "TEST: LimitExceeded supported! Thrown when swapping above limit: %i", + limit + ); + } else if (isTooSmall) { + console2.log( + "TEST: UNEXPECTED TooSmall error when swapping above limit: %i", + lowerLimit + ); + revert TooSmall(lowerLimit); + } else if ( + reason.length >= 4 + && bytes4(reason) == bytes4(keccak256("Error(string)")) + ) { + string memory s = abi.decode( + sliceBytes(reason, 4, reason.length - 4), (string) + ); + console2.log( + "TEST: Expected error when swapping above limit: %s", s + ); + } else { + // Unexpected error type: re-raise. + assembly { + revert(add(reason, 32), mload(reason)) + } + } + } + if (supportsLimitExceeded) { + console.log(unicode"Adapter supports LimitExceeded ✓"); } } @@ -244,7 +352,7 @@ contract AdapterTest is Test, ISwapAdapterTypes { pure returns (uint256) { - return price.numerator * pricePrecision / price.denominator; + return price.toQ128x128(); } function hasCapability( @@ -259,4 +367,85 @@ contract AdapterTest is Test, ISwapAdapterTypes { return false; } + + // + // Custom Error Helper Functions + // TODO should we expose these in a better location / library for solvers to + // also leverage? + + // Helper function to check if error is TooSmall and decode it + function decodeTooSmallError(bytes memory reason) + internal + pure + returns (bool, uint256) + { + if (reason.length >= 4 && bytes4(reason) == TooSmall.selector) { + if (reason.length == 36) { + uint256 lowerLimit = + abi.decode(sliceBytes(reason, 4, 32), (uint256)); + return (true, lowerLimit); + } + } + return (false, 0); + } + + // Helper function to check if error is LimitExceeded and decode it + function decodeLimitExceededError(bytes memory reason) + internal + pure + returns (bool, uint256) + { + if (reason.length >= 4 && bytes4(reason) == LimitExceeded.selector) { + if (reason.length == 36) { + uint256 limit = abi.decode(sliceBytes(reason, 4, 32), (uint256)); + return (true, limit); + } + } + return (false, 0); + } + + // Helper function to slice bytes + function sliceBytes(bytes memory data, uint256 start, uint256 length) + internal + pure + returns (bytes memory) + { + bytes memory result = new bytes(length); + for (uint256 i = 0; i < length; i++) { + result[i] = data[start + i]; + } + return result; + } + + // + // Helper functions to assert with tolerance + // + + function assertGeTol( + uint256 a, + uint256 b, + uint256 toleranceDenominator, + string memory errorMessage + ) internal { + // The tolerance is `1 / toleranceDenominator`, so we increase the value + // of `a` by this amount. adjustedA = a * (denom+1) / denom + uint256 adjustedA = FractionMath.mulDiv( + a, toleranceDenominator + 1, toleranceDenominator + ); + assertGe(adjustedA, b, errorMessage); + } + + function assertGtTol( + uint256 a, + uint256 b, + uint256 toleranceDenominator, + string memory errorMessage + ) internal { + // The tolerance is `1 / toleranceDenominator`, so we increase the value + // of `a` by this amount. adjustedA = a * (denom+1) / denom + uint256 adjustedA = FractionMath.mulDiv( + a, toleranceDenominator + 1, toleranceDenominator + ); + assertGt(adjustedA, b, errorMessage); + } } diff --git a/evm/test/LiquidityPartySwapAdapter.t.sol b/evm/test/LiquidityPartySwapAdapter.t.sol new file mode 100644 index 0000000..da7b562 --- /dev/null +++ b/evm/test/LiquidityPartySwapAdapter.t.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import "forge-std/console2.sol"; // todo + +import { + IERC20 +} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import { + IERC20Metadata +} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {FractionMath} from "../src/libraries/FractionMath.sol"; +import {IPartyInfo} from "../src/liquidityparty/IPartyInfo.sol"; +import {IPartyPlanner} from "../src/liquidityparty/IPartyPlanner.sol"; +import {IPartyPool} from "../src/liquidityparty/IPartyPool.sol"; +import { + LiquidityPartySwapAdapter +} from "../src/liquidityparty/LiquidityPartySwapAdapter.sol"; +import {AdapterTest} from "./AdapterTest.sol"; + +contract LiquidityPartyFunctionTest is AdapterTest { + using FractionMath for Fraction; + + IPartyPlanner internal constant PLANNER = + IPartyPlanner(0x42977f565971F6D288a05ddEbC87A17276F71A29); + IPartyInfo internal constant INFO = + IPartyInfo(0x605F803cD27F5c1fa01440B2cbd5D3E4Cf7EE850); + address internal constant MINT_IMPL = + 0xA0375403921e9B357E1BeD57bef3fA3FCE80acd0; + address internal constant SWAP_IMPL = + 0x6aA001e87F86E83bc4D569883332882cb47E2A13; + IPartyPool internal constant POOL = + IPartyPool(0x2A804e94500AE379ee0CcC423a67B07cc0aF548C); + bytes32 internal constant POOL_ID = bytes32(bytes20(address(POOL))); + uint256 internal constant FORK_BLOCK = 23978797; // block in which the pool + // was created + + LiquidityPartySwapAdapter internal adapter; + uint256 internal constant TEST_ITERATIONS = 10; + + address[] internal tokens; + address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address internal constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address internal constant UNI = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; + address internal constant WSOL = 0xD31a59c85aE9D8edEFeC411D448f90841571b89c; + address internal constant TRX = 0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5; + address internal constant AAVE = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; + address internal constant PEPE = 0x6982508145454Ce325dDbE47a25d4ec3d2311933; + address internal constant SHIB = 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE; + + address private constant INPUT_TOKEN = WBTC; + uint8 private constant INPUT_INDEX = 2; + address private constant OUTPUT_TOKEN = SHIB; + uint8 private constant OUTPUT_INDEX = 9; + + function setUp() public { + tokens = new address[](10); + tokens[0] = USDT; + tokens[1] = USDC; + tokens[2] = WBTC; + tokens[3] = WETH; + tokens[4] = UNI; + tokens[5] = WSOL; + tokens[6] = TRX; + tokens[7] = AAVE; + tokens[8] = PEPE; + tokens[9] = SHIB; + + vm.createSelectFork(vm.rpcUrl("mainnet"), FORK_BLOCK); + + adapter = new LiquidityPartySwapAdapter(PLANNER, INFO); + + vm.label(address(PLANNER), "PartyPlanner"); + vm.label(address(INFO), "PartyInfo"); + vm.label(address(MINT_IMPL), "PartyPoolMintImpl"); + vm.label(address(SWAP_IMPL), "PartyPoolSwapImpl"); + vm.label(address(POOL), "PartyPool"); + vm.label(address(adapter), "LiquidityPartySwapAdapter"); + for (uint256 i = 0; i < tokens.length; i++) { + vm.label(address(tokens[i]), IERC20Metadata(tokens[i]).symbol()); + } + } + + function testPrice() public view { + uint256[] memory amounts = new uint256[](3); + uint256 balance = IERC20(INPUT_TOKEN).balanceOf(address(POOL)); + amounts[0] = 2; // cannot use 1: the fee will round up and take + // everything, resulting in a zero-output reversion + amounts[1] = balance; + amounts[2] = balance * 2; + + Fraction[] memory prices = + adapter.price(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, amounts); + + for (uint256 i = 0; i < prices.length; i++) { + assertGt(prices[i].numerator, 0); + assertGt(prices[i].denominator, 0); + } + } + + function testPriceDecreasing() public view { + uint256[] memory limits = + adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN); + + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + // The first entry will be a zero amount which returns the current + // marginal price. + amounts[i] = limits[0] * i / (TEST_ITERATIONS - 1); + } + + Fraction[] memory prices = + adapter.price(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, amounts); + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + console2.log("compare price", prices[i].numerator); + console2.log(" ", prices[i].denominator); + console2.log(" > ", prices[i + 1].numerator); + console2.log(" ", prices[i + 1].denominator); + console2.log(); + assertEq(prices[i].compareFractions(prices[i + 1]), 1); + } + } + + function testSwapFuzz(uint256 amount) public { + uint256[] memory limits = + adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN); + vm.assume(amount > 1); // 1 will not work because we take fee-on-input + // and round up, leaving nothing to trade + vm.assume(amount <= limits[0]); + + deal(INPUT_TOKEN, address(this), amount); + IERC20(INPUT_TOKEN).approve(address(adapter), amount); + + uint256 usdtBalance = IERC20(INPUT_TOKEN).balanceOf(address(this)); + uint256 wethBalance = IERC20(OUTPUT_TOKEN).balanceOf(address(this)); + + Trade memory trade = adapter.swap( + POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, OrderSide.Sell, amount + ); + + if (trade.calculatedAmount > 0) { + assertEq( + amount, + usdtBalance - IERC20(INPUT_TOKEN).balanceOf(address(this)) + ); + assertEq( + trade.calculatedAmount, + IERC20(OUTPUT_TOKEN).balanceOf(address(this)) - wethBalance + ); + } + } + + function testSwapSellIncreasing() public { + uint256[] memory limits = + adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN); + uint256[] memory amounts = new uint256[](TEST_ITERATIONS); + Trade[] memory trades = new Trade[](TEST_ITERATIONS); + + for (uint256 i = 0; i < TEST_ITERATIONS; i++) { + amounts[i] = limits[0] * (i + 1) / (TEST_ITERATIONS - 1); + + uint256 beforeSwap = vm.snapshot(); + + deal(INPUT_TOKEN, address(this), amounts[i]); + IERC20(INPUT_TOKEN).approve(address(adapter), amounts[i]); + trades[i] = adapter.swap( + POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN, OrderSide.Sell, amounts[i] + ); + + vm.revertTo(beforeSwap); + } + + for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) { + assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount); + assertEq( + trades[i].price.denominator, trades[i + 1].price.denominator + ); // must share a basis + assertGe(trades[i].price.numerator, trades[i + 1].price.numerator); + } + } + + function testGetLimits() public view { + uint256[] memory limits = + adapter.getLimits(POOL_ID, INPUT_TOKEN, OUTPUT_TOKEN); + + assert(limits.length == 2); + assert(limits[0] > 0); + assert(limits[1] > 0); + } + + function testGetTokens() public view { + address[] memory adapterTokens = adapter.getTokens(POOL_ID); + for (uint256 i = 0; i < tokens.length; i++) { + assertEq(adapterTokens[i], tokens[i]); + } + } + + function testGetPoolIds() public view { + uint256 offset = 0; + uint256 limit = 10; + bytes32[] memory poolIds = adapter.getPoolIds(offset, limit); + + assertLe( + poolIds.length, + limit, + "Number of pool IDs should be less than or equal to limit" + ); + if (poolIds.length > 0) { + assertGt(uint256(poolIds[0]), 0, "Pool ID should be greater than 0"); + } + } + + // Many of the tests above seem entirely redundant with runPoolBehaviorTest + // :shrug: + function testLiquidityPartyPoolBehaviour() public { + bytes32[] memory poolIds = new bytes32[](1); + poolIds[0] = POOL_ID; + runPoolBehaviourTest(adapter, poolIds); + } +} diff --git a/substreams/ethereum-liquidityparty/Makefile b/substreams/ethereum-liquidityparty/Makefile index d9be557..8c961c9 100644 --- a/substreams/ethereum-liquidityparty/Makefile +++ b/substreams/ethereum-liquidityparty/Makefile @@ -1,2 +1,11 @@ +export PATH := /home/linuxbrew/.linuxbrew/bin:$(PATH) + +all: ethereum + build: - cargo build --target wasm32-unknown-unknown --release \ No newline at end of file + cargo build --target wasm32-unknown-unknown --release + +ethereum: build + substreams pack ethereum-liquidityparty.yaml -o ethereum-liquidityparty.spkg + +.PHONY: build ethereum diff --git a/substreams/ethereum-liquidityparty/abi/party_pool_viewer.abi.json b/substreams/ethereum-liquidityparty/abi/party_info.abi.json similarity index 93% rename from substreams/ethereum-liquidityparty/abi/party_pool_viewer.abi.json rename to substreams/ethereum-liquidityparty/abi/party_info.abi.json index 1541cd4..780b83d 100644 --- a/substreams/ethereum-liquidityparty/abi/party_pool_viewer.abi.json +++ b/substreams/ethereum-liquidityparty/abi/party_info.abi.json @@ -2,15 +2,15 @@ { "type": "constructor", "inputs": [ - { - "name": "swapImpl_", - "type": "address", - "internalType": "contract PartyPoolSwapImpl" - }, { "name": "mintImpl", "type": "address", "internalType": "contract PartyPoolMintImpl" + }, + { + "name": "swapImpl_", + "type": "address", + "internalType": "contract PartyPoolSwapImpl" } ], "stateMutability": "nonpayable" @@ -54,7 +54,7 @@ "internalType": "uint256" }, { - "name": "inputTokenIndex", + "name": "outputTokenIndex", "type": "uint256", "internalType": "uint256" } @@ -64,6 +64,11 @@ "name": "amountOut", "type": "uint256", "internalType": "uint256" + }, + { + "name": "outFee", + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "view" @@ -97,30 +102,6 @@ ], "stateMutability": "view" }, - { - "type": "function", - "name": "flashRepaymentAmounts", - "inputs": [ - { - "name": "pool", - "type": "address", - "internalType": "contract IPartyPool" - }, - { - "name": "loanAmounts", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "outputs": [ - { - "name": "repaymentAmounts", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "maxFlashLoan", @@ -216,8 +197,8 @@ "outputs": [ { "name": "", - "type": "int128", - "internalType": "int128" + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "view" @@ -249,12 +230,12 @@ "internalType": "uint256" }, { - "name": "fee", + "name": "lpMinted", "type": "uint256", "internalType": "uint256" }, { - "name": "lpMinted", + "name": "inFee", "type": "uint256", "internalType": "uint256" } @@ -298,11 +279,30 @@ "internalType": "uint256" }, { - "name": "fee", + "name": "inFee", "type": "uint256", "internalType": "uint256" } ], "stateMutability": "view" + }, + { + "type": "function", + "name": "working", + "inputs": [ + { + "name": "pool", + "type": "address", + "internalType": "contract IPartyPool" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" } ] diff --git a/substreams/ethereum-liquidityparty/abi/party_planner.abi.json b/substreams/ethereum-liquidityparty/abi/party_planner.abi.json index 4b6519e..f4b0402 100644 --- a/substreams/ethereum-liquidityparty/abi/party_planner.abi.json +++ b/substreams/ethereum-liquidityparty/abi/party_planner.abi.json @@ -23,14 +23,14 @@ "internalType": "contract PartyPoolMintImpl" }, { - "name": "deployer_", + "name": "poolInitCodeStorage_", "type": "address", - "internalType": "contract IPartyPoolDeployer" + "internalType": "contract PartyPoolInitCode" }, { - "name": "balancedPairDeployer_", + "name": "balancedPairInitCodeStorage_", "type": "address", - "internalType": "contract IPartyPoolDeployer" + "internalType": "contract PartyPoolBalancedPairInitCode" }, { "name": "protocolFeePpm_", @@ -174,12 +174,91 @@ "internalType": "contract IERC20[]" }, { - "name": "bases_", + "name": "kappa_", + "type": "int128", + "internalType": "int128" + }, + { + "name": "swapFeesPpm_", "type": "uint256[]", "internalType": "uint256[]" }, { - "name": "kappa_", + "name": "flashFeePpm_", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "stable_", + "type": "bool", + "internalType": "bool" + }, + { + "name": "payer", + "type": "address", + "internalType": "address" + }, + { + "name": "receiver", + "type": "address", + "internalType": "address" + }, + { + "name": "initialDeposits", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "initialLpAmount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "deadline", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "pool", + "type": "address", + "internalType": "contract IPartyPool" + }, + { + "name": "lpAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "newPool", + "inputs": [ + { + "name": "name_", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol_", + "type": "string", + "internalType": "string" + }, + { + "name": "tokens_", + "type": "address[]", + "internalType": "contract IERC20[]" + }, + { + "name": "tradeFrac_", + "type": "int128", + "internalType": "int128" + }, + { + "name": "targetSlippage_", "type": "int128", "internalType": "int128" }, @@ -258,17 +337,7 @@ "internalType": "contract IERC20[]" }, { - "name": "bases_", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "tradeFrac_", - "type": "int128", - "internalType": "int128" - }, - { - "name": "targetSlippage_", + "name": "kappa_", "type": "int128", "internalType": "int128" }, @@ -340,6 +409,86 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "params", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct IPartyPoolDeployer.DeployParams", + "components": [ + { + "name": "nonce", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "owner", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "symbol", + "type": "string", + "internalType": "string" + }, + { + "name": "tokens", + "type": "address[]", + "internalType": "contract IERC20[]" + }, + { + "name": "kappa", + "type": "int128", + "internalType": "int128" + }, + { + "name": "fees", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "flashFeePpm", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "protocolFeePpm", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "protocolFeeAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "wrapper", + "type": "address", + "internalType": "contract NativeWrapper" + }, + { + "name": "swapImpl", + "type": "address", + "internalType": "contract PartyPoolSwapImpl" + }, + { + "name": "mintImpl", + "type": "address", + "internalType": "contract PartyPoolMintImpl" + } + ] + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "poolCount", @@ -405,6 +554,19 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setProtocolFeeAddress", + "inputs": [ + { + "name": "feeAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "swapImpl", diff --git a/substreams/ethereum-liquidityparty/abi/party_pool.abi.json b/substreams/ethereum-liquidityparty/abi/party_pool.abi.json index 7c745e0..242f77e 100644 --- a/substreams/ethereum-liquidityparty/abi/party_pool.abi.json +++ b/substreams/ethereum-liquidityparty/abi/party_pool.abi.json @@ -1,73 +1,7 @@ [ { "type": "constructor", - "inputs": [ - { - "name": "owner_", - "type": "address", - "internalType": "address" - }, - { - "name": "name_", - "type": "string", - "internalType": "string" - }, - { - "name": "symbol_", - "type": "string", - "internalType": "string" - }, - { - "name": "tokens_", - "type": "address[]", - "internalType": "contract IERC20[]" - }, - { - "name": "bases_", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "kappa_", - "type": "int128", - "internalType": "int128" - }, - { - "name": "swapFeePpm_", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "flashFeePpm_", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "protocolFeePpm_", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "protocolFeeAddress_", - "type": "address", - "internalType": "address" - }, - { - "name": "wrapperToken_", - "type": "address", - "internalType": "contract NativeWrapper" - }, - { - "name": "swapImpl_", - "type": "address", - "internalType": "contract PartyPoolSwapImpl" - }, - { - "name": "mintImpl_", - "type": "address", - "internalType": "contract PartyPoolMintImpl" - } - ], + "inputs": [], "stateMutability": "nonpayable" }, { @@ -84,11 +18,6 @@ "type": "tuple", "internalType": "struct LMSRStabilized.State", "components": [ - { - "name": "nAssets", - "type": "uint256", - "internalType": "uint256" - }, { "name": "kappa", "type": "int128", @@ -256,7 +185,7 @@ "internalType": "uint256" }, { - "name": "inputTokenIndex", + "name": "outputTokenIndex", "type": "uint256", "internalType": "uint256" }, @@ -273,7 +202,12 @@ ], "outputs": [ { - "name": "amountOutUint", + "name": "amountOut", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "outFee", "type": "uint256", "internalType": "uint256" } @@ -313,6 +247,43 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "fee", + "inputs": [ + { + "name": "i", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "j", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "fees", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "flashFeePpm", @@ -360,25 +331,6 @@ ], "stateMutability": "nonpayable" }, - { - "type": "function", - "name": "getToken", - "inputs": [ - { - "name": "i", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IERC20" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "initialMint", @@ -478,7 +430,7 @@ { "name": "", "type": "address", - "internalType": "contract PartyPoolMintImpl" + "internalType": "address" } ], "stateMutability": "view" @@ -555,6 +507,19 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setProtocolFeeAddress", + "inputs": [ + { + "name": "feeAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "swap", @@ -564,6 +529,11 @@ "type": "address", "internalType": "address" }, + { + "name": "fundingSelector", + "type": "bytes4", + "internalType": "bytes4" + }, { "name": "receiver", "type": "address", @@ -598,6 +568,11 @@ "name": "unwrap", "type": "bool", "internalType": "bool" + }, + { + "name": "cbData", + "type": "bytes", + "internalType": "bytes" } ], "outputs": [ @@ -612,7 +587,7 @@ "internalType": "uint256" }, { - "name": "fee", + "name": "inFee", "type": "uint256", "internalType": "uint256" } @@ -656,7 +631,7 @@ "internalType": "uint256" }, { - "name": "fee", + "name": "inFee", "type": "uint256", "internalType": "uint256" } @@ -665,13 +640,13 @@ }, { "type": "function", - "name": "swapFeePpm", + "name": "swapImpl", "inputs": [], "outputs": [ { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address", + "internalType": "address" } ], "stateMutability": "view" @@ -707,27 +682,24 @@ } ], "outputs": [ + { + "name": "amountInUsed", + "type": "uint256", + "internalType": "uint256" + }, { "name": "lpMinted", "type": "uint256", "internalType": "uint256" + }, + { + "name": "inFee", + "type": "uint256", + "internalType": "uint256" } ], "stateMutability": "payable" }, - { - "type": "function", - "name": "swapMintImpl", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract PartyPoolSwapImpl" - } - ], - "stateMutability": "view" - }, { "type": "function", "name": "swapToLimit", @@ -737,6 +709,11 @@ "type": "address", "internalType": "address" }, + { + "name": "fundingSelector", + "type": "bytes4", + "internalType": "bytes4" + }, { "name": "receiver", "type": "address", @@ -766,6 +743,11 @@ "name": "unwrap", "type": "bool", "internalType": "bool" + }, + { + "name": "cbData", + "type": "bytes", + "internalType": "bytes" } ], "outputs": [ @@ -780,7 +762,7 @@ "internalType": "uint256" }, { - "name": "fee", + "name": "inFee", "type": "uint256", "internalType": "uint256" } @@ -800,6 +782,25 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "token", + "inputs": [ + { + "name": "i", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract IERC20" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "totalSupply", @@ -1333,6 +1334,22 @@ "name": "FailedCall", "inputs": [] }, + { + "type": "error", + "name": "InsufficientBalance", + "inputs": [ + { + "name": "balance", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "needed", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "OwnableInvalidOwner", diff --git a/substreams/ethereum-liquidityparty/bin/generate_abi b/substreams/ethereum-liquidityparty/bin/generate_abi new file mode 100755 index 0000000..e32190f --- /dev/null +++ b/substreams/ethereum-liquidityparty/bin/generate_abi @@ -0,0 +1,11 @@ +#!/bin/bash +LMSR_HOME=../../../lmsr-amm +CHAIN_ID=11155111 +abi() { + jq '.abi' $LMSR_HOME/deployment/$CHAIN_ID/v1/out/$1.sol/$1.json > abi/$2.abi.json + echo abi/$2.abi.json +} + +abi PartyPlanner party_planner +abi PartyPool party_pool +abi PartyInfo party_info diff --git a/substreams/ethereum-liquidityparty/sepolia-liquidityparty.yaml b/substreams/ethereum-liquidityparty/ethereum-liquidityparty.yaml similarity index 73% rename from substreams/ethereum-liquidityparty/sepolia-liquidityparty.yaml rename to substreams/ethereum-liquidityparty/ethereum-liquidityparty.yaml index a45a5f7..0c69040 100644 --- a/substreams/ethereum-liquidityparty/sepolia-liquidityparty.yaml +++ b/substreams/ethereum-liquidityparty/ethereum-liquidityparty.yaml @@ -16,18 +16,17 @@ binaries: type: wasm/rust-v1 file: ../target/wasm32-unknown-unknown/release/ethereum_liquidityparty.wasm -network: sepolia +network: mainnet networks: - sepolia: + mainnet: initialBlock: - map_protocol_components: 9460804 + map_protocol_components: 23978797 params: - map_protocol_components: planner=0x0ad06C08ab5049e6Fd4d7f5AF457115A1475326b&viewer=0x750d63a39a4ccfCfB69D2f5aFDa065909C717cAB&mint_impl=0x25bb10BA84944F8aAEf1fD247C3B7Fe7271C23F9&swap_impl=0x69b4F102e0747f61F8529b3bbFf2FC4b27438d0F&deployer=0x0939F93BAa3c96226853F9F39A95beF48eA8fF04&bp_deployer=0xfda454fF7876aad9408517Ed2F0d11AA229Ad0a4 + map_protocol_components: planner=0x42977f565971F6D288a05ddEbC87A17276F71A29&info=0x605F803cD27F5c1fa01440B2cbd5D3E4Cf7EE850&mint_impl=0xA0375403921e9B357E1BeD57bef3fA3FCE80acd0&swap_impl=0x6aA001e87F86E83bc4D569883332882cb47E2A13 modules: - name: map_protocol_components kind: map - initialBlock: 1 inputs: - params: string - source: sf.ethereum.type.v2.Block @@ -36,7 +35,6 @@ modules: - name: store_protocol_components kind: store - initialBlock: 1 updatePolicy: set valueType: string inputs: @@ -44,7 +42,6 @@ modules: - name: map_relative_component_balance kind: map - initialBlock: 1 inputs: - source: sf.ethereum.type.v2.Block - store: store_protocol_components @@ -53,7 +50,6 @@ modules: - name: store_balances kind: store - initialBlock: 1 updatePolicy: add valueType: bigint inputs: @@ -61,7 +57,6 @@ modules: - name: map_protocol_changes kind: map - initialBlock: 1 inputs: - source: sf.ethereum.type.v2.Block - map: map_protocol_components diff --git a/substreams/ethereum-liquidityparty/integration_test.tycho.yaml b/substreams/ethereum-liquidityparty/integration_test.tycho.yaml index 3a5c57a..e3fbda8 100644 --- a/substreams/ethereum-liquidityparty/integration_test.tycho.yaml +++ b/substreams/ethereum-liquidityparty/integration_test.tycho.yaml @@ -1,69 +1,40 @@ -# Name of the substreams config file in your substreams module. Usually "./substreams.yaml" -substreams_yaml_path: ./substreams.yaml -# Name of the adapter contract, usually: ProtocolSwapAdapter" -adapter_contract: "SwapAdapter" -# Constructor signature of the Adapter contract" -adapter_build_signature: "constructor(address)" -# A comma separated list of args to be passed to the contructor of the Adapter contract" -adapter_build_args: "0x0000000000000000000000000000000000000000" -# Whether the testing script should skip checking balances of the protocol components. -# If set to `true` please always add a reason why it's skipped. -skip_balance_check: false -# Accounts that will be automatically initialized at test start -# IMPORTANT: These are TEST FIXTURES ONLY. Your actual code must still properly -# initialize these accounts. This configuration only eliminates the need to include -# historical blocks containing the initialization events in your test data. -# -# Example use case: -# - Your substream would normally initialize account XYZ at block 10000 -# - Your test only includes blocks 20000-21000 for efficiency -# - You list XYZ here so the test environment will automatically initialize the account XYZ with the state it had at block 20000 -# - Your actual substream code MUST STILL contain the initialization and state tracking logic for this contract -# -# Without this, you would need to include block 10000 in your test data or your -# test would fail because the account appears uninitialized to your code. -initialized_accounts: - - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" # Needed for .... -# A list of protocol types names created by your Substreams module. +substreams_yaml_path: ./sepolia-liquidityparty.yaml +adapter_contract: "LiquidityPartySwapAdapter" +adapter_build_signature: "constructor(address,address)" +# PartyPlanner, PartyInfo +adapter_build_args: "0x42977f565971F6D288a05ddEbC87A17276F71A29,0x605F803cD27F5c1fa01440B2cbd5D3E4Cf7EE850" +# Liquidity Party protocol fees are retained in the pool until collection, so the pool balance does not match the TVL. +skip_balance_check: true protocol_type_names: - - "type_name_1" - - "type_name_2" -# A list of tests. -# The name of the protocol system -protocol_system: "protocol_name" + - "liquidityparty_pool" +protocol_system: "vm:liquidityparty" +initialized_accounts: [ + "0x42977f565971F6D288a05ddEbC87A17276F71A29", # PartyPlanner + "0x605F803cD27F5c1fa01440B2cbd5D3E4Cf7EE850", # PartyInfo + "0xA0375403921e9B357E1BeD57bef3fA3FCE80acd0", # PartyPoolMintImpl + "0x6aA001e87F86E83bc4D569883332882cb47E2A13", # PartyPoolSwapImpl +] + tests: - # Name of the test - name: test_pool_creation - # Indexed block range - start_block: 123 - stop_block: 456 - # Same as global `initialized_accounts` but only scoped to this test. - initialized_accounts: - - "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" # Needed for .... - # A list of expected component indexed in the block range. Each component must match perfectly the `ProtocolComponent` indexed by your subtreams module. + start_block: 23978797 + stop_block: 23978798 + initialized_accounts: [] expected_components: - - id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" + - id: "0x2A804e94500AE379ee0CcC423a67B07cc0aF548C" tokens: - - "0xdac17f958d2ee523a2206206994597c13d831ec7" - - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - - "0x6b175474e89094c44da98b954eedeac495271d0f" - static_attributes: { } - creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" - # Whether the script should skip trying to simulate a swap on this component. - # If set to `true` please always add a reason why it's skipped. + # Does this test whether the tokens are in the correct order? + - "0xdAC17F958D2ee523a2206206994597C13D831ec7" # USDT + - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" # USDC + - "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" # WBTC + - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH + - "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" # UNI + - "0xD31a59c85aE9D8edEFeC411D448f90841571b89c" # SOL + - "0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5" # TRX + - "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9" # AAVE + - "0x6982508145454Ce325dDbE47a25d4ec3d2311933" # PEPE + - "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE" # SHIB + static_attributes: {} + creation_tx: "0x5c8b1e1e6ec10143a1252799d14df09c7e84f6a99ccde95fc11295a61c20060e" skip_simulation: false - # Whether the script should skip trying to simulate execution of a swap on this component. - # If set to `true` please always add a reason why it's skipped. skip_execution: false - - name: test_something_else - start_block: 123 - stop_block: 456 - expected_components: - - id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022" - tokens: - - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" - - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - static_attributes: { } - creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" - skip_simulation: true # If true, always add a reason - skip_execution: true # If true, always add a reason diff --git a/substreams/ethereum-liquidityparty/src/abi/.gitignore b/substreams/ethereum-liquidityparty/src/abi/.gitignore index a7d41f4..b21a68a 100644 --- a/substreams/ethereum-liquidityparty/src/abi/.gitignore +++ b/substreams/ethereum-liquidityparty/src/abi/.gitignore @@ -1,4 +1,4 @@ mod.rs party_planner.rs party_pool.rs -party_pool_viewer.rs \ No newline at end of file +party_info.rs diff --git a/substreams/ethereum-liquidityparty/src/modules.rs b/substreams/ethereum-liquidityparty/src/modules.rs index 6950c3f..06c7805 100644 --- a/substreams/ethereum-liquidityparty/src/modules.rs +++ b/substreams/ethereum-liquidityparty/src/modules.rs @@ -53,6 +53,7 @@ fn map_protocol_components( param_string: String, block: eth::v2::Block ) -> Result { + substreams::log::debug!("Processing block {} for protocol components", block.number); let params = Params::parse(¶m_string)?; Ok(BlockTransactionProtocolComponents { tx_components: block @@ -94,6 +95,7 @@ fn store_protocol_components( map_protocol_components: BlockTransactionProtocolComponents, store: StoreSetRaw, ) { + substreams::log::debug!("Storing {} protocol components", map_protocol_components.tx_components.len()); map_protocol_components .tx_components .into_iter() @@ -129,6 +131,7 @@ fn map_relative_component_balance( block: eth::v2::Block, store: StoreGetRaw, ) -> Result { + substreams::log::debug!("Processing block {} for balance changes", block.number); let mut res = Vec::new(); for log in block.logs() { @@ -239,6 +242,12 @@ fn map_protocol_changes( balance_store: StoreDeltas, deltas: BlockBalanceDeltas, ) -> Result { + substreams::log::debug!( + "Processing block {} changes: {} new components, {} balance deltas", + block.number, + new_components.tx_components.len(), + deltas.balance_deltas.len() + ); // We merge contract changes by transaction (identified by transaction index) // making it easy to sort them at the very end. let mut transaction_changes: HashMap<_, TransactionChangesBuilder> = HashMap::new(); diff --git a/substreams/ethereum-liquidityparty/src/params.rs b/substreams/ethereum-liquidityparty/src/params.rs index a285223..5625b4c 100644 --- a/substreams/ethereum-liquidityparty/src/params.rs +++ b/substreams/ethereum-liquidityparty/src/params.rs @@ -4,20 +4,17 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] struct StringParams { planner: String, - viewer: String, + info: String, mint_impl: String, swap_impl: String, - deployer: String, - bp_deployer: String, } pub(crate) struct Params { pub planner: Vec, - pub viewer: Vec, + #[allow(dead_code)] // We keep the unused info field for future pricing/view operations + pub info: Vec, pub mint_impl: Vec, pub swap_impl: Vec, - pub deployer: Vec, - pub bp_deployer: Vec, } impl StringParams { @@ -44,11 +41,9 @@ impl Params { Ok(Self { planner: decode_addr(¶ms.planner)?, - viewer: decode_addr(¶ms.viewer)?, + info: decode_addr(¶ms.info)?, mint_impl: decode_addr(¶ms.mint_impl)?, swap_impl: decode_addr(¶ms.swap_impl)?, - deployer: decode_addr(¶ms.deployer)?, - bp_deployer: decode_addr(¶ms.bp_deployer)?, }) } } diff --git a/substreams/ethereum-liquidityparty/src/pool_factories.rs b/substreams/ethereum-liquidityparty/src/pool_factories.rs index 99407db..676768a 100644 --- a/substreams/ethereum-liquidityparty/src/pool_factories.rs +++ b/substreams/ethereum-liquidityparty/src/pool_factories.rs @@ -1,10 +1,10 @@ +use crate::abi; +use crate::params::Params; use substreams_ethereum::pb::eth::v2::{Call, Log, TransactionTrace}; use substreams_ethereum::Event; use tycho_substreams::models::{ - ChangeType, FinancialType, ImplementationType, ProtocolComponent, ProtocolType, + ImplementationType, ProtocolComponent, }; -use crate::abi; -use crate::params::Params; /// Potentially constructs a new ProtocolComponent given a call /// @@ -21,19 +21,10 @@ pub fn maybe_create_component( ) -> Option { if call.address.as_slice() == params.planner { if let Some(event) = abi::party_planner::events::PartyStarted::match_and_decode(_log) { - return Some(ProtocolComponent { - id: hex::encode(&event.pool), - tokens: event.tokens, - contracts: vec![event.pool.clone(), params.mint_impl.clone(), params.swap_impl.clone()], - static_att: vec![], - change: ChangeType::Creation.into(), - protocol_type: Some(ProtocolType { - name: "liquidity_party".to_string(), - financial_type: FinancialType::Swap.into(), - attribute_schema: vec![], - implementation_type: ImplementationType::Vm.into(), - }), - }); + return Some(ProtocolComponent::new(&format!("0x{}", hex::encode(&event.pool))) + .with_tokens(&event.tokens.clone()) + .with_contracts(&vec![event.pool.clone(), params.mint_impl.clone(), params.swap_impl.clone()]) + .as_swap_type("liquidityparty_pool", ImplementationType::Vm)); } } None