refactor PartyPoolSwapMintImpl
This commit is contained in:
@@ -9,7 +9,7 @@ remappings = [
|
|||||||
optimizer=true
|
optimizer=true
|
||||||
optimizer_runs=999999999
|
optimizer_runs=999999999
|
||||||
viaIR=true
|
viaIR=true
|
||||||
gas_reports = ['PartyPool', 'PartyPlanner']
|
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolViewImpl']
|
||||||
fs_permissions = [{ access = "write", path = "chain.json"}]
|
fs_permissions = [{ access = "write", path = "chain.json"}]
|
||||||
|
|
||||||
[lint]
|
[lint]
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "../src/Deploy.sol";
|
||||||
|
import "../src/IPartyPool.sol";
|
||||||
|
import "../src/PartyPlanner.sol";
|
||||||
|
import "../src/PartyPool.sol";
|
||||||
|
import "../test/MockERC20.sol";
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
import "forge-std/Script.sol";
|
import "forge-std/Script.sol";
|
||||||
import "forge-std/console2.sol";
|
import "forge-std/console2.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
|
||||||
import "../test/MockERC20.sol";
|
|
||||||
import "../src/IPartyPool.sol";
|
|
||||||
import "../src/PartyPool.sol";
|
|
||||||
import "../src/PartyPlanner.sol";
|
|
||||||
|
|
||||||
contract DeployMock is Script {
|
contract DeployMock is Script {
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ contract DeployMock is Script {
|
|||||||
uint256 _feePpm = 100;
|
uint256 _feePpm = 100;
|
||||||
|
|
||||||
// deploy a PartyPlanner factory and create the pool via factory
|
// deploy a PartyPlanner factory and create the pool via factory
|
||||||
PartyPlanner planner = new PartyPlanner();
|
PartyPlanner planner = Deploy.newPartyPlanner();
|
||||||
|
|
||||||
// prepare initial deposits (10_000 units of each token, scaled by bases)
|
// prepare initial deposits (10_000 units of each token, scaled by bases)
|
||||||
uint256[] memory initialDeposits = new uint256[](3);
|
uint256[] memory initialDeposits = new uint256[](3);
|
||||||
@@ -56,8 +57,8 @@ contract DeployMock is Script {
|
|||||||
IERC20(tokens[i]).approve(address(planner), initialDeposits[i]);
|
IERC20(tokens[i]).approve(address(planner), initialDeposits[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call full createPool signature on factory which will take the deposits and mint initial LP
|
// call full newPool signature on factory which will take the deposits and mint initial LP
|
||||||
(PartyPool pool, uint256 lpAmount) = planner.createPool(
|
(PartyPool pool, ) = planner.newPool(
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
tokens,
|
tokens,
|
||||||
|
|||||||
32
src/Deploy.sol
Normal file
32
src/Deploy.sol
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import {PartyPool} from "./PartyPool.sol";
|
||||||
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
import {PartyPlanner} from "./PartyPlanner.sol";
|
||||||
|
|
||||||
|
library Deploy {
|
||||||
|
|
||||||
|
function newPartyPlanner() internal returns (PartyPlanner) {
|
||||||
|
return new PartyPlanner(
|
||||||
|
new PartyPoolSwapMintImpl()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newPartyPool(
|
||||||
|
string memory name_,
|
||||||
|
string memory symbol_,
|
||||||
|
IERC20[] memory tokens_,
|
||||||
|
uint256[] memory bases_,
|
||||||
|
int128 _kappa,
|
||||||
|
uint256 _swapFeePpm,
|
||||||
|
uint256 _flashFeePpm,
|
||||||
|
bool _stable
|
||||||
|
) internal returns (PartyPool) {
|
||||||
|
return new PartyPool(name_, symbol_, tokens_, bases_, _kappa, _swapFeePpm, _flashFeePpm, _stable,
|
||||||
|
new PartyPoolSwapMintImpl()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ interface IPartyPlanner {
|
|||||||
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
||||||
/// @return pool Address of the newly created and initialized PartyPool
|
/// @return pool Address of the newly created and initialized PartyPool
|
||||||
/// @return lpAmount Amount of LP tokens minted to the receiver
|
/// @return lpAmount Amount of LP tokens minted to the receiver
|
||||||
function createPool(
|
function newPool(
|
||||||
// Pool constructor args (legacy)
|
// Pool constructor args (legacy)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -61,7 +61,7 @@ interface IPartyPlanner {
|
|||||||
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
/// @param deadline Reverts if nonzero and the current blocktime is later than the deadline
|
||||||
/// @return pool Address of the newly created and initialized PartyPool
|
/// @return pool Address of the newly created and initialized PartyPool
|
||||||
/// @return lpAmount Amount of LP tokens minted to the receiver
|
/// @return lpAmount Amount of LP tokens minted to the receiver
|
||||||
function createPool(
|
function newPool(
|
||||||
// Pool constructor args (kappa-based)
|
// Pool constructor args (kappa-based)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -115,4 +115,8 @@ interface IPartyPlanner {
|
|||||||
/// @param limit Maximum number of items to return
|
/// @param limit Maximum number of items to return
|
||||||
/// @return pools Array of pool addresses containing the specified token
|
/// @return pools Array of pool addresses containing the specified token
|
||||||
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
|
function getPoolsByToken(IERC20 token, uint256 offset, uint256 limit) external view returns (PartyPool[] memory pools);
|
||||||
|
|
||||||
|
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
||||||
|
function swapMintImpl() external view returns (address);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,9 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Immutable pool configuration (public getters)
|
|
||||||
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
||||||
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
||||||
function tokens(uint256) external view returns (IERC20); // get single token
|
function getToken(uint256) external view returns (IERC20); // get single token
|
||||||
|
|
||||||
/// @notice Returns the number of tokens (n) in the pool.
|
/// @notice Returns the number of tokens (n) in the pool.
|
||||||
function numTokens() external view returns (uint256);
|
function numTokens() external view returns (uint256);
|
||||||
@@ -80,10 +79,6 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
|
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
|
||||||
function kappa() external view returns (int128);
|
function kappa() external view returns (int128);
|
||||||
|
|
||||||
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
|
||||||
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
|
||||||
function tokenAddressToIndexPlusOne(IERC20) external view returns (uint);
|
|
||||||
|
|
||||||
// Initialization / Mint / Burn (LP token managed)
|
// Initialization / Mint / Burn (LP token managed)
|
||||||
|
|
||||||
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
/// @notice Calculate the proportional deposit amounts required for a given LP token amount
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import "./PartyPool.sol";
|
|||||||
import "./LMSRStabilized.sol";
|
import "./LMSRStabilized.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
|
||||||
/// @title PartyPlanner
|
/// @title PartyPlanner
|
||||||
/// @notice Factory contract for creating and tracking PartyPool instances
|
/// @notice Factory contract for creating and tracking PartyPool instances
|
||||||
@@ -13,6 +14,9 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
|
int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
|
||||||
|
|
||||||
|
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
||||||
|
address public immutable swapMintImpl;
|
||||||
|
|
||||||
// On-chain pool indexing
|
// On-chain pool indexing
|
||||||
PartyPool[] private _allPools;
|
PartyPool[] private _allPools;
|
||||||
IERC20[] private _allTokens;
|
IERC20[] private _allTokens;
|
||||||
@@ -20,8 +24,14 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
mapping(IERC20 => bool) private _tokenSupported;
|
mapping(IERC20 => bool) private _tokenSupported;
|
||||||
mapping(IERC20 => PartyPool[]) private _poolsByToken;
|
mapping(IERC20 => PartyPool[]) private _poolsByToken;
|
||||||
|
|
||||||
/// Main createPool variant: accepts kappa directly (preferred).
|
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
|
||||||
function createPool(
|
constructor(PartyPoolSwapMintImpl _swapMintImpl) {
|
||||||
|
require(address(_swapMintImpl) != address(0), "Planner: impl address cannot be zero");
|
||||||
|
swapMintImpl = address(_swapMintImpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main newPool variant: accepts kappa directly (preferred).
|
||||||
|
function newPool(
|
||||||
// Pool constructor args
|
// Pool constructor args
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -56,7 +66,8 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
_kappa,
|
_kappa,
|
||||||
_swapFeePpm,
|
_swapFeePpm,
|
||||||
_flashFeePpm,
|
_flashFeePpm,
|
||||||
_stable
|
_stable,
|
||||||
|
PartyPoolSwapMintImpl(swapMintImpl)
|
||||||
);
|
);
|
||||||
|
|
||||||
_allPools.push(pool);
|
_allPools.push(pool);
|
||||||
@@ -89,8 +100,10 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
lpAmount = pool.initialMint(receiver, initialLpAmount);
|
lpAmount = pool.initialMint(receiver, initialLpAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backwards-compatible convenience overload: compute kappa from (tradeFrac, targetSlippage) then call kappa-based createPool.
|
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
|
||||||
function createPool(
|
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
|
||||||
|
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
|
||||||
|
function newPool(
|
||||||
// Pool constructor args (old signature)
|
// Pool constructor args (old signature)
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
@@ -115,8 +128,8 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
|
// Compute kappa from slippage params using LMSR helper (kappa depends only on n, f and s)
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(_tokens.length, _tradeFrac, _targetSlippage);
|
||||||
|
|
||||||
// Delegate to the kappa-based createPool variant
|
// Delegate to the kappa-based newPool variant
|
||||||
return createPool(
|
return newPool(
|
||||||
name_,
|
name_,
|
||||||
symbol_,
|
symbol_,
|
||||||
_tokens,
|
_tokens,
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ pragma solidity ^0.8.30;
|
|||||||
|
|
||||||
import "forge-std/console2.sol";
|
import "forge-std/console2.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
import "./LMSRStabilized.sol";
|
import "./LMSRStabilized.sol";
|
||||||
import "./LMSRStabilizedBalancedPair.sol";
|
import "./LMSRStabilizedBalancedPair.sol";
|
||||||
import "./IPartyPool.sol";
|
import "./IPartyPool.sol";
|
||||||
import "./IPartyFlashCallback.sol";
|
import "./IPartyFlashCallback.sol";
|
||||||
|
import "./PartyPoolBase.sol";
|
||||||
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
|
||||||
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
||||||
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
||||||
@@ -25,30 +26,11 @@ import "./IPartyFlashCallback.sol";
|
|||||||
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls.
|
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls.
|
||||||
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool
|
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool
|
||||||
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
|
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
|
||||||
contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
contract PartyPool is PartyPoolBase, IPartyPool {
|
||||||
using ABDKMath64x64 for int128;
|
using ABDKMath64x64 for int128;
|
||||||
using LMSRStabilized for LMSRStabilized.State;
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Immutable pool configuration
|
|
||||||
//
|
|
||||||
|
|
||||||
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
|
||||||
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
|
||||||
IERC20[] public tokens; // effectively immutable since there is no interface to change the tokens
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
|
||||||
function numTokens() external view returns (uint256) { return tokens.length; }
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
|
||||||
function allTokens() external view returns (IERC20[] memory) { return tokens; }
|
|
||||||
|
|
||||||
// NOTE that the slippage target is only exactly achieved in completely balanced pools where all assets are
|
|
||||||
// priced the same. This target is actually a minimum slippage that the pool imposes on traders, and the actual
|
|
||||||
// slippage cost can be multiples bigger in practice due to pool inventory imbalances.
|
|
||||||
|
|
||||||
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
/// @notice Liquidity parameter κ (Q64.64) used by the LMSR kernel: b = κ * S(q)
|
||||||
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage
|
/// @dev Pool is constructed with a fixed κ. Clients that previously passed tradeFrac/targetSlippage
|
||||||
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
|
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
|
||||||
@@ -60,71 +42,65 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||||
uint256 public immutable flashFeePpm;
|
uint256 public immutable flashFeePpm;
|
||||||
|
|
||||||
//
|
|
||||||
// Internal state
|
|
||||||
//
|
|
||||||
|
|
||||||
LMSRStabilized.State internal lmsr;
|
|
||||||
|
|
||||||
/// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations.
|
/// @notice If true and there are exactly two assets, an optimized 2-asset stable-pair path is used for some computations.
|
||||||
bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
|
bool immutable private _stablePair; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
|
||||||
|
|
||||||
// Cached on-chain balances (uint) and internal 64.64 representation
|
/// @notice Address of the SwapMint implementation contract for delegatecall
|
||||||
// balance / base = internal
|
address public immutable swapMintImpl;
|
||||||
uint256[] internal cachedUintBalances;
|
|
||||||
|
|
||||||
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
|
/// @inheritdoc IPartyPool
|
||||||
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
|
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
|
||||||
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function numTokens() external view returns (uint256) { return tokens.length; }
|
||||||
|
|
||||||
|
/// @inheritdoc IPartyPool
|
||||||
|
function allTokens() external view returns (IERC20[] memory) { return tokens; }
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function denominators() external view returns (uint256[] memory) { return bases; }
|
function denominators() external view returns (uint256[] memory) { return bases; }
|
||||||
|
|
||||||
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
|
||||||
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
|
||||||
mapping(IERC20=>uint) public tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
|
|
||||||
|
|
||||||
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
|
||||||
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
|
||||||
uint256 public constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
|
|
||||||
|
|
||||||
/// @param name_ LP token name
|
/// @param name_ LP token name
|
||||||
/// @param symbol_ LP token symbol
|
/// @param symbol_ LP token symbol
|
||||||
/// @param _tokens token addresses (n)
|
/// @param tokens_ token addresses (n)
|
||||||
/// @param _bases scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
|
/// @param bases_ scaling bases for each token (n) - used when converting to/from internal 64.64 amounts
|
||||||
/// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
/// @param _kappa liquidity parameter κ (Q64.64) used to derive b = κ * S(q)
|
||||||
/// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
/// @param _swapFeePpm fee in parts-per-million, taken from swap input amounts before LMSR calculations
|
||||||
/// @param _flashFeePpm fee in parts-per-million, taken for flash loans
|
/// @param _flashFeePpm fee in parts-per-million, taken for flash loans
|
||||||
/// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
|
/// @param _stable if true and assets.length==2, then the optimization for 2-asset stablecoin pools is activated.
|
||||||
|
/// @param _swapMintImpl address of the SwapMint implementation contract
|
||||||
constructor(
|
constructor(
|
||||||
string memory name_,
|
string memory name_,
|
||||||
string memory symbol_,
|
string memory symbol_,
|
||||||
IERC20[] memory _tokens,
|
IERC20[] memory tokens_,
|
||||||
uint256[] memory _bases,
|
uint256[] memory bases_,
|
||||||
int128 _kappa,
|
int128 _kappa,
|
||||||
uint256 _swapFeePpm,
|
uint256 _swapFeePpm,
|
||||||
uint256 _flashFeePpm,
|
uint256 _flashFeePpm,
|
||||||
bool _stable
|
bool _stable,
|
||||||
) ERC20(name_, symbol_) {
|
PartyPoolSwapMintImpl _swapMintImpl
|
||||||
require(_tokens.length > 1, "Pool: need >1 asset");
|
) PartyPoolBase(name_, symbol_) {
|
||||||
require(_tokens.length == _bases.length, "Pool: lengths mismatch");
|
require(tokens_.length > 1, "Pool: need >1 asset");
|
||||||
tokens = _tokens;
|
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
||||||
bases = _bases;
|
tokens = tokens_;
|
||||||
|
bases = bases_;
|
||||||
kappa = _kappa;
|
kappa = _kappa;
|
||||||
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
||||||
swapFeePpm = _swapFeePpm;
|
swapFeePpm = _swapFeePpm;
|
||||||
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
||||||
flashFeePpm = _flashFeePpm;
|
flashFeePpm = _flashFeePpm;
|
||||||
_stablePair = _stable && _tokens.length == 2;
|
_stablePair = _stable && tokens_.length == 2;
|
||||||
|
require(address(_swapMintImpl) != address(0), "Pool: impl address zero");
|
||||||
|
swapMintImpl = address(_swapMintImpl);
|
||||||
|
|
||||||
uint256 n = _tokens.length;
|
uint256 n = tokens_.length;
|
||||||
|
|
||||||
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
||||||
lmsr.nAssets = n;
|
lmsr.nAssets = n;
|
||||||
|
|
||||||
// Initialize token address to index mapping
|
// Initialize token address to index mapping
|
||||||
for (uint i = 0; i < n;) {
|
for (uint i = 0; i < n;) {
|
||||||
tokenAddressToIndexPlusOne[_tokens[i]] = i + 1;
|
tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
|
||||||
unchecked {i++;}
|
unchecked {i++;}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,14 +479,6 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
return (amountInUsedUint, amountOutUint, feeUint);
|
return (amountInUsedUint, amountOutUint, feeUint);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
|
|
||||||
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
|
|
||||||
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
|
||||||
if (feePpm == 0) return 0;
|
|
||||||
// ceil division: (num + denom - 1) / denom
|
|
||||||
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application
|
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application
|
||||||
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored.
|
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored.
|
||||||
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
||||||
@@ -539,7 +507,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
require(lmsr.nAssets > 0, "swap: empty pool");
|
require(lmsr.nAssets > 0, "swap: empty pool");
|
||||||
|
|
||||||
// Estimate max net input (fee on gross rounded up, then subtract)
|
// Estimate max net input (fee on gross rounded up, then subtract)
|
||||||
(, uint256 netUintForSwap) = _computeFee(maxAmountIn);
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
// Convert to internal (floor)
|
// Convert to internal (floor)
|
||||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
||||||
@@ -637,9 +605,10 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
|
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
|
||||||
// which is already present in the standard Mint/Burn events.
|
// which is already present in the standard Mint/Burn events.
|
||||||
|
|
||||||
|
// todo swapMintAmounts and burnSwapAmounts
|
||||||
|
|
||||||
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
/// @dev This function forwards the call to the swapMint implementation via delegatecall
|
||||||
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
|
||||||
/// @param payer who transfers the input token
|
/// @param payer who transfers the input token
|
||||||
/// @param receiver who receives the minted LP tokens
|
/// @param receiver who receives the minted LP tokens
|
||||||
/// @param inputTokenIndex index of the input token
|
/// @param inputTokenIndex index of the input token
|
||||||
@@ -652,95 +621,23 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 lpMinted) {
|
) external returns (uint256 lpMinted) {
|
||||||
uint256 n = tokens.length;
|
bytes memory data = abi.encodeWithSignature(
|
||||||
require(inputTokenIndex < n, "swapMint: idx");
|
"swapMint(address,address,uint256,uint256,uint256,uint256)",
|
||||||
require(maxAmountIn > 0, "swapMint: input zero");
|
payer,
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
receiver,
|
||||||
|
inputTokenIndex,
|
||||||
|
maxAmountIn,
|
||||||
|
deadline,
|
||||||
|
swapFeePpm
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure pool initialized
|
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
||||||
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
return abi.decode(result, (uint256));
|
||||||
|
|
||||||
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
|
||||||
(, uint256 netUintGuess) = _computeFee(maxAmountIn);
|
|
||||||
|
|
||||||
// Convert the net guess to internal (floor)
|
|
||||||
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
|
|
||||||
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
|
|
||||||
|
|
||||||
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
|
||||||
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
|
||||||
|
|
||||||
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
|
||||||
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
||||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
|
||||||
|
|
||||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
|
||||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
|
||||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
|
||||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
|
||||||
|
|
||||||
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer)
|
|
||||||
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
|
||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
||||||
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
|
||||||
|
|
||||||
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
|
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
|
||||||
|
|
||||||
// Compute old and new scaled size metrics to determine LP minted
|
|
||||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
require(oldTotal > int128(0), "swapMint: zero total");
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
|
||||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
|
|
||||||
uint256 actualLpToMint;
|
|
||||||
if (totalSupply() == 0) {
|
|
||||||
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
|
||||||
actualLpToMint = newScaled;
|
|
||||||
} else {
|
|
||||||
require(oldScaled > 0, "swapMint: oldScaled zero");
|
|
||||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
|
||||||
if (delta > 0) {
|
|
||||||
// floor truncation rounds in favor of pool
|
|
||||||
actualLpToMint = (totalSupply() * delta) / oldScaled;
|
|
||||||
} else {
|
|
||||||
actualLpToMint = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require(actualLpToMint > 0, "swapMint: zero LP minted");
|
|
||||||
|
|
||||||
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
|
|
||||||
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached internal and kappa via updateForProportionalChange
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
|
|
||||||
// Note: we updated cachedUintBalances[inputTokenIndex] above via reading balance; other token uint balances did not
|
|
||||||
// change externally (they were not transferred in). We keep cachedUintBalances for others unchanged.
|
|
||||||
// Mint LP tokens to receiver
|
|
||||||
_mint(receiver, actualLpToMint);
|
|
||||||
|
|
||||||
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
|
|
||||||
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
|
|
||||||
|
|
||||||
// Emit standard Mint event which records deposit amounts and LP minted
|
|
||||||
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
|
||||||
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
|
||||||
|
|
||||||
return actualLpToMint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
/// @dev This function forwards the call to the burnSwap implementation via delegatecall
|
||||||
/// @param payer who burns LP tokens
|
/// @param payer who burns LP tokens
|
||||||
/// @param receiver who receives the single asset
|
/// @param receiver who receives the single asset
|
||||||
/// @param lpAmount amount of LP tokens to burn
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
@@ -753,61 +650,19 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
uint256 lpAmount,
|
uint256 lpAmount,
|
||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external nonReentrant returns (uint256 amountOutUint) {
|
) external returns (uint256 amountOutUint) {
|
||||||
uint256 n = tokens.length;
|
bytes memory data = abi.encodeWithSignature(
|
||||||
require(inputTokenIndex < n, "burnSwap: idx");
|
"burnSwap(address,address,uint256,uint256,uint256,uint256)",
|
||||||
require(lpAmount > 0, "burnSwap: zero lp");
|
payer,
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
receiver,
|
||||||
|
lpAmount,
|
||||||
|
inputTokenIndex,
|
||||||
|
deadline,
|
||||||
|
swapFeePpm
|
||||||
|
);
|
||||||
|
|
||||||
uint256 supply = totalSupply();
|
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
||||||
require(supply > 0, "burnSwap: empty supply");
|
return abi.decode(result, (uint256));
|
||||||
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
|
||||||
|
|
||||||
// alpha = lpAmount / supply as Q64.64
|
|
||||||
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
|
|
||||||
|
|
||||||
// Use LMSR view to compute single-asset payout and burned size-metric
|
|
||||||
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
|
||||||
|
|
||||||
// Convert payoutInternal -> uint (floor) to favor pool
|
|
||||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
|
||||||
require(amountOutUint > 0, "burnSwap: output zero");
|
|
||||||
|
|
||||||
// Transfer the payout to receiver
|
|
||||||
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
||||||
|
|
||||||
// Burn LP tokens from payer (authorization via allowance)
|
|
||||||
if (msg.sender != payer) {
|
|
||||||
uint256 allowed = allowance(payer, msg.sender);
|
|
||||||
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
|
||||||
}
|
|
||||||
_burn(payer, lpAmount);
|
|
||||||
|
|
||||||
// Update cached balances by reading on-chain balances for all tokens
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[idx] = bal;
|
|
||||||
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
|
|
||||||
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
|
|
||||||
|
|
||||||
// If entire pool drained, deinit; else update proportionally
|
|
||||||
bool allZero = true;
|
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
|
||||||
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
|
||||||
}
|
|
||||||
if (allZero) {
|
|
||||||
lmsr.deinit();
|
|
||||||
} else {
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
|
||||||
return amountOutUint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -831,6 +686,8 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
/// @param recipient The address which will receive the token amounts
|
/// @param recipient The address which will receive the token amounts
|
||||||
/// @param amounts The amount of each token to send (array length must equal pool size)
|
/// @param amounts The amount of each token to send (array length must equal pool size)
|
||||||
/// @param data Any data to be passed through to the callback
|
/// @param data Any data to be passed through to the callback
|
||||||
|
// todo gas-efficient single-asset flash
|
||||||
|
// todo fix this func's gas
|
||||||
function flash(
|
function flash(
|
||||||
address recipient,
|
address recipient,
|
||||||
uint256[] memory amounts,
|
uint256[] memory amounts,
|
||||||
@@ -890,37 +747,7 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------
|
/* Conversion helpers moved to PartyPoolBase (abstract) to centralize internal helpers and storage. */
|
||||||
Conversion helpers
|
|
||||||
---------------------- */
|
|
||||||
|
|
||||||
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
|
|
||||||
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
|
||||||
// internal = amount / base (as Q64.64)
|
|
||||||
return ABDKMath64x64.divu(amount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
|
|
||||||
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// uint = internal * base (floored)
|
|
||||||
return ABDKMath64x64.mulu(internalAmount, base);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
|
|
||||||
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
|
||||||
// Get the floor value first
|
|
||||||
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
|
||||||
|
|
||||||
// Check if there was any fractional part by comparing to a reconstruction of the original
|
|
||||||
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
|
||||||
|
|
||||||
// If reconstructed is less than original, there was a fractional part that was truncated
|
|
||||||
if (reconstructed < internalAmount) {
|
|
||||||
return floorValue + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return floorValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
||||||
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
||||||
@@ -958,15 +785,4 @@ contract PartyPool is IPartyPool, ERC20, ReentrancyGuard {
|
|||||||
return pricePerQ.mul(factor);
|
return pricePerQ.mul(factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
|
||||||
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
|
|
||||||
function _computeSizeMetric(int128[] memory qInternal_) private pure returns (int128) {
|
|
||||||
int128 total = int128(0);
|
|
||||||
for (uint i = 0; i < qInternal_.length; ) {
|
|
||||||
total = total.add(qInternal_[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/PartyPoolBase.sol
Normal file
118
src/PartyPoolBase.sol
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
|
||||||
|
/// @notice Abstract base contract that contains storage and internal helpers only.
|
||||||
|
/// No external/public functions or constructor here — derived implementations own immutables and constructors.
|
||||||
|
abstract contract PartyPoolBase is ERC20, ReentrancyGuard {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Internal state (no immutables here; immutables belong to derived contracts)
|
||||||
|
//
|
||||||
|
|
||||||
|
// LMSR internal state
|
||||||
|
LMSRStabilized.State internal lmsr;
|
||||||
|
|
||||||
|
/// @notice Scale factor used when converting LMSR Q64.64 totals to LP token units (uint).
|
||||||
|
/// @dev LP tokens are minted in units equal to ABDK.mulu(lastTotalQ64x64, LP_SCALE).
|
||||||
|
uint256 internal constant LP_SCALE = 1e18; // Scale used to convert LMSR lastTotal (Q64.64) into LP token units (uint)
|
||||||
|
|
||||||
|
/// @notice Token addresses comprising the pool. Effectively immutable after construction.
|
||||||
|
/// @dev tokens[i] corresponds to the i-th asset and maps to index i in the internal LMSR arrays.
|
||||||
|
IERC20[] internal tokens; // effectively immutable since there is no interface to change the tokens
|
||||||
|
|
||||||
|
/// @notice Per-token uint base denominators used to convert uint token amounts <-> internal Q64.64 representation.
|
||||||
|
/// @dev denominators()[i] is the base for tokens[i]. These bases are chosen by deployer and must match token decimals.
|
||||||
|
uint256[] internal bases; // per-token uint base used to scale token amounts <-> internal
|
||||||
|
|
||||||
|
/// @notice Mapping from token address => (index+1). A zero value indicates the token is not in the pool.
|
||||||
|
/// @dev Use index = tokenAddressToIndexPlusOne[token] - 1 when non-zero.
|
||||||
|
mapping(IERC20=>uint) internal tokenAddressToIndexPlusOne; // Uses index+1 so a result of 0 indicates a failed lookup
|
||||||
|
|
||||||
|
// Cached on-chain balances (uint) and internal 64.64 representation
|
||||||
|
// balance / base = internal
|
||||||
|
uint256[] internal cachedUintBalances;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {}
|
||||||
|
|
||||||
|
|
||||||
|
/* ----------------------
|
||||||
|
Conversion & fee helpers (internal)
|
||||||
|
---------------------- */
|
||||||
|
|
||||||
|
// Convert uint token amount -> internal 64.64 (floor). Uses ABDKMath64x64.divu which truncates.
|
||||||
|
function _uintToInternalFloor(uint256 amount, uint256 base) internal pure returns (int128) {
|
||||||
|
// internal = amount / base (as Q64.64)
|
||||||
|
return ABDKMath64x64.divu(amount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (floor). Uses ABDKMath64x64.mulu which floors the product.
|
||||||
|
function _internalToUintFloor(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// uint = internal * base (floored)
|
||||||
|
return ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert internal 64.64 -> uint token amount (ceiling). Rounds up to protect the pool.
|
||||||
|
function _internalToUintCeil(int128 internalAmount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// Get the floor value first
|
||||||
|
uint256 floorValue = ABDKMath64x64.mulu(internalAmount, base);
|
||||||
|
|
||||||
|
// Check if there was any fractional part by comparing to a reconstruction of the original
|
||||||
|
int128 reconstructed = ABDKMath64x64.divu(floorValue, base);
|
||||||
|
|
||||||
|
// If reconstructed is less than original, there was a fractional part that was truncated
|
||||||
|
if (reconstructed < internalAmount) {
|
||||||
|
return floorValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return floorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Ceiling fee helper: computes ceil(x * feePpm / 1_000_000)
|
||||||
|
/// @dev Internal helper; public-facing functions use this to ensure fees round up in favor of pool.
|
||||||
|
function _ceilFee(uint256 x, uint256 feePpm) internal pure returns (uint256) {
|
||||||
|
if (feePpm == 0) return 0;
|
||||||
|
// ceil division: (num + denom - 1) / denom
|
||||||
|
return (x * feePpm + 1_000_000 - 1) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
|
||||||
|
/// @param gross total gross input
|
||||||
|
/// @param feePpm fee in ppm to apply
|
||||||
|
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
|
||||||
|
function _computeFee(uint256 gross, uint256 feePpm) internal pure returns (uint256 feeUint, uint256 netUint) {
|
||||||
|
if (feePpm == 0) {
|
||||||
|
return (0, gross);
|
||||||
|
}
|
||||||
|
feeUint = _ceilFee(gross, feePpm);
|
||||||
|
netUint = gross - feeUint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
||||||
|
/// @param netUint net amount
|
||||||
|
/// @param feePpm fee in ppm to apply
|
||||||
|
function _addFee(uint256 netUint, uint256 feePpm) internal pure returns (uint256 gross) {
|
||||||
|
if (feePpm == 0) return netUint;
|
||||||
|
uint256 fee = _ceilFee(netUint, feePpm);
|
||||||
|
return netUint + fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Helper to compute size metric (sum of all asset quantities) from internal balances
|
||||||
|
/// @dev Returns the sum of all provided qInternal_ entries as a Q64.64 value.
|
||||||
|
function _computeSizeMetric(int128[] memory qInternal_) internal pure returns (int128) {
|
||||||
|
int128 total = int128(0);
|
||||||
|
for (uint i = 0; i < qInternal_.length; ) {
|
||||||
|
total = total.add(qInternal_[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/PartyPoolSwapMintImpl.sol
Normal file
203
src/PartyPoolSwapMintImpl.sol
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "./PartyPoolBase.sol";
|
||||||
|
import "./LMSRStabilized.sol";
|
||||||
|
|
||||||
|
/// @title PartyPoolSwapMintImpl - Implementation contract for swapMint and burnSwap functions
|
||||||
|
/// @notice This contract contains the swapMint and burnSwap implementation that will be called via delegatecall
|
||||||
|
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
|
||||||
|
contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
// Events that mirror the main contract events
|
||||||
|
event SwapMint(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 totalTransfer, uint256 amountInUint, uint256 feeUintActual);
|
||||||
|
event BurnSwap(address indexed payer, address indexed receiver, uint256 indexed inputTokenIndex, uint256 amountOutUint);
|
||||||
|
event Mint(address indexed payer, address indexed receiver, uint256[] depositAmounts, uint256 lpMinted);
|
||||||
|
event Burn(address indexed payer, address indexed receiver, uint256[] withdrawAmounts, uint256 lpBurned);
|
||||||
|
|
||||||
|
constructor() PartyPoolBase('','') {}
|
||||||
|
|
||||||
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
||||||
|
/// @dev swapMint executes as an exact-in planned swap followed by proportional scaling of qInternal.
|
||||||
|
/// The function emits SwapMint (gross, net, fee) and also emits Mint for LP issuance.
|
||||||
|
/// @param payer who transfers the input token
|
||||||
|
/// @param receiver who receives the minted LP tokens
|
||||||
|
/// @param inputTokenIndex index of the input token
|
||||||
|
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
||||||
|
/// @param deadline optional deadline
|
||||||
|
/// @param swapFeePpm fee in parts-per-million for this pool
|
||||||
|
/// @return lpMinted actual LP minted (uint)
|
||||||
|
function swapMint(
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) external returns (uint256 lpMinted) {
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
require(inputTokenIndex < n, "swapMint: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMint: input zero");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
||||||
|
|
||||||
|
// Ensure pool initialized
|
||||||
|
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
||||||
|
|
||||||
|
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
||||||
|
(, uint256 netUintGuess) = _computeFee(maxAmountIn, swapFeePpm);
|
||||||
|
|
||||||
|
// Convert the net guess to internal (floor)
|
||||||
|
int128 netInternalGuess = _uintToInternalFloor(netUintGuess, bases[inputTokenIndex]);
|
||||||
|
require(netInternalGuess > int128(0), "swapMint: input too small after fee");
|
||||||
|
|
||||||
|
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
||||||
|
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) = lmsr.swapAmountsForMint(inputTokenIndex, netInternalGuess);
|
||||||
|
|
||||||
|
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
||||||
|
uint256 amountInUint = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
||||||
|
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
||||||
|
|
||||||
|
// Compute fee on the actual used input and total transfer amount (ceiling)
|
||||||
|
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||||
|
uint256 totalTransfer = amountInUint + feeUintActual;
|
||||||
|
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
||||||
|
|
||||||
|
// Record pre-balance and transfer tokens from payer, require exact receipt (revert on fee-on-transfer)
|
||||||
|
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransfer);
|
||||||
|
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||||
|
|
||||||
|
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
|
||||||
|
cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
|
||||||
|
// Compute old and new scaled size metrics to determine LP minted
|
||||||
|
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
||||||
|
require(oldTotal > int128(0), "swapMint: zero total");
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
// Use natural ERC20 function since base contract inherits from ERC20
|
||||||
|
uint256 currentSupply = totalSupply();
|
||||||
|
if (currentSupply == 0) {
|
||||||
|
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
||||||
|
actualLpToMint = newScaled;
|
||||||
|
} else {
|
||||||
|
require(oldScaled > 0, "swapMint: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
// floor truncation rounds in favor of pool
|
||||||
|
actualLpToMint = (currentSupply * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(actualLpToMint > 0, "swapMint: zero LP minted");
|
||||||
|
|
||||||
|
// Update LMSR internal state: scale qInternal proportionally by newTotal/oldTotal
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
// newQInternal[idx] = qInternal[idx] * (newTotal / oldTotal)
|
||||||
|
newQInternal[idx] = lmsr.qInternal[idx].mul(newTotal).div(oldTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached internal and kappa via updateForProportionalChange
|
||||||
|
lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
// Use natural ERC20 function since base contract inherits from ERC20
|
||||||
|
_mint(receiver, actualLpToMint);
|
||||||
|
|
||||||
|
// Emit SwapMint event with gross transfer, net input and fee (planned exact-in)
|
||||||
|
emit SwapMint(payer, receiver, inputTokenIndex, totalTransfer, amountInUint, feeUintActual);
|
||||||
|
|
||||||
|
// Emit standard Mint event which records deposit amounts and LP minted
|
||||||
|
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||||
|
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
||||||
|
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
||||||
|
/// @dev The function burns LP tokens (authorization via allowance if needed), sends the single-asset payout and updates LMSR state.
|
||||||
|
/// @param payer who burns LP tokens
|
||||||
|
/// @param receiver who receives the single asset
|
||||||
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
|
/// @param inputTokenIndex index of target asset to receive
|
||||||
|
/// @param deadline optional deadline
|
||||||
|
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
|
||||||
|
/// @return amountOutUint uint amount of asset i sent to receiver
|
||||||
|
// todo fee!?
|
||||||
|
function burnSwap(
|
||||||
|
address payer,
|
||||||
|
address receiver,
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 deadline,
|
||||||
|
uint256 swapFeePpm
|
||||||
|
) external returns (uint256 amountOutUint) {
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
require(inputTokenIndex < n, "burnSwap: idx");
|
||||||
|
require(lpAmount > 0, "burnSwap: zero lp");
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burnSwap: deadline");
|
||||||
|
|
||||||
|
uint256 supply = totalSupply();
|
||||||
|
require(supply > 0, "burnSwap: empty supply");
|
||||||
|
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
||||||
|
|
||||||
|
// alpha = lpAmount / supply as Q64.64
|
||||||
|
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
|
||||||
|
|
||||||
|
// Use LMSR view to compute single-asset payout and burned size-metric
|
||||||
|
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
||||||
|
|
||||||
|
// Convert payoutInternal -> uint (floor) to favor pool
|
||||||
|
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
||||||
|
require(amountOutUint > 0, "burnSwap: output zero");
|
||||||
|
|
||||||
|
// Transfer the payout to receiver
|
||||||
|
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
|
||||||
|
// Burn LP tokens from payer (authorization via allowance)
|
||||||
|
if (msg.sender != payer) {
|
||||||
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
|
require(allowed >= lpAmount, "burnSwap: allowance insufficient");
|
||||||
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
|
}
|
||||||
|
_burn(payer, lpAmount);
|
||||||
|
|
||||||
|
// Update cached balances by reading on-chain balances for all tokens
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
||||||
|
cachedUintBalances[idx] = bal;
|
||||||
|
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit BurnSwap with public-facing info only (do not expose ΔS or LP burned)
|
||||||
|
emit BurnSwap(payer, receiver, inputTokenIndex, amountOutUint);
|
||||||
|
|
||||||
|
// If entire pool drained, deinit; else update proportionally
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
|
if (newQInternal[idx] != int128(0)) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
if (allZero) {
|
||||||
|
lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||||
|
return amountOutUint;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import "@abdk/ABDKMath64x64.sol";
|
|||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
|
import "../src/PartyPlanner.sol";
|
||||||
// Import the flash callback interface
|
|
||||||
import "../src/IPartyFlashCallback.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
|
import {Deploy} from "../src/Deploy.sol";
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IPartyFlashCallback {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
@@ -130,6 +130,7 @@ contract TestERC20 is ERC20 {
|
|||||||
contract GasTest is Test {
|
contract GasTest is Test {
|
||||||
using ABDKMath64x64 for int128;
|
using ABDKMath64x64 for int128;
|
||||||
|
|
||||||
|
PartyPlanner planner;
|
||||||
PartyPool pool2;
|
PartyPool pool2;
|
||||||
PartyPool pool10;
|
PartyPool pool10;
|
||||||
PartyPool pool20;
|
PartyPool pool20;
|
||||||
@@ -172,7 +173,7 @@ contract GasTest is Test {
|
|||||||
}
|
}
|
||||||
// Compute kappa from slippage params and number of tokens, then construct pool with kappa
|
// Compute kappa from slippage params and number of tokens, then construct pool with kappa
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);
|
PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint
|
// Transfer initial deposit amounts into pool before initial mint
|
||||||
for (uint256 i = 0; i < numTokens; i++) {
|
for (uint256 i = 0; i < numTokens; i++) {
|
||||||
@@ -212,7 +213,7 @@ contract GasTest is Test {
|
|||||||
ierc20Tokens[i] = IERC20(tokens[i]);
|
ierc20Tokens[i] = IERC20(tokens[i]);
|
||||||
}
|
}
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(ierc20Tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool newPool = new PartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);
|
PartyPool newPool = Deploy.newPartyPool(poolName, poolName, ierc20Tokens, bases, computedKappa, feePpm, feePpm, true);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint
|
// Transfer initial deposit amounts into pool before initial mint
|
||||||
for (uint256 i = 0; i < numTokens; i++) {
|
for (uint256 i = 0; i < numTokens; i++) {
|
||||||
@@ -229,6 +230,8 @@ contract GasTest is Test {
|
|||||||
alice = address(0xA11ce);
|
alice = address(0xA11ce);
|
||||||
bob = address(0xB0b);
|
bob = address(0xB0b);
|
||||||
|
|
||||||
|
planner = Deploy.newPartyPlanner();
|
||||||
|
|
||||||
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
|
// Configure LMSR parameters similar to other tests: trade size 1% of asset -> 0.01, slippage 0.001
|
||||||
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
|
tradeFrac = ABDKMath64x64.divu(100, 10_000); // 0.01
|
||||||
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
|
targetSlippage = ABDKMath64x64.divu(10, 10_000); // 0.001
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "forge-std/Test.sol";
|
|||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/PartyPlanner.sol";
|
import "../src/PartyPlanner.sol";
|
||||||
import "../src/PartyPool.sol";
|
import "../src/PartyPool.sol";
|
||||||
|
import {Deploy} from "../src/Deploy.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
// Mock ERC20 token for testing
|
// Mock ERC20 token for testing
|
||||||
@@ -38,7 +39,7 @@ contract PartyPlannerTest is Test {
|
|||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
// Deploy PartyPlanner
|
// Deploy PartyPlanner
|
||||||
planner = new PartyPlanner();
|
planner = Deploy.newPartyPlanner();
|
||||||
|
|
||||||
// Deploy mock tokens
|
// Deploy mock tokens
|
||||||
tokenA = new MockERC20("Token A", "TKNA", 18);
|
tokenA = new MockERC20("Token A", "TKNA", 18);
|
||||||
@@ -87,7 +88,7 @@ contract PartyPlannerTest is Test {
|
|||||||
// Compute kappa then create pool via kappa overload
|
// Compute kappa then create pool via kappa overload
|
||||||
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 computedKappa = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
|
|
||||||
(PartyPool pool, uint256 lpAmount) = planner.createPool(
|
(PartyPool pool, uint256 lpAmount) = planner.newPool(
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
tokens,
|
tokens,
|
||||||
@@ -166,7 +167,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
|
deposits1[1] = INITIAL_DEPOSIT_AMOUNT;
|
||||||
|
|
||||||
int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappa1 = LMSRStabilized.computeKappaFromSlippage(tokens1.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(PartyPool pool1,) = planner.createPool(
|
(PartyPool pool1,) = planner.newPool(
|
||||||
"Pool 1", "LP1", tokens1, bases1,
|
"Pool 1", "LP1", tokens1, bases1,
|
||||||
kappa1, 3000, 5000, false,
|
kappa1, 3000, 5000, false,
|
||||||
payer, receiver, deposits1, 1000e18, 0
|
payer, receiver, deposits1, 1000e18, 0
|
||||||
@@ -186,7 +187,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
|
deposits2[1] = INITIAL_DEPOSIT_AMOUNT / 1e12; // Adjust for 6 decimals
|
||||||
|
|
||||||
int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappa2 = LMSRStabilized.computeKappaFromSlippage(tokens2.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(PartyPool pool2,) = planner.createPool(
|
(PartyPool pool2,) = planner.newPool(
|
||||||
"Pool 2", "LP2", tokens2, bases2,
|
"Pool 2", "LP2", tokens2, bases2,
|
||||||
kappa2, 3000, 5000, false,
|
kappa2, 3000, 5000, false,
|
||||||
payer, receiver, deposits2, 1000e18, 0
|
payer, receiver, deposits2, 1000e18, 0
|
||||||
@@ -230,7 +231,7 @@ contract PartyPlannerTest is Test {
|
|||||||
// Test token/deposit length mismatch
|
// Test token/deposit length mismatch
|
||||||
vm.expectRevert("Planner: tokens and deposits length mismatch");
|
vm.expectRevert("Planner: tokens and deposits length mismatch");
|
||||||
// call old-signature convenience (it will still exist) for the mismatched-length revert check
|
// call old-signature convenience (it will still exist) for the mismatched-length revert check
|
||||||
planner.createPool(
|
planner.newPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
|
int128((1 << 64) - 1), int128(1 << 62), 3000, 5000, false,
|
||||||
payer, receiver, deposits, 1000e18, 0
|
payer, receiver, deposits, 1000e18, 0
|
||||||
@@ -244,7 +245,7 @@ contract PartyPlannerTest is Test {
|
|||||||
int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaErr = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
|
|
||||||
vm.expectRevert("Planner: payer cannot be zero address");
|
vm.expectRevert("Planner: payer cannot be zero address");
|
||||||
planner.createPool(
|
planner.newPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaErr, 3000, 5000, false,
|
kappaErr, 3000, 5000, false,
|
||||||
address(0), receiver, validDeposits, 1000e18, 0
|
address(0), receiver, validDeposits, 1000e18, 0
|
||||||
@@ -252,7 +253,7 @@ contract PartyPlannerTest is Test {
|
|||||||
|
|
||||||
// Test zero receiver address
|
// Test zero receiver address
|
||||||
vm.expectRevert("Planner: receiver cannot be zero address");
|
vm.expectRevert("Planner: receiver cannot be zero address");
|
||||||
planner.createPool(
|
planner.newPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaErr, 3000, 5000, false,
|
kappaErr, 3000, 5000, false,
|
||||||
payer, address(0), validDeposits, 1000e18, 0
|
payer, address(0), validDeposits, 1000e18, 0
|
||||||
@@ -263,7 +264,7 @@ contract PartyPlannerTest is Test {
|
|||||||
int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaDeadline = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
vm.warp(1000);
|
vm.warp(1000);
|
||||||
vm.expectRevert("Planner: deadline exceeded");
|
vm.expectRevert("Planner: deadline exceeded");
|
||||||
planner.createPool(
|
planner.newPool(
|
||||||
"Test Pool", "TESTLP", tokens, bases,
|
"Test Pool", "TESTLP", tokens, bases,
|
||||||
kappaDeadline, 3000, 5000, false,
|
kappaDeadline, 3000, 5000, false,
|
||||||
payer, receiver, validDeposits, 1000e18, block.timestamp - 1
|
payer, receiver, validDeposits, 1000e18, block.timestamp - 1
|
||||||
@@ -289,7 +290,7 @@ contract PartyPlannerTest is Test {
|
|||||||
deposits[1] = INITIAL_DEPOSIT_AMOUNT;
|
deposits[1] = INITIAL_DEPOSIT_AMOUNT;
|
||||||
|
|
||||||
int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
int128 kappaLoop = LMSRStabilized.computeKappaFromSlippage(tokens.length, int128((1 << 64) - 1), int128(1 << 62));
|
||||||
(PartyPool pool,) = planner.createPool(
|
(PartyPool pool,) = planner.newPool(
|
||||||
string(abi.encodePacked("Pool ", vm.toString(i))),
|
string(abi.encodePacked("Pool ", vm.toString(i))),
|
||||||
string(abi.encodePacked("LP", vm.toString(i))),
|
string(abi.encodePacked("LP", vm.toString(i))),
|
||||||
tokens, bases,
|
tokens, bases,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import "../src/PartyPool.sol";
|
|||||||
|
|
||||||
// Import the flash callback interface
|
// Import the flash callback interface
|
||||||
import "../src/IPartyFlashCallback.sol";
|
import "../src/IPartyFlashCallback.sol";
|
||||||
|
import {PartyPlanner} from "../src/PartyPlanner.sol";
|
||||||
|
import {Deploy} from "../src/Deploy.sol";
|
||||||
|
|
||||||
/// @notice Test contract that implements the flash callback for testing flash loans
|
/// @notice Test contract that implements the flash callback for testing flash loans
|
||||||
contract FlashBorrower is IPartyFlashCallback {
|
contract FlashBorrower is IPartyFlashCallback {
|
||||||
@@ -137,6 +139,7 @@ contract PartyPoolTest is Test {
|
|||||||
TestERC20 token7;
|
TestERC20 token7;
|
||||||
TestERC20 token8;
|
TestERC20 token8;
|
||||||
TestERC20 token9;
|
TestERC20 token9;
|
||||||
|
PartyPlanner planner;
|
||||||
PartyPool pool;
|
PartyPool pool;
|
||||||
PartyPool pool10;
|
PartyPool pool10;
|
||||||
|
|
||||||
@@ -151,6 +154,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
|
planner = Deploy.newPartyPlanner();
|
||||||
alice = address(0xA11ce);
|
alice = address(0xA11ce);
|
||||||
bob = address(0xB0b);
|
bob = address(0xB0b);
|
||||||
|
|
||||||
@@ -197,7 +201,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 feePpm = 1000;
|
uint256 feePpm = 1000;
|
||||||
|
|
||||||
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappa3 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
pool = new PartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
|
pool = Deploy.newPartyPool("LP", "LP", tokens, bases, kappa3, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
|
// Transfer initial deposit amounts into pool before initial mint (pool expects tokens already in contract)
|
||||||
// We deposit equal amounts INIT_BAL for each token
|
// We deposit equal amounts INIT_BAL for each token
|
||||||
@@ -227,7 +231,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
|
int128 kappa10 = LMSRStabilized.computeKappaFromSlippage(tokens10.length, tradeFrac, targetSlippage);
|
||||||
pool10 = new PartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
|
pool10 = Deploy.newPartyPool("LP10", "LP10", tokens10, bases10, kappa10, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens for pool10 initial deposit
|
// Mint additional tokens for pool10 initial deposit
|
||||||
token0.mint(address(this), INIT_BAL);
|
token0.mint(address(this), INIT_BAL);
|
||||||
@@ -1231,11 +1235,11 @@ contract PartyPoolTest is Test {
|
|||||||
|
|
||||||
// Pool with default initialization (lpTokens = 0)
|
// Pool with default initialization (lpTokens = 0)
|
||||||
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaDefault = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
|
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Pool with custom initialization (lpTokens = custom amount)
|
// Pool with custom initialization (lpTokens = custom amount)
|
||||||
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaCustom = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
|
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens for both pools
|
// Mint additional tokens for both pools
|
||||||
token0.mint(address(this), INIT_BAL * 2);
|
token0.mint(address(this), INIT_BAL * 2);
|
||||||
@@ -1307,9 +1311,9 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 feePpm = 1000;
|
uint256 feePpm = 1000;
|
||||||
|
|
||||||
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaDefault2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolDefault = new PartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
|
PartyPool poolDefault = Deploy.newPartyPool("LP_DEFAULT", "LP_DEFAULT", tokens, bases, kappaDefault2, feePpm, feePpm, false);
|
||||||
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
int128 kappaCustom2 = LMSRStabilized.computeKappaFromSlippage(tokens.length, tradeFrac, targetSlippage);
|
||||||
PartyPool poolCustom = new PartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
|
PartyPool poolCustom = Deploy.newPartyPool("LP_CUSTOM", "LP_CUSTOM", tokens, bases, kappaCustom2, feePpm, feePpm, false);
|
||||||
|
|
||||||
// Mint additional tokens
|
// Mint additional tokens
|
||||||
token0.mint(address(this), INIT_BAL * 4);
|
token0.mint(address(this), INIT_BAL * 4);
|
||||||
@@ -1329,7 +1333,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
|
uint256 lpDefault = poolDefault.initialMint(address(this), 0);
|
||||||
uint256 scaleFactor = 3;
|
uint256 scaleFactor = 3;
|
||||||
uint256 customLpAmount = lpDefault * scaleFactor;
|
uint256 customLpAmount = lpDefault * scaleFactor;
|
||||||
uint256 lpCustom = poolCustom.initialMint(address(this), customLpAmount);
|
poolCustom.initialMint(address(this), customLpAmount);
|
||||||
|
|
||||||
// Verify initial LP supplies
|
// Verify initial LP supplies
|
||||||
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
|
assertEq(poolDefault.totalSupply(), lpDefault, "Default pool should have default LP supply");
|
||||||
|
|||||||
Reference in New Issue
Block a user