Liquidity Party adapter

This commit is contained in:
tim
2025-12-10 15:53:45 -04:00
parent f0f2f20c3c
commit f1e602d414
21 changed files with 1589 additions and 321 deletions

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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)));
}
}

View File

@@ -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: {}

View File

@@ -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,51 +106,94 @@ 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];
) 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(
assertGeTol(
executedPrice,
priceAtAmount,
"Price should be greated than executed price."
toleranceDenominator,
"Price should be greater than executed price."
);
assertGt(
assertGtTol(
executedPrice,
priceAfterSwap,
toleranceDenominator,
"Executed price should be greater than price after swap."
);
assertGt(
assertGtTol(
priceAtZero,
executedPrice,
"Price should be greated than price after swap."
toleranceDenominator,
"Price should be greater than price after swap."
);
} else {
assertGe(
assertGeTol(
priceAtZero,
priceAfterSwap,
toleranceDenominator,
"Executed price should be or equal to price after swap."
);
assertGe(
assertGeTol(
priceAtZero,
priceAtAmount,
toleranceDenominator,
"Executed price should be or equal to price after swap."
);
assertGe(
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) {
} catch (bytes memory reason) {
(bool isTooSmall, uint256 lowerLimit) = decodeTooSmallError(reason);
(bool isLimitExceeded, uint256 limit) =
decodeLimitExceededError(reason);
if (isLimitExceeded) {
supportsLimitExceeded = true;
console2.log(
"TEST: Expected error when fetching price above limit: %s", s
"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) {
} 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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,2 +1,11 @@
export PATH := /home/linuxbrew/.linuxbrew/bin:$(PATH)
all: ethereum
build:
cargo build --target wasm32-unknown-unknown --release
ethereum: build
substreams pack ethereum-liquidityparty.yaml -o ethereum-liquidityparty.spkg
.PHONY: build ethereum

View File

@@ -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"
}
]

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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"
# 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: "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.
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

View File

@@ -1,4 +1,4 @@
mod.rs
party_planner.rs
party_pool.rs
party_pool_viewer.rs
party_info.rs

View File

@@ -53,6 +53,7 @@ fn map_protocol_components(
param_string: String,
block: eth::v2::Block
) -> Result<BlockTransactionProtocolComponents> {
substreams::log::debug!("Processing block {} for protocol components", block.number);
let params = Params::parse(&param_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<BlockBalanceDeltas, anyhow::Error> {
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<BlockChanges, substreams::errors::Error> {
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();

View File

@@ -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<u8>,
pub viewer: Vec<u8>,
#[allow(dead_code)] // We keep the unused info field for future pricing/view operations
pub info: Vec<u8>,
pub mint_impl: Vec<u8>,
pub swap_impl: Vec<u8>,
pub deployer: Vec<u8>,
pub bp_deployer: Vec<u8>,
}
impl StringParams {
@@ -44,11 +41,9 @@ impl Params {
Ok(Self {
planner: decode_addr(&params.planner)?,
viewer: decode_addr(&params.viewer)?,
info: decode_addr(&params.info)?,
mint_impl: decode_addr(&params.mint_impl)?,
swap_impl: decode_addr(&params.swap_impl)?,
deployer: decode_addr(&params.deployer)?,
bp_deployer: decode_addr(&params.bp_deployer)?,
})
}
}

View File

@@ -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<ProtocolComponent> {
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