Compare commits
10 Commits
9cac58013b
...
c002d26daf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c002d26daf | ||
|
|
b7e1b1cac2 | ||
|
|
5a2e7039d1 | ||
|
|
a6f6fd034c | ||
|
|
d46e60f83c | ||
|
|
3a5f0842b3 | ||
|
|
40e1d25e72 | ||
|
|
5ce14ab2e1 | ||
|
|
91e6a916ac | ||
|
|
28b9474363 |
@@ -9,10 +9,10 @@ remappings = [
|
|||||||
optimizer=true
|
optimizer=true
|
||||||
optimizer_runs=999999999
|
optimizer_runs=999999999
|
||||||
viaIR=true
|
viaIR=true
|
||||||
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolViewImpl']
|
gas_reports = ['PartyPool', 'PartyPlanner', 'PartyPoolSwapMintImpl', 'PartyPoolMintImpl',]
|
||||||
fs_permissions = [{ access = "write", path = "chain.json"}]
|
fs_permissions = [{ access = "write", path = "chain.json"}]
|
||||||
|
|
||||||
[lint]
|
[lint]
|
||||||
exclude_lints=['mixed-case-variable', 'unaliased-plain-import', ]
|
lint_on_build=false # more annoying than helpful
|
||||||
|
|
||||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import "forge-std/console2.sol";
|
|||||||
|
|
||||||
contract DeployMock is Script {
|
contract DeployMock is Script {
|
||||||
|
|
||||||
address constant devAccount0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
|
address constant public DEV_ACCOUNT_0 = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
|
||||||
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
|
// private key 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
|
||||||
address constant devAccount7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
|
address constant public DEV_ACCOUNT_7 = 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955;
|
||||||
|
|
||||||
function run() public {
|
function run() public {
|
||||||
vm.startBroadcast();
|
vm.startBroadcast();
|
||||||
@@ -69,14 +69,14 @@ contract DeployMock is Script {
|
|||||||
_feePpm,
|
_feePpm,
|
||||||
false,
|
false,
|
||||||
msg.sender, // payer: this script
|
msg.sender, // payer: this script
|
||||||
devAccount7, // receiver of initial LP
|
DEV_ACCOUNT_7, // receiver of initial LP
|
||||||
initialDeposits,
|
initialDeposits,
|
||||||
initialLpAmount,
|
initialLpAmount,
|
||||||
deadline
|
deadline
|
||||||
);
|
);
|
||||||
|
|
||||||
// give tokens to dev7 for later use
|
// give tokens to dev7 for later use
|
||||||
mintAll(devAccount7, 1_000_000);
|
mintAll(DEV_ACCOUNT_7, 1_000_000);
|
||||||
|
|
||||||
vm.stopBroadcast();
|
vm.stopBroadcast();
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ pragma solidity ^0.8.30;
|
|||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import {PartyPool} from "./PartyPool.sol";
|
import {PartyPool} from "./PartyPool.sol";
|
||||||
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
||||||
import {PartyPlanner} from "./PartyPlanner.sol";
|
import {PartyPlanner} from "./PartyPlanner.sol";
|
||||||
|
|
||||||
library Deploy {
|
library Deploy {
|
||||||
|
|
||||||
function newPartyPlanner() internal returns (PartyPlanner) {
|
function newPartyPlanner() internal returns (PartyPlanner) {
|
||||||
return new PartyPlanner(
|
return new PartyPlanner(
|
||||||
new PartyPoolSwapMintImpl()
|
new PartyPoolSwapMintImpl(),
|
||||||
|
new PartyPoolMintImpl(),
|
||||||
|
0, // protocolFeePpm = 0 for deploy helper
|
||||||
|
address(0) // protocolFeeAddress = address(0) for deploy helper
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +28,23 @@ library Deploy {
|
|||||||
uint256 _flashFeePpm,
|
uint256 _flashFeePpm,
|
||||||
bool _stable
|
bool _stable
|
||||||
) internal returns (PartyPool) {
|
) internal returns (PartyPool) {
|
||||||
return new PartyPool(name_, symbol_, tokens_, bases_, _kappa, _swapFeePpm, _flashFeePpm, _stable,
|
// default protocol fee/off parameters (per your instruction) - set to 0 / address(0)
|
||||||
new PartyPoolSwapMintImpl()
|
uint256 protocolFeePpm = 0;
|
||||||
|
address protocolAddr = address(0);
|
||||||
|
|
||||||
|
return new PartyPool(
|
||||||
|
name_,
|
||||||
|
symbol_,
|
||||||
|
tokens_,
|
||||||
|
bases_,
|
||||||
|
_kappa,
|
||||||
|
_swapFeePpm,
|
||||||
|
_flashFeePpm,
|
||||||
|
protocolFeePpm,
|
||||||
|
protocolAddr,
|
||||||
|
_stable,
|
||||||
|
new PartyPoolSwapMintImpl(),
|
||||||
|
new PartyPoolMintImpl()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ interface IPartyPlanner {
|
|||||||
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
|
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
||||||
function swapMintImpl() external view returns (address);
|
function mintImpl() external view returns (PartyPoolMintImpl);
|
||||||
|
|
||||||
|
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
||||||
|
function swapMintImpl() external view returns (PartyPoolSwapMintImpl);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,19 +75,36 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @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.
|
||||||
function flashFeePpm() external view returns (uint256);
|
function flashFeePpm() external view returns (uint256);
|
||||||
|
|
||||||
|
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
|
||||||
|
/// @dev This is the fraction (in ppm) of the pool-collected fees that are owed to the protocol.
|
||||||
|
function protocolFeePpm() external view returns (uint256);
|
||||||
|
|
||||||
|
/// @notice Address that will receive collected protocol tokens when collectProtocolFees() is called.
|
||||||
|
function protocolFeeAddress() external view returns (address);
|
||||||
|
|
||||||
|
/// @notice Per-token protocol fee ledger accessor. Returns tokens owed (raw uint token units) for token index i.
|
||||||
|
function protocolFeesOwed(uint256) external view returns (uint256);
|
||||||
|
|
||||||
/// @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 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);
|
||||||
|
|
||||||
// Initialization / Mint / Burn (LP token managed)
|
// Initialization / Mint / Burn (LP token managed)
|
||||||
|
|
||||||
|
/// @notice Initial mint to set up pool for the first time.
|
||||||
|
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
|
||||||
|
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
|
||||||
|
/// @param receiver address that receives the LP tokens
|
||||||
|
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
||||||
|
function initialMint(address receiver, uint256 lpTokens) external returns (uint256 lpMinted);
|
||||||
|
|
||||||
/// @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
|
||||||
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
|
/// @dev Returns the minimum token amounts (rounded up) that must be supplied to receive lpTokenAmount
|
||||||
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
|
/// LP tokens at current pool proportions. If the pool is empty (initial deposit) returns zeros
|
||||||
/// because the initial deposit is handled by transferring tokens then calling mint().
|
/// because the initial deposit is handled by transferring tokens then calling mint().
|
||||||
/// @param lpTokenAmount The amount of LP tokens desired
|
/// @param lpTokenAmount The amount of LP tokens desired
|
||||||
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
/// @return depositAmounts Array of token amounts to deposit (rounded up)
|
||||||
function mintDepositAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
|
function mintAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory depositAmounts);
|
||||||
|
|
||||||
/// @notice Proportional mint (or initial supply if first call).
|
/// @notice Proportional mint (or initial supply if first call).
|
||||||
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
|
/// @dev - For initial supply: assumes tokens have already been transferred to the pool prior to calling.
|
||||||
@@ -105,7 +122,7 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// If the pool is uninitialized or supply is zero, returns zeros.
|
/// If the pool is uninitialized or supply is zero, returns zeros.
|
||||||
/// @param lpTokenAmount The amount of LP tokens to burn
|
/// @param lpTokenAmount The amount of LP tokens to burn
|
||||||
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
|
/// @return withdrawAmounts Array of token amounts to withdraw (rounded down)
|
||||||
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
|
function burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts);
|
||||||
|
|
||||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
||||||
@@ -114,7 +131,7 @@ interface IPartyPool is IERC20Metadata {
|
|||||||
/// @param receiver address that receives the withdrawn tokens
|
/// @param receiver address that receives the withdrawn tokens
|
||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external;
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external returns (uint256[] memory withdrawAmounts);
|
||||||
|
|
||||||
|
|
||||||
// Swaps
|
// Swaps
|
||||||
|
|||||||
@@ -64,8 +64,15 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
/// @notice Cost C(q) = b * (M + ln(Z))
|
/// @notice Cost C(q) = b * (M + ln(Z))
|
||||||
function cost(State storage s) internal view returns (int128) {
|
function cost(State storage s) internal view returns (int128) {
|
||||||
int128 b = _computeB(s);
|
return cost(s.kappa, s.qInternal);
|
||||||
(int128 M, int128 Z) = _computeMAndZ(b, s.qInternal);
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Cost C(q) = b * (M + ln(Z))
|
||||||
|
function cost(int128 kappa, int128[] memory qInternal) internal pure returns (int128) {
|
||||||
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
|
(int128 M, int128 Z) = _computeMAndZ(b, qInternal);
|
||||||
int128 lnZ = _ln(Z);
|
int128 lnZ = _ln(Z);
|
||||||
int128 inner = M.add(lnZ);
|
int128 inner = M.add(lnZ);
|
||||||
int128 c = b.mul(inner);
|
int128 c = b.mul(inner);
|
||||||
@@ -102,23 +109,58 @@ library LMSRStabilized {
|
|||||||
int128 a,
|
int128 a,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
|
return swapAmountsForExactInput(s.nAssets, s.kappa, s.qInternal, i, j, a, limitPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Closed-form asset-i -> asset-j amountOut in 64.64 fixed-point format (fee-free kernel)
|
||||||
|
/// Uses the closed-form two-asset LMSR formula (no fees in kernel):
|
||||||
|
/// y = b * ln(1 + r0 * (1 - exp(-a / b)))
|
||||||
|
/// where r0 = e_i / e_j.
|
||||||
|
///
|
||||||
|
/// This variant accepts an additional `limitPrice` (64.64) which represents the
|
||||||
|
/// maximum acceptable marginal price (p_i / p_j). If the marginal price would
|
||||||
|
/// exceed `limitPrice` before the requested `a` is fully consumed, the input
|
||||||
|
/// `a` is truncated to the value that makes the marginal price equal `limitPrice`.
|
||||||
|
///
|
||||||
|
/// NOTE: Kernel is fee-free; fees should be handled by the wrapper/token layer.
|
||||||
|
///
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param i Index of input asset
|
||||||
|
/// @param j Index of output asset
|
||||||
|
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
|
||||||
|
/// @param limitPrice Maximum acceptable price ratio (64.64). If <= current price, this call reverts.
|
||||||
|
/// @return amountIn Actual amount of input asset used (may be less than `a` if limited by price)
|
||||||
|
/// @return amountOut Amount of output asset j in 64.64 fixed-point format
|
||||||
|
function swapAmountsForExactInput(
|
||||||
|
uint256 nAssets,
|
||||||
|
int128 kappa,
|
||||||
|
int128[] memory qInternal,
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
int128 a,
|
||||||
|
int128 limitPrice
|
||||||
|
) internal pure returns (int128 amountIn, int128 amountOut) {
|
||||||
|
require(i < nAssets && j < nAssets, "LMSR: idx");
|
||||||
|
|
||||||
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
|
// Initialize amountIn to full amount (will be adjusted if limit price is hit)
|
||||||
amountIn = a;
|
amountIn = a;
|
||||||
|
|
||||||
// Compute b and ensure positivity before deriving invB
|
// Compute b and ensure positivity before deriving invB
|
||||||
int128 b = _computeB(s);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Precompute reciprocal of b to avoid repeated divisions
|
// Precompute reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||||
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
|
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||||
|
|
||||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||||
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
||||||
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
|
require(r0 > int128(0), "LMSR: r0<=0"); // equivalent to e_j > 0 check
|
||||||
|
|
||||||
// If a positive limitPrice is given, determine whether the full `a` would
|
// If a positive limitPrice is given, determine whether the full `a` would
|
||||||
@@ -203,7 +245,7 @@ library LMSRStabilized {
|
|||||||
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
// If inner <= 0 then cap output to the current balance q_j (cannot withdraw more than q_j)
|
||||||
if (inner <= int128(0)) {
|
if (inner <= int128(0)) {
|
||||||
console2.log("WARNING: inner <= 0, capping output to balance q_j");
|
console2.log("WARNING: inner <= 0, capping output to balance q_j");
|
||||||
int128 qj64 = s.qInternal[j];
|
int128 qj64 = qInternal[j];
|
||||||
console2.log("Capped output (64.64):");
|
console2.log("Capped output (64.64):");
|
||||||
console2.logInt(qj64);
|
console2.logInt(qj64);
|
||||||
return (amountIn, qj64);
|
return (amountIn, qj64);
|
||||||
@@ -249,21 +291,48 @@ library LMSRStabilized {
|
|||||||
uint256 j,
|
uint256 j,
|
||||||
int128 limitPrice
|
int128 limitPrice
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
require(i < s.nAssets && j < s.nAssets, "LMSR: idx");
|
return swapAmountsForPriceLimit(s.nAssets, s.kappa, s.qInternal, i, j, limitPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Maximum input/output pair possible when swapping from asset i to asset j
|
||||||
|
/// given a maximum acceptable price ratio (p_i/p_j).
|
||||||
|
/// Returns the input amount that would drive the marginal price to the limit (amountIn)
|
||||||
|
/// and the corresponding output amount (amountOut). If the output would exceed the
|
||||||
|
/// j-balance, amountOut is capped and amountIn is solved for the capped output.
|
||||||
|
///
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param i Index of input asset
|
||||||
|
/// @param j Index of output asset
|
||||||
|
/// @param limitPrice Maximum acceptable price ratio (64.64)
|
||||||
|
/// @return amountIn Maximum input amount in 64.64 fixed-point format that reaches the price limit
|
||||||
|
/// @return amountOut Corresponding maximum output amount in 64.64 fixed-point format
|
||||||
|
function swapAmountsForPriceLimit(
|
||||||
|
uint256 nAssets,
|
||||||
|
int128 kappa,
|
||||||
|
int128[] memory qInternal,
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
int128 limitPrice
|
||||||
|
) internal pure returns (int128 amountIn, int128 amountOut) {
|
||||||
|
require(i < nAssets && j < nAssets, "LMSR: idx");
|
||||||
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
|
require(limitPrice > int128(0), "LMSR: limitPrice <= 0");
|
||||||
|
|
||||||
// Compute b and ensure positivity before deriving invB
|
// Compute b and ensure positivity before deriving invB
|
||||||
int128 b = _computeB(s);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Precompute reciprocal of b to avoid repeated divisions
|
// Precompute reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
// Guard: output asset must have non-zero effective weight to avoid degenerate/div-by-zero-like conditions
|
||||||
require(s.qInternal[j] > int128(0), "LMSR: e_j==0");
|
require(qInternal[j] > int128(0), "LMSR: e_j==0");
|
||||||
|
|
||||||
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
// Compute r0 = exp((q_i - q_j) / b) directly using invB
|
||||||
int128 r0 = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
int128 r0 = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
||||||
|
|
||||||
console2.log("\n=== Max Input/Output Calculation ===");
|
console2.log("\n=== Max Input/Output Calculation ===");
|
||||||
console2.log("Limit price (64x64):");
|
console2.log("Limit price (64x64):");
|
||||||
@@ -316,7 +385,7 @@ library LMSRStabilized {
|
|||||||
console2.logInt(maxOutput);
|
console2.logInt(maxOutput);
|
||||||
|
|
||||||
// Current balance of asset j (in 64.64)
|
// Current balance of asset j (in 64.64)
|
||||||
int128 qj64 = s.qInternal[j];
|
int128 qj64 = qInternal[j];
|
||||||
console2.log("Current j balance (64.64):");
|
console2.log("Current j balance (64.64):");
|
||||||
console2.logInt(qj64);
|
console2.logInt(qj64);
|
||||||
|
|
||||||
@@ -366,20 +435,45 @@ library LMSRStabilized {
|
|||||||
uint256 i,
|
uint256 i,
|
||||||
int128 a
|
int128 a
|
||||||
) internal view returns (int128 amountIn, int128 amountOut) {
|
) internal view returns (int128 amountIn, int128 amountOut) {
|
||||||
require(i < s.nAssets, "LMSR: idx");
|
return swapAmountsForMint(s.nAssets, s.kappa, s.qInternal, i, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Compute LP-size increase when minting from a single-token input using bisection only.
|
||||||
|
/// @dev Solve for α >= 0 such that:
|
||||||
|
/// a = α*q_i + sum_{j != i} x_j(α)
|
||||||
|
/// where x_j(α) is the input to swap i->j that yields y_j = α*q_j and
|
||||||
|
/// x_j = b * ln( r0_j / (r0_j + 1 - exp(y_j / b)) ), r0_j = exp((q_i - q_j)/b).
|
||||||
|
/// Bisection is used (no Newton) to keep implementation compact and gas-friendly.
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param i Index of input asset
|
||||||
|
/// @param a Amount of input asset (in int128 format, 64.64 fixed-point)
|
||||||
|
/// @return amountIn Actual amount of input consumed
|
||||||
|
/// @return amountOut LP size-metric increase (alpha * S)
|
||||||
|
function swapAmountsForMint(
|
||||||
|
uint256 nAssets,
|
||||||
|
int128 kappa,
|
||||||
|
int128[] memory qInternal,
|
||||||
|
uint256 i,
|
||||||
|
int128 a
|
||||||
|
) internal pure returns (int128 amountIn, int128 amountOut) {
|
||||||
|
require(i < nAssets, "LMSR: idx");
|
||||||
require(a > int128(0), "LMSR: amount <= 0");
|
require(a > int128(0), "LMSR: amount <= 0");
|
||||||
|
|
||||||
int128 b = _computeB(s);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
int128 S = _computeSizeMetric(s.qInternal);
|
int128 S = sizeMetric;
|
||||||
|
|
||||||
uint256 n = s.nAssets;
|
uint256 n = nAssets;
|
||||||
|
|
||||||
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
|
// Precompute r0_j = exp((q_i - q_j) / b) for all j to avoid recomputing during search.
|
||||||
int128[] memory r0 = new int128[](n);
|
int128[] memory r0 = new int128[](n);
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
r0[j] = _exp(s.qInternal[i].sub(s.qInternal[j]).mul(invB));
|
r0[j] = _exp(qInternal[i].sub(qInternal[j]).mul(invB));
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +509,7 @@ library LMSRStabilized {
|
|||||||
// loop j != i
|
// loop j != i
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alpha.mul(s.qInternal[j]); // target output y_j = alpha * q_j
|
int128 yj = alpha.mul(qInternal[j]); // target output y_j = alpha * q_j
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
// Guard exp arg
|
// Guard exp arg
|
||||||
@@ -433,7 +527,7 @@ library LMSRStabilized {
|
|||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
|
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
|
||||||
|
|
||||||
if (aReq >= a || high >= alphaCap) {
|
if (aReq >= a || high >= alphaCap) {
|
||||||
break;
|
break;
|
||||||
@@ -455,7 +549,7 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alpha.mul(s.qInternal[j]);
|
int128 yj = alpha.mul(qInternal[j]);
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
if (expArg > EXP_LIMIT) { fail = true; break; }
|
if (expArg > EXP_LIMIT) { fail = true; break; }
|
||||||
@@ -472,7 +566,7 @@ library LMSRStabilized {
|
|||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(s.qInternal[i]).add(sumX);
|
int128 aReq = fail ? int128(type(int128).max) : alpha.mul(qInternal[i]).add(sumX);
|
||||||
|
|
||||||
if (aReq > a) {
|
if (aReq > a) {
|
||||||
// mid requires more input than provided -> decrease alpha
|
// mid requires more input than provided -> decrease alpha
|
||||||
@@ -502,7 +596,7 @@ library LMSRStabilized {
|
|||||||
bool failFinal = false;
|
bool failFinal = false;
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 yj = alphaFinal.mul(s.qInternal[j]);
|
int128 yj = alphaFinal.mul(qInternal[j]);
|
||||||
if (yj > int128(0)) {
|
if (yj > int128(0)) {
|
||||||
int128 expArg = yj.mul(invB);
|
int128 expArg = yj.mul(invB);
|
||||||
if (expArg > EXP_LIMIT) { failFinal = true; break; }
|
if (expArg > EXP_LIMIT) { failFinal = true; break; }
|
||||||
@@ -524,7 +618,7 @@ library LMSRStabilized {
|
|||||||
return (int128(0), int128(0));
|
return (int128(0), int128(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
int128 aRequired = alphaFinal.mul(s.qInternal[i]).add(sumXFinal);
|
int128 aRequired = alphaFinal.mul(qInternal[i]).add(sumXFinal);
|
||||||
|
|
||||||
// amountIn is actual consumed input (may be <= provided a)
|
// amountIn is actual consumed input (may be <= provided a)
|
||||||
amountIn = aRequired;
|
amountIn = aRequired;
|
||||||
@@ -553,28 +647,56 @@ library LMSRStabilized {
|
|||||||
uint256 i,
|
uint256 i,
|
||||||
int128 alpha
|
int128 alpha
|
||||||
) internal view returns (int128 amountOut, int128 amountIn) {
|
) internal view returns (int128 amountOut, int128 amountIn) {
|
||||||
require(i < s.nAssets, "LMSR: idx");
|
return swapAmountsForBurn(s.nAssets, s.kappa, s.qInternal, i, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Compute single-asset payout when burning a proportional share alpha of the pool.
|
||||||
|
/// @dev Simulate q_after = (1 - alpha) * q, return the amount of asset `i` the burner
|
||||||
|
/// would receive after swapping each other asset's withdrawn portion into `i`.
|
||||||
|
/// For each j != i:
|
||||||
|
/// - wrapper holds a_j = alpha * q_j
|
||||||
|
/// - swap j->i with closed-form exact-input formula using the current q_local
|
||||||
|
/// - cap output to q_local[i] when necessary (solve inverse for input used)
|
||||||
|
/// Treat any per-asset rhs<=0 as "this asset contributes zero" (do not revert).
|
||||||
|
/// Revert only if the final single-asset payout is zero.
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param i Index of output asset
|
||||||
|
/// @param alpha Proportional share to burn (0 < alpha <= 1)
|
||||||
|
/// @return amountOut Amount of asset i received (in 64.64 fixed-point)
|
||||||
|
/// @return amountIn LP size-metric redeemed (alpha * S)
|
||||||
|
function swapAmountsForBurn(
|
||||||
|
uint256 nAssets,
|
||||||
|
int128 kappa,
|
||||||
|
int128[] memory qInternal,
|
||||||
|
uint256 i,
|
||||||
|
int128 alpha
|
||||||
|
) internal pure returns (int128 amountOut, int128 amountIn) {
|
||||||
|
require(i < nAssets, "LMSR: idx");
|
||||||
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
|
require(alpha > int128(0) && alpha <= ONE, "LMSR: alpha");
|
||||||
|
|
||||||
int128 b = _computeB(s);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
uint256 n = s.nAssets;
|
uint256 n = nAssets;
|
||||||
|
|
||||||
// Size metric and burned size (amountIn returned)
|
// Size metric and burned size (amountIn returned)
|
||||||
int128 S = _computeSizeMetric(s.qInternal);
|
int128 S = sizeMetric;
|
||||||
amountIn = alpha.mul(S); // total size-metric redeemed
|
amountIn = alpha.mul(S); // total size-metric redeemed
|
||||||
|
|
||||||
// Build q_local := q_after_burn = (1 - alpha) * q
|
// Build q_local := q_after_burn = (1 - alpha) * q
|
||||||
int128[] memory qLocal = new int128[](n);
|
int128[] memory qLocal = new int128[](n);
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
qLocal[j] = s.qInternal[j].mul(ONE.sub(alpha));
|
qLocal[j] = qInternal[j].mul(ONE.sub(alpha));
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start totalOut with direct portion of asset i redeemed
|
// Start totalOut with direct portion of asset i redeemed
|
||||||
int128 totalOut = alpha.mul(s.qInternal[i]);
|
int128 totalOut = alpha.mul(qInternal[i]);
|
||||||
|
|
||||||
// Track whether any non-zero contribution was produced
|
// Track whether any non-zero contribution was produced
|
||||||
bool anyNonZero = (totalOut > int128(0));
|
bool anyNonZero = (totalOut > int128(0));
|
||||||
@@ -582,7 +704,7 @@ library LMSRStabilized {
|
|||||||
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
|
// For each asset j != i, swap the withdrawn a_j := alpha * q_j into i
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
if (j != i) {
|
if (j != i) {
|
||||||
int128 aj = alpha.mul(s.qInternal[j]); // wrapper-held withdrawn amount of j
|
int128 aj = alpha.mul(qInternal[j]); // wrapper-held withdrawn amount of j
|
||||||
if (aj > int128(0)) {
|
if (aj > int128(0)) {
|
||||||
// expArg = aj / b
|
// expArg = aj / b
|
||||||
int128 expArg = aj.mul(invB);
|
int128 expArg = aj.mul(invB);
|
||||||
@@ -722,15 +844,26 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
/// @notice Price-share of asset i: exp(z_i) / Z (64.64)
|
||||||
function priceShare(State storage s, uint256 i) internal view returns (int128) {
|
function priceShare(State storage s, uint256 i) internal view returns (int128) {
|
||||||
int128 b = _computeB(s);
|
return priceShare(s.kappa, s.qInternal, i);
|
||||||
uint len = s.qInternal.length;
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Price-share of asset i: exp(z_i) / Z (64.64)
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param i Index of asset
|
||||||
|
/// @return Price share in 64.64 fixed-point format
|
||||||
|
function priceShare(int128 kappa, int128[] memory qInternal, uint256 i) internal pure returns (int128) {
|
||||||
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
|
uint len = qInternal.length;
|
||||||
require(len > 0, "LMSR: no assets");
|
require(len > 0, "LMSR: no assets");
|
||||||
|
|
||||||
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
|
// Precompute reciprocal of b and perform a single pass that tracks M, Z, and e_i
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Initialize from the first element
|
// Initialize from the first element
|
||||||
int128 M = s.qInternal[0].mul(invB);
|
int128 M = qInternal[0].mul(invB);
|
||||||
int128 Z = ONE; // exp(0)
|
int128 Z = ONE; // exp(0)
|
||||||
int128 e_i_acc;
|
int128 e_i_acc;
|
||||||
bool setEi;
|
bool setEi;
|
||||||
@@ -741,7 +874,7 @@ library LMSRStabilized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (uint idx = 1; idx < len; ) {
|
for (uint idx = 1; idx < len; ) {
|
||||||
int128 yi = s.qInternal[idx].mul(invB);
|
int128 yi = qInternal[idx].mul(invB);
|
||||||
if (yi <= M) {
|
if (yi <= M) {
|
||||||
// Add contribution under current center
|
// Add contribution under current center
|
||||||
int128 term = _exp(yi.sub(M));
|
int128 term = _exp(yi.sub(M));
|
||||||
@@ -770,7 +903,7 @@ library LMSRStabilized {
|
|||||||
if (!setEi) {
|
if (!setEi) {
|
||||||
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
|
// Only possible when len == 1 and i != 0, guarded by caller invariants typically
|
||||||
// Fallback: compute directly (kept for completeness)
|
// Fallback: compute directly (kept for completeness)
|
||||||
int128 yi = s.qInternal[i].mul(invB);
|
int128 yi = qInternal[i].mul(invB);
|
||||||
e_i_acc = _exp(yi.sub(M));
|
e_i_acc = _exp(yi.sub(M));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,27 +913,54 @@ library LMSRStabilized {
|
|||||||
/// @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 exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
||||||
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
|
function price(State storage s, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal view returns (int128) {
|
||||||
require(baseTokenIndex < s.nAssets && quoteTokenIndex < s.nAssets, "LMSR: idx");
|
return price(s.nAssets, s.kappa, s.qInternal, baseTokenIndex, quoteTokenIndex);
|
||||||
int128 b = _computeB(s);
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
||||||
|
/// @dev Returns exp((q_quote - q_base) / b). Indices must be valid and b > 0.
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param baseTokenIndex Index of base token
|
||||||
|
/// @param quoteTokenIndex Index of quote token
|
||||||
|
/// @return Price in 64.64 fixed-point format
|
||||||
|
function price(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 baseTokenIndex, uint256 quoteTokenIndex) internal pure returns (int128) {
|
||||||
|
require(baseTokenIndex < nAssets && quoteTokenIndex < nAssets, "LMSR: idx");
|
||||||
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Use reciprocal of b to avoid repeated divisions
|
// Use reciprocal of b to avoid repeated divisions
|
||||||
int128 invB = ABDKMath64x64.div(ONE, b);
|
int128 invB = ABDKMath64x64.div(ONE, b);
|
||||||
|
|
||||||
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
|
// Marginal price p_quote / p_base = exp((q_quote - q_base) / b)
|
||||||
return _exp(s.qInternal[quoteTokenIndex].sub(s.qInternal[baseTokenIndex]).mul(invB));
|
return _exp(qInternal[quoteTokenIndex].sub(qInternal[baseTokenIndex]).mul(invB));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
/// @notice Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
||||||
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
function poolPrice(State storage s, uint256 quoteTokenIndex) internal view returns (int128) {
|
||||||
require(quoteTokenIndex < s.nAssets, "LMSR: idx");
|
return poolPrice(s.nAssets, s.kappa, s.qInternal, quoteTokenIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version: Price of one unit of the LP size-metric (S = sum q_i) denominated in `quote` asset (Q64.64)
|
||||||
|
/// @dev Computes: poolPrice_quote = (1 / S) * sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
|
/// @param nAssets Number of assets in the pool
|
||||||
|
/// @param kappa Liquidity parameter κ (64.64 fixed point)
|
||||||
|
/// @param qInternal Cached internal balances in 64.64 fixed-point format
|
||||||
|
/// @param quoteTokenIndex Index of quote token
|
||||||
|
/// @return Pool price in 64.64 fixed-point format
|
||||||
|
function poolPrice(uint256 nAssets, int128 kappa, int128[] memory qInternal, uint256 quoteTokenIndex) internal pure returns (int128) {
|
||||||
|
require(quoteTokenIndex < nAssets, "LMSR: idx");
|
||||||
// Compute b and ensure positivity
|
// Compute b and ensure positivity
|
||||||
int128 b = _computeB(s);
|
int128 sizeMetric = _computeSizeMetric(qInternal);
|
||||||
|
require(sizeMetric > int128(0), "LMSR: size metric zero");
|
||||||
|
int128 b = kappa.mul(sizeMetric);
|
||||||
require(b > int128(0), "LMSR: b<=0");
|
require(b > int128(0), "LMSR: b<=0");
|
||||||
|
|
||||||
// Compute total size metric S = sum q_i
|
// Compute total size metric S = sum q_i
|
||||||
int128 S = _computeSizeMetric(s.qInternal);
|
int128 S = sizeMetric;
|
||||||
require(S > int128(0), "LMSR: size zero");
|
require(S > int128(0), "LMSR: size zero");
|
||||||
|
|
||||||
// Precompute reciprocal of b
|
// Precompute reciprocal of b
|
||||||
@@ -808,12 +968,12 @@ library LMSRStabilized {
|
|||||||
|
|
||||||
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
|
// Accumulate weighted exponentials: sum_j q_j * exp((q_j - q_quote) / b)
|
||||||
int128 acc = int128(0);
|
int128 acc = int128(0);
|
||||||
uint256 n = s.nAssets;
|
uint256 n = nAssets;
|
||||||
for (uint256 j = 0; j < n; ) {
|
for (uint256 j = 0; j < n; ) {
|
||||||
// factor = exp((q_j - q_quote) / b)
|
// factor = exp((q_j - q_quote) / b)
|
||||||
int128 factor = _exp(s.qInternal[j].sub(s.qInternal[quoteTokenIndex]).mul(invB));
|
int128 factor = _exp(qInternal[j].sub(qInternal[quoteTokenIndex]).mul(invB));
|
||||||
// term = q_j * factor
|
// term = q_j * factor
|
||||||
int128 term = s.qInternal[j].mul(factor);
|
int128 term = qInternal[j].mul(factor);
|
||||||
acc = acc.add(term);
|
acc = acc.add(term);
|
||||||
unchecked { j++; }
|
unchecked { j++; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,29 @@ 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";
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
||||||
|
|
||||||
/// @title PartyPlanner
|
/// @title PartyPlanner
|
||||||
/// @notice Factory contract for creating and tracking PartyPool instances
|
/// @notice Factory contract for creating and tracking PartyPool instances
|
||||||
contract PartyPlanner is IPartyPlanner {
|
contract PartyPlanner is IPartyPlanner {
|
||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
int128 private constant FIXED_ONE_64x64 = int128(1) << 64;
|
int128 private constant ONE = int128(1) << 64;
|
||||||
|
|
||||||
|
/// @notice Address of the Mint implementation contract used by all pools created by this factory
|
||||||
|
PartyPoolMintImpl private immutable MINT_IMPL;
|
||||||
|
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
|
||||||
|
|
||||||
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
/// @notice Address of the SwapMint implementation contract used by all pools created by this factory
|
||||||
address public immutable swapMintImpl;
|
PartyPoolSwapMintImpl private immutable SWAP_MINT_IMPL;
|
||||||
|
function swapMintImpl() external view returns (PartyPoolSwapMintImpl) { return SWAP_MINT_IMPL; }
|
||||||
|
|
||||||
|
/// @notice Protocol fee share (ppm) applied to fees collected by pools created by this planner
|
||||||
|
uint256 private immutable PROTOCOL_FEE_PPM;
|
||||||
|
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
|
||||||
|
|
||||||
|
/// @notice Address to receive protocol fees for pools created by this planner (may be address(0))
|
||||||
|
address private immutable PROTOCOL_FEE_ADDRESS;
|
||||||
|
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
|
||||||
|
|
||||||
// On-chain pool indexing
|
// On-chain pool indexing
|
||||||
PartyPool[] private _allPools;
|
PartyPool[] private _allPools;
|
||||||
@@ -25,9 +39,23 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
mapping(IERC20 => PartyPool[]) private _poolsByToken;
|
mapping(IERC20 => PartyPool[]) private _poolsByToken;
|
||||||
|
|
||||||
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
|
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
|
||||||
constructor(PartyPoolSwapMintImpl _swapMintImpl) {
|
/// @param _mintImpl address of the Mint implementation contract to be used by all pools
|
||||||
require(address(_swapMintImpl) != address(0), "Planner: impl address cannot be zero");
|
/// @param _protocolFeePpm protocol fee share (ppm) to be used for pools created by this planner
|
||||||
swapMintImpl = address(_swapMintImpl);
|
/// @param _protocolFeeAddress recipient address for protocol fees for pools created by this planner (may be address(0))
|
||||||
|
constructor(
|
||||||
|
PartyPoolSwapMintImpl _swapMintImpl,
|
||||||
|
PartyPoolMintImpl _mintImpl,
|
||||||
|
uint256 _protocolFeePpm,
|
||||||
|
address _protocolFeeAddress
|
||||||
|
) {
|
||||||
|
require(address(_swapMintImpl) != address(0), "Planner: swapMintImpl address cannot be zero");
|
||||||
|
SWAP_MINT_IMPL = _swapMintImpl;
|
||||||
|
require(address(_mintImpl) != address(0), "Planner: mintImpl address cannot be zero");
|
||||||
|
MINT_IMPL = _mintImpl;
|
||||||
|
|
||||||
|
require(_protocolFeePpm < 1_000_000, "Planner: protocol fee >= ppm");
|
||||||
|
PROTOCOL_FEE_PPM = _protocolFeePpm;
|
||||||
|
PROTOCOL_FEE_ADDRESS = _protocolFeeAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main newPool variant: accepts kappa directly (preferred).
|
/// Main newPool variant: accepts kappa directly (preferred).
|
||||||
@@ -66,8 +94,11 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
_kappa,
|
_kappa,
|
||||||
_swapFeePpm,
|
_swapFeePpm,
|
||||||
_flashFeePpm,
|
_flashFeePpm,
|
||||||
|
PROTOCOL_FEE_PPM,
|
||||||
|
PROTOCOL_FEE_ADDRESS,
|
||||||
_stable,
|
_stable,
|
||||||
PartyPoolSwapMintImpl(swapMintImpl)
|
PartyPoolSwapMintImpl(SWAP_MINT_IMPL),
|
||||||
|
MINT_IMPL
|
||||||
);
|
);
|
||||||
|
|
||||||
_allPools.push(pool);
|
_allPools.push(pool);
|
||||||
@@ -122,8 +153,8 @@ contract PartyPlanner is IPartyPlanner {
|
|||||||
uint256 deadline
|
uint256 deadline
|
||||||
) external returns (PartyPool pool, uint256 lpAmount) {
|
) external returns (PartyPool pool, uint256 lpAmount) {
|
||||||
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
|
// Validate fixed-point fractions: must be less than 1.0 in 64.64 fixed-point
|
||||||
require(_tradeFrac < FIXED_ONE_64x64, "Planner: tradeFrac must be < 1 (64.64)");
|
require(_tradeFrac < ONE, "Planner: tradeFrac must be < 1 (64.64)");
|
||||||
require(_targetSlippage < FIXED_ONE_64x64, "Planner: targetSlippage must be < 1 (64.64)");
|
require(_targetSlippage < ONE, "Planner: targetSlippage must be < 1 (64.64)");
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import "./IPartyPool.sol";
|
|||||||
import "./IPartyFlashCallback.sol";
|
import "./IPartyFlashCallback.sol";
|
||||||
import "./PartyPoolBase.sol";
|
import "./PartyPoolBase.sol";
|
||||||
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
||||||
|
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.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.
|
||||||
@@ -34,19 +35,36 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
/// @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.
|
||||||
int128 public immutable kappa; // kappa in Q64.64
|
int128 private immutable KAPPA; // kappa in Q64.64
|
||||||
|
function kappa() external view returns (int128) { return KAPPA; }
|
||||||
|
|
||||||
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
||||||
uint256 public immutable swapFeePpm;
|
uint256 private immutable SWAP_FEE_PPM;
|
||||||
|
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
|
||||||
|
|
||||||
/// @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 private immutable FLASH_FEE_PPM;
|
||||||
|
function flashFeePpm() external view returns (uint256) { return FLASH_FEE_PPM; }
|
||||||
|
|
||||||
|
/// @notice Protocol fee share (ppm) applied to fees collected by the pool (floored when accrued)
|
||||||
|
uint256 private immutable PROTOCOL_FEE_PPM;
|
||||||
|
function protocolFeePpm() external view returns (uint256) { return PROTOCOL_FEE_PPM; }
|
||||||
|
|
||||||
|
/// @notice Address to which collected protocol tokens will be sent on collectProtocolFees()
|
||||||
|
address private immutable PROTOCOL_FEE_ADDRESS;
|
||||||
|
function protocolFeeAddress() external view returns (address) { return PROTOCOL_FEE_ADDRESS; }
|
||||||
|
|
||||||
/// @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 IS_STABLE_PAIR; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
|
||||||
|
|
||||||
|
/// @notice Address of the Mint implementation contract for delegatecall
|
||||||
|
PartyPoolMintImpl private immutable MINT_IMPL;
|
||||||
|
function mintImpl() external view returns (PartyPoolMintImpl) { return MINT_IMPL; }
|
||||||
|
|
||||||
/// @notice Address of the SwapMint implementation contract for delegatecall
|
/// @notice Address of the SwapMint implementation contract for delegatecall
|
||||||
address public immutable swapMintImpl;
|
PartyPoolSwapMintImpl private immutable SWAP_MINT_IMPL;
|
||||||
|
function swapMintImpl() external view returns (PartyPoolSwapMintImpl) { return SWAP_MINT_IMPL; }
|
||||||
|
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
|
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
|
||||||
@@ -64,34 +82,41 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
/// @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
|
/// @param swapMintImpl_ address of the SwapMint implementation contract
|
||||||
|
/// @param mintImpl_ address of the Mint 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,
|
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
|
||||||
PartyPoolSwapMintImpl _swapMintImpl
|
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
|
||||||
|
bool stable_,
|
||||||
|
PartyPoolSwapMintImpl swapMintImpl_,
|
||||||
|
PartyPoolMintImpl mintImpl_
|
||||||
) PartyPoolBase(name_, symbol_) {
|
) PartyPoolBase(name_, symbol_) {
|
||||||
require(tokens_.length > 1, "Pool: need >1 asset");
|
require(tokens_.length > 1, "Pool: need >1 asset");
|
||||||
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
||||||
tokens = tokens_;
|
tokens = tokens_;
|
||||||
bases = bases_;
|
bases = bases_;
|
||||||
kappa = _kappa;
|
KAPPA = kappa_;
|
||||||
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
|
||||||
swapFeePpm = _swapFeePpm;
|
SWAP_FEE_PPM = swapFeePpm_;
|
||||||
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
||||||
flashFeePpm = _flashFeePpm;
|
FLASH_FEE_PPM = flashFeePpm_;
|
||||||
_stablePair = _stable && tokens_.length == 2;
|
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
|
||||||
require(address(_swapMintImpl) != address(0), "Pool: impl address zero");
|
PROTOCOL_FEE_PPM = protocolFeePpm_;
|
||||||
swapMintImpl = address(_swapMintImpl);
|
PROTOCOL_FEE_ADDRESS = protocolFeeAddress_;
|
||||||
|
IS_STABLE_PAIR = stable_ && tokens_.length == 2;
|
||||||
|
SWAP_MINT_IMPL = swapMintImpl_;
|
||||||
|
MINT_IMPL = mintImpl_;
|
||||||
|
|
||||||
uint256 n = tokens_.length;
|
uint256 n = tokens_.length;
|
||||||
|
|
||||||
@@ -104,8 +129,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
unchecked {i++;}
|
unchecked {i++;}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize caches to zero
|
// Initialize caches to zero and protocol ledger
|
||||||
cachedUintBalances = new uint256[](n);
|
cachedUintBalances = new uint256[](n);
|
||||||
|
protocolFeesOwed = new uint256[](n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -114,240 +140,64 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
---------------------- */
|
---------------------- */
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
depositAmounts = new uint256[](n);
|
|
||||||
|
|
||||||
// If this is the first mint or pool is empty, return zeros
|
|
||||||
// For first mint, tokens should already be transferred to the pool
|
|
||||||
if (totalSupply() == 0 || lmsr.nAssets == 0) {
|
|
||||||
return depositAmounts; // Return zeros, initial deposit handled differently
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate deposit based on current proportions
|
|
||||||
uint256 totalLpSupply = totalSupply();
|
|
||||||
|
|
||||||
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
|
|
||||||
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
|
|
||||||
// We round up to protect the pool
|
|
||||||
for (uint i = 0; i < n; i++) {
|
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
|
||||||
// Calculate with rounding up: (a * b + c - 1) / c
|
|
||||||
depositAmounts[i] = (lpTokenAmount * currentBalance + totalLpSupply - 1) / totalLpSupply;
|
|
||||||
}
|
|
||||||
|
|
||||||
return depositAmounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @notice Initial mint to set up pool for the first time.
|
|
||||||
/// @dev Assumes tokens have already been transferred to the pool prior to calling.
|
|
||||||
/// Can only be called when the pool is uninitialized (totalSupply() == 0 or lmsr.nAssets == 0).
|
|
||||||
/// @param receiver address that receives the LP tokens
|
|
||||||
/// @param lpTokens The number of LP tokens to issue for this mint. If 0, then the number of tokens returned will equal the LMSR internal q total
|
|
||||||
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
|
function initialMint(address receiver, uint256 lpTokens) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
uint256 n = tokens.length;
|
bytes memory data = abi.encodeWithSignature(
|
||||||
|
"initialMint(address,uint256,int128)",
|
||||||
|
receiver,
|
||||||
|
lpTokens,
|
||||||
|
KAPPA
|
||||||
|
);
|
||||||
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
||||||
|
return abi.decode(result, (uint256));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is initial deposit - revert if not
|
/// @inheritdoc IPartyPool
|
||||||
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
function mintAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
||||||
require(isInitialDeposit, "initialMint: pool already initialized");
|
return MINT_IMPL.mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
|
||||||
|
|
||||||
// Update cached balances for all assets
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
uint256[] memory depositAmounts = new uint256[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
|
||||||
depositAmounts[i] = bal;
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the stabilized LMSR state with provided kappa
|
|
||||||
lmsr.init(newQInternal, kappa);
|
|
||||||
|
|
||||||
// Compute actual LP tokens to mint based on size metric (scaled)
|
|
||||||
if( lpTokens != 0 )
|
|
||||||
lpMinted = lpTokens;
|
|
||||||
else {
|
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
|
||||||
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
}
|
|
||||||
|
|
||||||
require(lpMinted > 0, "initialMint: zero LP amount");
|
|
||||||
_mint(receiver, lpMinted);
|
|
||||||
emit Mint(address(0), receiver, depositAmounts, lpMinted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Proportional mint for existing pool.
|
/// @notice Proportional mint for existing pool.
|
||||||
/// @dev Payer must approve the required token amounts before calling.
|
/// @dev This function forwards the call to the mint implementation via delegatecall
|
||||||
/// Can only be called when pool is already initialized (totalSupply() > 0 and lmsr.nAssets > 0).
|
|
||||||
/// Rounds follow the pool-favorable conventions documented in helpers (ceil inputs, floor outputs).
|
|
||||||
/// @param payer address that provides the input tokens
|
/// @param payer address that provides the input tokens
|
||||||
/// @param receiver address that receives the LP tokens
|
/// @param receiver address that receives the LP tokens
|
||||||
/// @param lpTokenAmount desired amount of LP tokens to mint
|
/// @param lpTokenAmount desired amount of LP tokens to mint
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external nonReentrant
|
||||||
returns (uint256 lpMinted) {
|
returns (uint256 lpMinted) {
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
bytes memory data = abi.encodeWithSignature(
|
||||||
uint256 n = tokens.length;
|
"mint(address,address,uint256,uint256)",
|
||||||
|
payer,
|
||||||
// Check if this is NOT initial deposit - revert if it is
|
receiver,
|
||||||
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
lpTokenAmount,
|
||||||
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
deadline
|
||||||
require(lpTokenAmount > 0, "mint: zero LP amount");
|
);
|
||||||
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
||||||
// Capture old pool size metric (scaled) by computing from current balances
|
return abi.decode(result, (uint256));
|
||||||
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
|
||||||
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
|
||||||
|
|
||||||
// Calculate required deposit amounts for the desired LP tokens
|
|
||||||
uint256[] memory depositAmounts = mintDepositAmounts(lpTokenAmount);
|
|
||||||
|
|
||||||
// Transfer in all token amounts
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (depositAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances for all assets
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update for proportional change
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
|
|
||||||
// Compute actual LP tokens to mint based on change in size metric (scaled)
|
|
||||||
// floor truncation rounds in favor of the pool
|
|
||||||
int128 newTotal = _computeSizeMetric(newQInternal);
|
|
||||||
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
|
||||||
uint256 actualLpToMint;
|
|
||||||
|
|
||||||
require(oldScaled > 0, "mint: oldScaled zero");
|
|
||||||
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
|
||||||
// Proportional issuance: totalSupply * delta / oldScaled
|
|
||||||
if (delta > 0) {
|
|
||||||
// floor truncation rounds in favor of the pool
|
|
||||||
actualLpToMint = (totalSupply() * delta) / oldScaled;
|
|
||||||
} else {
|
|
||||||
actualLpToMint = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the calculated LP amount is not too different from requested
|
|
||||||
require(actualLpToMint > 0, "mint: zero LP minted");
|
|
||||||
|
|
||||||
// Allow actual amount to be at most 0.00001% less than requested
|
|
||||||
// This accounts for rounding in deposit calculations
|
|
||||||
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
|
||||||
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
|
||||||
|
|
||||||
_mint(receiver, actualLpToMint);
|
|
||||||
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
|
||||||
return actualLpToMint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @inheritdoc IPartyPool
|
/// @inheritdoc IPartyPool
|
||||||
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
function burnAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
||||||
return _burnReceiveAmounts(lpTokenAmount);
|
return MINT_IMPL.burnAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
|
||||||
}
|
|
||||||
|
|
||||||
function _burnReceiveAmounts(uint256 lpTokenAmount) internal view returns (uint256[] memory withdrawAmounts) {
|
|
||||||
uint256 n = tokens.length;
|
|
||||||
withdrawAmounts = new uint256[](n);
|
|
||||||
|
|
||||||
// If supply is zero or pool uninitialized, return zeros
|
|
||||||
if (totalSupply() == 0 || lmsr.nAssets == 0) {
|
|
||||||
return withdrawAmounts; // Return zeros, nothing to withdraw
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate withdrawal amounts based on current proportions
|
|
||||||
uint256 totalLpSupply = totalSupply();
|
|
||||||
|
|
||||||
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
|
|
||||||
for (uint i = 0; i < n; i++) {
|
|
||||||
uint256 currentBalance = cachedUintBalances[i];
|
|
||||||
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalLpSupply;
|
|
||||||
}
|
|
||||||
|
|
||||||
return withdrawAmounts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
/// @dev This function forwards the call to the burn implementation via delegatecall
|
||||||
/// proportionally to reflect the reduced pool size after the withdrawal.
|
|
||||||
/// @param payer address that provides the LP tokens to burn
|
/// @param payer address that provides the LP tokens to burn
|
||||||
/// @param receiver address that receives the withdrawn tokens
|
/// @param receiver address that receives the withdrawn tokens
|
||||||
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant {
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external nonReentrant
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
returns (uint256[] memory withdrawAmounts) {
|
||||||
uint256 n = tokens.length;
|
bytes memory data = abi.encodeWithSignature(
|
||||||
require(lpAmount > 0, "burn: zero lp");
|
"burn(address,address,uint256,uint256)",
|
||||||
|
payer,
|
||||||
uint256 supply = totalSupply();
|
receiver,
|
||||||
require(supply > 0, "burn: empty supply");
|
lpAmount,
|
||||||
require(lmsr.nAssets > 0, "burn: uninit pool");
|
deadline
|
||||||
require(balanceOf(payer) >= lpAmount, "burn: insufficient LP");
|
);
|
||||||
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
||||||
// Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts
|
return abi.decode(result, (uint256[]));
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
|
|
||||||
uint256[] memory withdrawAmounts = _burnReceiveAmounts(lpAmount);
|
|
||||||
|
|
||||||
// Transfer underlying tokens out to receiver according to computed proportions
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (withdrawAmounts[i] > 0) {
|
|
||||||
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cached balances and internal q for all assets
|
|
||||||
int128[] memory newQInternal = new int128[](n);
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
|
||||||
cachedUintBalances[i] = bal;
|
|
||||||
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply proportional update or deinitialize if drained
|
|
||||||
bool allZero = true;
|
|
||||||
for (uint i = 0; i < n; ) {
|
|
||||||
if (newQInternal[i] != int128(0)) {
|
|
||||||
allZero = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unchecked { i++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allZero) {
|
|
||||||
lmsr.deinit();
|
|
||||||
} else {
|
|
||||||
lmsr.updateForProportionalChange(newQInternal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Burn exactly the requested LP amount from payer (authorization via allowance)
|
|
||||||
if (msg.sender != payer) {
|
|
||||||
uint256 allowed = allowance(payer, msg.sender);
|
|
||||||
require(allowed >= lpAmount, "burn: allowance insufficient");
|
|
||||||
_approve(payer, msg.sender, allowed - lpAmount);
|
|
||||||
}
|
|
||||||
_burn(payer, lpAmount);
|
|
||||||
|
|
||||||
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------
|
/* ----------------------
|
||||||
@@ -376,6 +226,36 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Per-token owed protocol fees (raw token units). Public getter autogenerated.
|
||||||
|
uint256[] public protocolFeesOwed;
|
||||||
|
|
||||||
|
/// @notice Transfer all protocol fees to the configured protocolFeeAddress and zero the ledger.
|
||||||
|
/// @dev Anyone can call; must have protocolFeeAddress != address(0) to be operational.
|
||||||
|
function collectProtocolFees() external nonReentrant {
|
||||||
|
address dest = PROTOCOL_FEE_ADDRESS;
|
||||||
|
require(dest != address(0), "collect: zero addr");
|
||||||
|
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
for (uint256 i = 0; i < n; i++) {
|
||||||
|
uint256 owed = protocolFeesOwed[i];
|
||||||
|
if (owed == 0) continue;
|
||||||
|
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
require(bal >= owed, "collect: fee > bal");
|
||||||
|
protocolFeesOwed[i] = 0;
|
||||||
|
// transfer owed tokens to protocol destination
|
||||||
|
tokens[i].safeTransfer(dest, owed);
|
||||||
|
// update cached to effective onchain minus owed
|
||||||
|
cachedUintBalances[i] = bal - owed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Helper to record cached balances as effectiveBalance = onchain - owed. Reverts if owed > onchain.
|
||||||
|
function _recordCachedBalance(uint256 idx, uint256 onchainBal) internal {
|
||||||
|
uint256 owed = protocolFeesOwed[idx];
|
||||||
|
require(onchainBal >= owed, "balance < protocol owed");
|
||||||
|
cachedUintBalances[idx] = onchainBal - owed;
|
||||||
|
}
|
||||||
|
|
||||||
/// @notice Swap input token i -> token j. Payer must approve token i.
|
/// @notice Swap input token i -> token j. Payer must approve token i.
|
||||||
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
|
/// @dev This function transfers the exact gross input (including fee) from payer and sends the computed output to receiver.
|
||||||
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
|
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
|
||||||
@@ -419,9 +299,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
||||||
|
|
||||||
// Update cached uint balances for i and j using actual balances
|
// Accrue protocol share (floor) from the fee on input token
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||||
cachedUintBalances[outputTokenIndex] = balJAfter;
|
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
||||||
|
if (protoShare > 0) {
|
||||||
|
protocolFeesOwed[inputTokenIndex] += protoShare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached uint balances for i and j using effective balances (onchain - owed)
|
||||||
|
_recordCachedBalance(inputTokenIndex, balIAfter);
|
||||||
|
_recordCachedBalance(outputTokenIndex, balJAfter);
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts actually used
|
// Apply swap to LMSR state with the internal amounts actually used
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||||
@@ -466,9 +354,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
||||||
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
||||||
|
|
||||||
// Update caches to actual balances
|
// Accrue protocol share (floor) from the fee on input token
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||||
cachedUintBalances[outputTokenIndex] = balJAfter;
|
uint256 protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
||||||
|
if (protoShare > 0) {
|
||||||
|
protocolFeesOwed[inputTokenIndex] += protoShare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update caches to effective balances
|
||||||
|
_recordCachedBalance(inputTokenIndex, balIAfter);
|
||||||
|
_recordCachedBalance(outputTokenIndex, balJAfter);
|
||||||
|
|
||||||
// Apply swap to LMSR state with the internal amounts
|
// Apply swap to LMSR state with the internal amounts
|
||||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
||||||
@@ -507,7 +403,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
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, swapFeePpm);
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
|
||||||
|
|
||||||
// Convert to internal (floor)
|
// Convert to internal (floor)
|
||||||
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
||||||
@@ -515,9 +411,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
|
|
||||||
// Compute internal amounts using LMSR (exact-input with price limit)
|
// Compute internal amounts using LMSR (exact-input with price limit)
|
||||||
// if _stablePair is true, use the optimized path
|
// if _stablePair is true, use the optimized path
|
||||||
console2.log('stablepair optimization?', _stablePair);
|
console2.log('stablepair optimization?', IS_STABLE_PAIR);
|
||||||
(amountInInternalUsed, amountOutInternal) =
|
(amountInInternalUsed, amountOutInternal) =
|
||||||
_stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
|
IS_STABLE_PAIR ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
|
||||||
: lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
: lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
||||||
|
|
||||||
// Convert actual used input internal -> uint (ceil)
|
// Convert actual used input internal -> uint (ceil)
|
||||||
@@ -527,8 +423,8 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
// Compute gross transfer including fee on the used input (ceil)
|
// Compute gross transfer including fee on the used input (ceil)
|
||||||
feeUint = 0;
|
feeUint = 0;
|
||||||
grossIn = amountInUintNoFee;
|
grossIn = amountInUintNoFee;
|
||||||
if (swapFeePpm > 0) {
|
if (SWAP_FEE_PPM > 0) {
|
||||||
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
|
||||||
grossIn += feeUint;
|
grossIn += feeUint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,8 +471,8 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
|
|
||||||
feeUint = 0;
|
feeUint = 0;
|
||||||
grossIn = amountInUintNoFee;
|
grossIn = amountInUintNoFee;
|
||||||
if (swapFeePpm > 0) {
|
if (SWAP_FEE_PPM > 0) {
|
||||||
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
|
||||||
grossIn += feeUint;
|
grossIn += feeUint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,25 +483,43 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
|
/// @notice Compute fee and net amounts for a gross input (fee rounded up to favor the pool).
|
||||||
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
|
/// @return feeUint fee taken (uint) and netUint remaining for protocol use (uint)
|
||||||
function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) {
|
function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) {
|
||||||
if (swapFeePpm == 0) {
|
if (SWAP_FEE_PPM == 0) {
|
||||||
return (0, gross);
|
return (0, gross);
|
||||||
}
|
}
|
||||||
feeUint = _ceilFee(gross, swapFeePpm);
|
feeUint = _ceilFee(gross, SWAP_FEE_PPM);
|
||||||
netUint = gross - feeUint;
|
netUint = gross - feeUint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
||||||
function _addFee(uint256 netUint) internal view returns (uint256 gross) {
|
function _addFee(uint256 netUint) internal view returns (uint256 gross) {
|
||||||
if (swapFeePpm == 0) return netUint;
|
if (SWAP_FEE_PPM == 0) return netUint;
|
||||||
uint256 fee = _ceilFee(netUint, swapFeePpm);
|
uint256 fee = _ceilFee(netUint, SWAP_FEE_PPM);
|
||||||
return netUint + fee;
|
return netUint + fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- New events for single-token mint/burn flows ---
|
function swapMintAmounts(uint256 inputTokenIndex, uint256 maxAmountIn) external view
|
||||||
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
|
returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
||||||
// which is already present in the standard Mint/Burn events.
|
return SWAP_MINT_IMPL.swapMintAmounts(
|
||||||
|
inputTokenIndex,
|
||||||
|
maxAmountIn,
|
||||||
|
SWAP_FEE_PPM,
|
||||||
|
lmsr,
|
||||||
|
bases,
|
||||||
|
totalSupply()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// todo swapMintAmounts and burnSwapAmounts
|
function burnSwapAmounts(uint256 lpAmount, uint256 inputTokenIndex) external view
|
||||||
|
returns (uint256 amountOut) {
|
||||||
|
return SWAP_MINT_IMPL.burnSwapAmounts(
|
||||||
|
lpAmount,
|
||||||
|
inputTokenIndex,
|
||||||
|
SWAP_FEE_PPM,
|
||||||
|
lmsr,
|
||||||
|
bases,
|
||||||
|
totalSupply()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// @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 This function forwards the call to the swapMint implementation via delegatecall
|
/// @dev This function forwards the call to the swapMint implementation via delegatecall
|
||||||
@@ -629,11 +543,26 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
inputTokenIndex,
|
inputTokenIndex,
|
||||||
maxAmountIn,
|
maxAmountIn,
|
||||||
deadline,
|
deadline,
|
||||||
swapFeePpm
|
SWAP_FEE_PPM
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data);
|
||||||
return abi.decode(result, (uint256));
|
// New ABI: implementation returns (uint256 lpMinted, uint256 feeUintActual)
|
||||||
|
(uint256 lpOut, uint256 feeUintActual) = abi.decode(result, (uint256, uint256));
|
||||||
|
|
||||||
|
// Accrue protocol share (floor) from the fee on the input token
|
||||||
|
if (PROTOCOL_FEE_PPM > 0 && feeUintActual > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||||
|
uint256 protoShare = (feeUintActual * PROTOCOL_FEE_PPM) / 1_000_000;
|
||||||
|
if (protoShare > 0) {
|
||||||
|
protocolFeesOwed[inputTokenIndex] += protoShare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balance for the input token to effective onchain - owed
|
||||||
|
uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
_recordCachedBalance(inputTokenIndex, bal);
|
||||||
|
|
||||||
|
return lpOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @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.
|
||||||
@@ -658,11 +587,26 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
lpAmount,
|
lpAmount,
|
||||||
inputTokenIndex,
|
inputTokenIndex,
|
||||||
deadline,
|
deadline,
|
||||||
swapFeePpm
|
SWAP_FEE_PPM
|
||||||
);
|
);
|
||||||
|
|
||||||
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
bytes memory result = Address.functionDelegateCall(address(SWAP_MINT_IMPL), data);
|
||||||
return abi.decode(result, (uint256));
|
// New ABI: implementation returns (uint256 amountOutUint, uint256 feeTokenUint)
|
||||||
|
(uint256 outAmt, uint256 feeTokenUint) = abi.decode(result, (uint256, uint256));
|
||||||
|
|
||||||
|
// Accrue protocol share (floor) from the token-side fee computed by implementation
|
||||||
|
if (PROTOCOL_FEE_PPM > 0 && feeTokenUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||||
|
uint256 protoShare = (feeTokenUint * PROTOCOL_FEE_PPM) / 1_000_000;
|
||||||
|
if (protoShare > 0) {
|
||||||
|
protocolFeesOwed[inputTokenIndex] += protoShare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balance for the target token to effective onchain - owed
|
||||||
|
uint256 bal = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
|
_recordCachedBalance(inputTokenIndex, bal);
|
||||||
|
|
||||||
|
return outAmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -673,7 +617,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
for (uint256 i = 0; i < tokens.length; i++) {
|
for (uint256 i = 0; i < tokens.length; i++) {
|
||||||
uint256 amount = loanAmounts[i];
|
uint256 amount = loanAmounts[i];
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
repaymentAmounts[i] = amount + _ceilFee(amount, FLASH_FEE_PPM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,7 +657,7 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
hasNonZeroAmount = true;
|
hasNonZeroAmount = true;
|
||||||
|
|
||||||
// Calculate repayment amount with fee (ceiling)
|
// Calculate repayment amount with fee (ceiling)
|
||||||
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
repaymentAmounts[i] = amount + _ceilFee(amount, FLASH_FEE_PPM);
|
||||||
|
|
||||||
// Record initial balance
|
// Record initial balance
|
||||||
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
|
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
@@ -734,14 +678,25 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
|||||||
if (amounts[i] > 0) {
|
if (amounts[i] > 0) {
|
||||||
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
|
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
|
||||||
|
// Compute expected fee (ceiling)
|
||||||
|
uint256 feeExpected = _ceilFee(amounts[i], FLASH_FEE_PPM);
|
||||||
|
|
||||||
// Verify repayment: current balance must be at least (initial balance + fee)
|
// Verify repayment: current balance must be at least (initial balance + fee)
|
||||||
require(
|
require(
|
||||||
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
|
currentBalance >= initialBalances[i] + feeExpected,
|
||||||
"flash: repayment failed"
|
"flash: repayment failed"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update cached balance
|
// Accrue protocol share (floor) of the flash fee
|
||||||
cachedUintBalances[i] = currentBalance;
|
if (PROTOCOL_FEE_PPM > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||||
|
uint256 protoShare = (feeExpected * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
||||||
|
if (protoShare > 0) {
|
||||||
|
protocolFeesOwed[i] += protoShare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balance to onchain minus owed
|
||||||
|
_recordCachedBalance(i, currentBalance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
src/PartyPoolMintImpl.sol
Normal file
237
src/PartyPoolMintImpl.sol
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// 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";
|
||||||
|
import {PartyPool} from "./PartyPool.sol";
|
||||||
|
|
||||||
|
/// @title PartyPoolMintImpl - Implementation contract for mint and burn functions
|
||||||
|
/// @notice This contract contains the mint and burn implementation that will be called via delegatecall
|
||||||
|
/// @dev This contract inherits from PartyPoolBase to access storage and internal functions
|
||||||
|
contract PartyPoolMintImpl is PartyPoolBase {
|
||||||
|
using ABDKMath64x64 for int128;
|
||||||
|
using LMSRStabilized for LMSRStabilized.State;
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
// Events that mirror the main contract events
|
||||||
|
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('','') {}
|
||||||
|
|
||||||
|
function initialMint(address receiver, uint256 lpTokens, int128 KAPPA) external
|
||||||
|
returns (uint256 lpMinted) {
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
|
||||||
|
// Check if this is initial deposit - revert if not
|
||||||
|
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
||||||
|
require(isInitialDeposit, "initialMint: pool already initialized");
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
uint256[] memory depositAmounts = new uint256[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
||||||
|
depositAmounts[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the stabilized LMSR state with provided kappa
|
||||||
|
lmsr.init(newQInternal, KAPPA);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint based on size metric (scaled)
|
||||||
|
if( lpTokens != 0 )
|
||||||
|
lpMinted = lpTokens;
|
||||||
|
else {
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
lpMinted = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
require(lpMinted > 0, "initialMint: zero LP amount");
|
||||||
|
_mint(receiver, lpMinted);
|
||||||
|
emit Mint(address(0), receiver, depositAmounts, lpMinted);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mint(address payer, address receiver, uint256 lpTokenAmount, uint256 deadline) external returns (uint256 lpMinted) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "mint: deadline exceeded");
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
|
||||||
|
// Check if this is NOT initial deposit - revert if it is
|
||||||
|
bool isInitialDeposit = totalSupply() == 0 || lmsr.nAssets == 0;
|
||||||
|
require(!isInitialDeposit, "mint: use initialMint for pool initialization");
|
||||||
|
require(lpTokenAmount > 0, "mint: zero LP amount");
|
||||||
|
|
||||||
|
// Capture old pool size metric (scaled) by computing from current balances
|
||||||
|
int128 oldTotal = _computeSizeMetric(lmsr.qInternal);
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
// Calculate required deposit amounts for the desired LP tokens
|
||||||
|
uint256[] memory depositAmounts = mintAmounts(lpTokenAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
|
||||||
|
|
||||||
|
// Transfer in all token amounts
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (depositAmounts[i] > 0) {
|
||||||
|
tokens[i].safeTransferFrom(payer, address(this), depositAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update for proportional change
|
||||||
|
lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
|
||||||
|
// Compute actual LP tokens to mint based on change in size metric (scaled)
|
||||||
|
// floor truncation rounds in favor of the pool
|
||||||
|
int128 newTotal = _computeSizeMetric(newQInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
uint256 actualLpToMint;
|
||||||
|
|
||||||
|
require(oldScaled > 0, "mint: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
// Proportional issuance: totalSupply * delta / oldScaled
|
||||||
|
if (delta > 0) {
|
||||||
|
// floor truncation rounds in favor of the pool
|
||||||
|
actualLpToMint = (totalSupply() * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
actualLpToMint = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the calculated LP amount is not too different from requested
|
||||||
|
require(actualLpToMint > 0, "mint: zero LP minted");
|
||||||
|
|
||||||
|
// Allow actual amount to be at most 0.00001% less than requested
|
||||||
|
// This accounts for rounding in deposit calculations
|
||||||
|
uint256 minAcceptable = lpTokenAmount * 99_999 / 100_000;
|
||||||
|
require(actualLpToMint >= minAcceptable, "mint: insufficient LP minted");
|
||||||
|
|
||||||
|
_mint(receiver, actualLpToMint);
|
||||||
|
emit Mint(payer, receiver, depositAmounts, actualLpToMint);
|
||||||
|
return actualLpToMint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Burn LP tokens and withdraw the proportional basket to receiver.
|
||||||
|
/// @dev Payer must own or approve the LP tokens being burned. The function updates LMSR state
|
||||||
|
/// proportionally to reflect the reduced pool size after the withdrawal.
|
||||||
|
/// @param payer address that provides the LP tokens to burn
|
||||||
|
/// @param receiver address that receives the withdrawn tokens
|
||||||
|
/// @param lpAmount amount of LP tokens to burn (proportional withdrawal)
|
||||||
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
||||||
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline) external
|
||||||
|
returns (uint256[] memory withdrawAmounts) {
|
||||||
|
require(deadline == 0 || block.timestamp <= deadline, "burn: deadline exceeded");
|
||||||
|
uint256 n = tokens.length;
|
||||||
|
require(lpAmount > 0, "burn: zero lp");
|
||||||
|
|
||||||
|
uint256 supply = totalSupply();
|
||||||
|
require(supply > 0, "burn: empty supply");
|
||||||
|
require(lmsr.nAssets > 0, "burn: uninit pool");
|
||||||
|
require(balanceOf(payer) >= lpAmount, "burn: insufficient LP");
|
||||||
|
|
||||||
|
// Refresh cached balances to reflect current on-chain balances before computing withdrawal amounts
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
cachedUintBalances[i] = bal;
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute proportional withdrawal amounts for the requested LP amount (rounded down)
|
||||||
|
withdrawAmounts = burnAmounts(lpAmount, lmsr.nAssets, totalSupply(), cachedUintBalances);
|
||||||
|
|
||||||
|
// Transfer underlying tokens out to receiver according to computed proportions
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (withdrawAmounts[i] > 0) {
|
||||||
|
tokens[i].safeTransfer(receiver, withdrawAmounts[i]);
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached balances and internal q for all assets
|
||||||
|
int128[] memory newQInternal = new int128[](n);
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
uint256 bal = IERC20(tokens[i]).balanceOf(address(this));
|
||||||
|
cachedUintBalances[i] = bal;
|
||||||
|
newQInternal[i] = _uintToInternalFloor(bal, bases[i]);
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply proportional update or deinitialize if drained
|
||||||
|
bool allZero = true;
|
||||||
|
for (uint i = 0; i < n; ) {
|
||||||
|
if (newQInternal[i] != int128(0)) {
|
||||||
|
allZero = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unchecked { i++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allZero) {
|
||||||
|
lmsr.deinit();
|
||||||
|
} else {
|
||||||
|
lmsr.updateForProportionalChange(newQInternal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Burn exactly the requested LP amount from payer (authorization via allowance)
|
||||||
|
if (msg.sender != payer) {
|
||||||
|
uint256 allowed = allowance(payer, msg.sender);
|
||||||
|
require(allowed >= lpAmount, "burn: allowance insufficient");
|
||||||
|
_approve(payer, msg.sender, allowed - lpAmount);
|
||||||
|
}
|
||||||
|
_burn(payer, lpAmount);
|
||||||
|
|
||||||
|
emit Burn(payer, receiver, withdrawAmounts, lpAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mintAmounts(uint256 lpTokenAmount, uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
|
||||||
|
returns (uint256[] memory depositAmounts) {
|
||||||
|
depositAmounts = new uint256[](numAssets);
|
||||||
|
|
||||||
|
// If this is the first mint or pool is empty, return zeros
|
||||||
|
// For first mint, tokens should already be transferred to the pool
|
||||||
|
if (totalSupply == 0 || numAssets == 0) {
|
||||||
|
return depositAmounts; // Return zeros, initial deposit handled differently
|
||||||
|
}
|
||||||
|
|
||||||
|
// lpTokenAmount / totalLpSupply = depositAmount / currentBalance
|
||||||
|
// Therefore: depositAmount = (lpTokenAmount * currentBalance) / totalLpSupply
|
||||||
|
// We round up to protect the pool
|
||||||
|
for (uint i = 0; i < numAssets; i++) {
|
||||||
|
uint256 currentBalance = cachedUintBalances[i];
|
||||||
|
// Calculate with rounding up: (a * b + c - 1) / c
|
||||||
|
depositAmounts[i] = (lpTokenAmount * currentBalance + totalSupply - 1) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return depositAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function burnAmounts(uint256 lpTokenAmount,
|
||||||
|
uint256 numAssets, uint256 totalSupply, uint256[] memory cachedUintBalances) public pure
|
||||||
|
returns (uint256[] memory withdrawAmounts) {
|
||||||
|
withdrawAmounts = new uint256[](numAssets);
|
||||||
|
|
||||||
|
// If supply is zero or pool uninitialized, return zeros
|
||||||
|
if (totalSupply == 0 || numAssets == 0) {
|
||||||
|
return withdrawAmounts; // Return zeros, nothing to withdraw
|
||||||
|
}
|
||||||
|
|
||||||
|
// withdrawAmount = floor(lpTokenAmount * currentBalance / totalLpSupply)
|
||||||
|
for (uint i = 0; i < numAssets; i++) {
|
||||||
|
uint256 currentBalance = cachedUintBalances[i];
|
||||||
|
withdrawAmounts[i] = (lpTokenAmount * currentBalance) / totalSupply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return withdrawAmounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,13 +40,11 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
uint256 maxAmountIn,
|
uint256 maxAmountIn,
|
||||||
uint256 deadline,
|
uint256 deadline,
|
||||||
uint256 swapFeePpm
|
uint256 swapFeePpm
|
||||||
) external returns (uint256 lpMinted) {
|
) external returns (uint256 lpMinted, uint256 feeUintActual) {
|
||||||
uint256 n = tokens.length;
|
uint256 n = tokens.length;
|
||||||
require(inputTokenIndex < n, "swapMint: idx");
|
require(inputTokenIndex < n, "swapMint: idx");
|
||||||
require(maxAmountIn > 0, "swapMint: input zero");
|
require(maxAmountIn > 0, "swapMint: input zero");
|
||||||
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
require(deadline == 0 || block.timestamp <= deadline, "swapMint: deadline");
|
||||||
|
|
||||||
// Ensure pool initialized
|
|
||||||
require(lmsr.nAssets > 0, "swapMint: uninit pool");
|
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)
|
// compute fee on gross maxAmountIn to get an initial net estimate (we'll recompute based on actual used)
|
||||||
@@ -64,7 +62,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
||||||
|
|
||||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
// Compute fee on the actual used input and total transfer amount (ceiling)
|
||||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
uint256 totalTransfer = amountInUint + feeUintActual;
|
||||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||||
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||||
|
|
||||||
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
|
// Update cached uint balances for token inputTokenIndex (implementation writes onchain value; wrapper will set effective)
|
||||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||||
|
|
||||||
// Compute old and new scaled size metrics to determine LP minted
|
// Compute old and new scaled size metrics to determine LP minted
|
||||||
@@ -124,7 +122,119 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||||
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
// Note: depositAmounts array omitted (empty) since swapMint uses single-token input
|
||||||
|
|
||||||
return actualLpToMint;
|
lpMinted = actualLpToMint;
|
||||||
|
return (lpMinted, feeUintActual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculate the amounts for a swap mint operation
|
||||||
|
/// @dev This is a pure view function that computes swap mint amounts from provided state
|
||||||
|
/// @param inputTokenIndex index of the input token
|
||||||
|
/// @param maxAmountIn maximum amount of token to deposit (inclusive of fee)
|
||||||
|
/// @param swapFeePpm fee in parts-per-million
|
||||||
|
/// @param lmsrState current LMSR state
|
||||||
|
/// @param bases_ scaling bases for each token
|
||||||
|
/// @param totalSupply_ current total LP token supply
|
||||||
|
/// @return amountInUsed actual input amount used (excluding fee)
|
||||||
|
/// @return fee fee amount charged
|
||||||
|
/// @return lpMinted LP tokens that would be minted
|
||||||
|
function swapMintAmounts(
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 maxAmountIn,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
LMSRStabilized.State memory lmsrState,
|
||||||
|
uint256[] memory bases_,
|
||||||
|
uint256 totalSupply_
|
||||||
|
) public pure returns (uint256 amountInUsed, uint256 fee, uint256 lpMinted) {
|
||||||
|
require(inputTokenIndex < bases_.length, "swapMintAmounts: idx");
|
||||||
|
require(maxAmountIn > 0, "swapMintAmounts: input zero");
|
||||||
|
require(lmsrState.nAssets > 0, "swapMintAmounts: uninit pool");
|
||||||
|
|
||||||
|
// Compute fee on gross maxAmountIn to get an initial net estimate
|
||||||
|
uint256 feeGuess = 0;
|
||||||
|
uint256 netUintGuess = maxAmountIn;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
feeGuess = (maxAmountIn * swapFeePpm + 999999) / 1000000; // ceil fee
|
||||||
|
netUintGuess = maxAmountIn - feeGuess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the net guess to internal (floor)
|
||||||
|
int128 netInternalGuess = _uintToInternalFloorPure(netUintGuess, bases_[inputTokenIndex]);
|
||||||
|
require(netInternalGuess > int128(0), "swapMintAmounts: input too small after fee");
|
||||||
|
|
||||||
|
// Use LMSR view to determine actual internal consumed and size-increase (ΔS) for mint
|
||||||
|
(int128 amountInInternalUsed, int128 sizeIncreaseInternal) =
|
||||||
|
LMSRStabilized.swapAmountsForMint(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
|
||||||
|
inputTokenIndex, netInternalGuess);
|
||||||
|
|
||||||
|
// amountInInternalUsed may be <= netInternalGuess. Convert to uint (ceil) to determine actual transfer
|
||||||
|
amountInUsed = _internalToUintCeilPure(amountInInternalUsed, bases_[inputTokenIndex]);
|
||||||
|
require(amountInUsed > 0, "swapMintAmounts: input zero after internal conversion");
|
||||||
|
|
||||||
|
// Compute fee on the actual used input (ceiling)
|
||||||
|
fee = 0;
|
||||||
|
if (swapFeePpm > 0) {
|
||||||
|
fee = (amountInUsed * swapFeePpm + 999999) / 1000000; // ceil fee
|
||||||
|
}
|
||||||
|
uint256 totalTransfer = amountInUsed + fee;
|
||||||
|
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMintAmounts: transfer exceeds max");
|
||||||
|
|
||||||
|
// Compute old and new scaled size metrics to determine LP minted
|
||||||
|
int128 oldTotal = _computeSizeMetricPure(lmsrState.qInternal);
|
||||||
|
require(oldTotal > int128(0), "swapMintAmounts: zero total");
|
||||||
|
uint256 oldScaled = ABDKMath64x64.mulu(oldTotal, LP_SCALE);
|
||||||
|
|
||||||
|
int128 newTotal = oldTotal.add(sizeIncreaseInternal);
|
||||||
|
uint256 newScaled = ABDKMath64x64.mulu(newTotal, LP_SCALE);
|
||||||
|
|
||||||
|
if (totalSupply_ == 0) {
|
||||||
|
// If somehow supply zero (shouldn't happen as lmsr.nAssets>0), mint newScaled
|
||||||
|
lpMinted = newScaled;
|
||||||
|
} else {
|
||||||
|
require(oldScaled > 0, "swapMintAmounts: oldScaled zero");
|
||||||
|
uint256 delta = (newScaled > oldScaled) ? (newScaled - oldScaled) : 0;
|
||||||
|
if (delta > 0) {
|
||||||
|
// floor truncation rounds in favor of pool
|
||||||
|
lpMinted = (totalSupply_ * delta) / oldScaled;
|
||||||
|
} else {
|
||||||
|
lpMinted = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(lpMinted > 0, "swapMintAmounts: zero LP minted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculate the amounts for a burn swap operation
|
||||||
|
/// @dev This is a pure view function that computes burn swap amounts from provided state
|
||||||
|
/// @param lpAmount amount of LP tokens to burn
|
||||||
|
/// @param inputTokenIndex index of target asset to receive
|
||||||
|
/// @param swapFeePpm fee in parts-per-million
|
||||||
|
/// @param lmsrState current LMSR state
|
||||||
|
/// @param bases_ scaling bases for each token
|
||||||
|
/// @param totalSupply_ current total LP token supply
|
||||||
|
/// @return amountOut amount of target asset that would be received
|
||||||
|
function burnSwapAmounts(
|
||||||
|
uint256 lpAmount,
|
||||||
|
uint256 inputTokenIndex,
|
||||||
|
uint256 swapFeePpm,
|
||||||
|
LMSRStabilized.State memory lmsrState,
|
||||||
|
uint256[] memory bases_,
|
||||||
|
uint256 totalSupply_
|
||||||
|
) public pure returns (uint256 amountOut) {
|
||||||
|
require(inputTokenIndex < bases_.length, "burnSwapAmounts: idx");
|
||||||
|
require(lpAmount > 0, "burnSwapAmounts: zero lp");
|
||||||
|
require(totalSupply_ > 0, "burnSwapAmounts: empty supply");
|
||||||
|
|
||||||
|
// alpha = lpAmount / supply as Q64.64
|
||||||
|
int128 alpha = ABDKMath64x64.divu(lpAmount, totalSupply_) // fraction of total supply to burn
|
||||||
|
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
|
||||||
|
|
||||||
|
// Use LMSR view to compute single-asset payout and burned size-metric
|
||||||
|
(int128 payoutInternal, ) = LMSRStabilized.swapAmountsForBurn(lmsrState.nAssets, lmsrState.kappa, lmsrState.qInternal,
|
||||||
|
inputTokenIndex, alpha);
|
||||||
|
|
||||||
|
// Convert payoutInternal -> uint (floor) to favor pool
|
||||||
|
amountOut = _internalToUintFloorPure(payoutInternal, bases_[inputTokenIndex]);
|
||||||
|
require(amountOut > 0, "burnSwapAmounts: output zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @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.
|
||||||
@@ -136,7 +246,6 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
/// @param deadline optional deadline
|
/// @param deadline optional deadline
|
||||||
/// @param swapFeePpm fee in parts-per-million for this pool (may be used for future fee logic)
|
/// @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
|
/// @return amountOutUint uint amount of asset i sent to receiver
|
||||||
// todo fee!?
|
|
||||||
function burnSwap(
|
function burnSwap(
|
||||||
address payer,
|
address payer,
|
||||||
address receiver,
|
address receiver,
|
||||||
@@ -144,7 +253,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
uint256 inputTokenIndex,
|
uint256 inputTokenIndex,
|
||||||
uint256 deadline,
|
uint256 deadline,
|
||||||
uint256 swapFeePpm
|
uint256 swapFeePpm
|
||||||
) external returns (uint256 amountOutUint) {
|
) external returns (uint256 amountOutUint, uint256 feeTokenUint) {
|
||||||
uint256 n = tokens.length;
|
uint256 n = tokens.length;
|
||||||
require(inputTokenIndex < n, "burnSwap: idx");
|
require(inputTokenIndex < n, "burnSwap: idx");
|
||||||
require(lpAmount > 0, "burnSwap: zero lp");
|
require(lpAmount > 0, "burnSwap: zero lp");
|
||||||
@@ -154,8 +263,9 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
require(supply > 0, "burnSwap: empty supply");
|
require(supply > 0, "burnSwap: empty supply");
|
||||||
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
require(balanceOf(payer) >= lpAmount, "burnSwap: insufficient LP");
|
||||||
|
|
||||||
// alpha = lpAmount / supply as Q64.64
|
// alpha = lpAmount / supply as Q64.64 (adjusted for fee)
|
||||||
int128 alpha = ABDKMath64x64.divu(lpAmount, supply);
|
int128 alpha = ABDKMath64x64.divu(lpAmount, supply) // fraction of total supply to burn
|
||||||
|
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
|
||||||
|
|
||||||
// Use LMSR view to compute single-asset payout and burned size-metric
|
// Use LMSR view to compute single-asset payout and burned size-metric
|
||||||
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
(int128 payoutInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alpha);
|
||||||
@@ -164,6 +274,12 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
||||||
require(amountOutUint > 0, "burnSwap: output zero");
|
require(amountOutUint > 0, "burnSwap: output zero");
|
||||||
|
|
||||||
|
// Compute gross payout (no swap fee) so we can determine token-side fee = gross - net
|
||||||
|
int128 alphaGross = ABDKMath64x64.divu(lpAmount, supply); // gross fraction (no swap fee)
|
||||||
|
(int128 payoutGrossInternal, ) = lmsr.swapAmountsForBurn(inputTokenIndex, alphaGross);
|
||||||
|
uint256 payoutGrossUint = _internalToUintFloor(payoutGrossInternal, bases[inputTokenIndex]);
|
||||||
|
feeTokenUint = (payoutGrossUint > amountOutUint) ? (payoutGrossUint - amountOutUint) : 0;
|
||||||
|
|
||||||
// Transfer the payout to receiver
|
// Transfer the payout to receiver
|
||||||
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||||
|
|
||||||
@@ -179,6 +295,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
int128[] memory newQInternal = new int128[](n);
|
int128[] memory newQInternal = new int128[](n);
|
||||||
for (uint256 idx = 0; idx < n; idx++) {
|
for (uint256 idx = 0; idx < n; idx++) {
|
||||||
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
||||||
|
// implementation writes raw onchain values; wrapper will set effective cached values
|
||||||
cachedUintBalances[idx] = bal;
|
cachedUintBalances[idx] = bal;
|
||||||
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
||||||
}
|
}
|
||||||
@@ -198,6 +315,42 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||||
return amountOutUint;
|
return (amountOutUint, feeTokenUint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version of _uintToInternalFloor for use in view functions
|
||||||
|
function _uintToInternalFloorPure(uint256 amount, uint256 base) internal pure returns (int128) {
|
||||||
|
// amount / base as Q64.64, floored
|
||||||
|
return ABDKMath64x64.divu(amount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version of _internalToUintCeil for use in view functions
|
||||||
|
function _internalToUintCeilPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// Convert Q64.64 to uint with ceiling: ceil(amount * base)
|
||||||
|
// Use mulu which floors, then add remainder check for ceiling
|
||||||
|
uint256 floored = ABDKMath64x64.mulu(amount, base);
|
||||||
|
// Check if there's a fractional part by computing amount * base - floored
|
||||||
|
int128 baseQ64 = ABDKMath64x64.fromUInt(base);
|
||||||
|
int128 flooredQ64 = ABDKMath64x64.fromUInt(floored);
|
||||||
|
int128 product = amount.mul(baseQ64);
|
||||||
|
if (product > flooredQ64) {
|
||||||
|
return floored + 1; // Ceiling
|
||||||
|
}
|
||||||
|
return floored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version of _internalToUintFloor for use in view functions
|
||||||
|
function _internalToUintFloorPure(int128 amount, uint256 base) internal pure returns (uint256) {
|
||||||
|
// Convert Q64.64 to uint with floor: floor(amount * base)
|
||||||
|
return ABDKMath64x64.mulu(amount, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Pure version of _computeSizeMetric for use in view functions
|
||||||
|
function _computeSizeMetricPure(int128[] memory qInternal) internal pure returns (int128) {
|
||||||
|
int128 sum = int128(0);
|
||||||
|
for (uint256 i = 0; i < qInternal.length; i++) {
|
||||||
|
sum = sum.add(qInternal[i]);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
/* solhint-disable erc20-unchecked-transfer */
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "@abdk/ABDKMath64x64.sol";
|
import "@abdk/ABDKMath64x64.sol";
|
||||||
@@ -129,22 +130,23 @@ contract TestERC20 is ERC20 {
|
|||||||
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
|
/// @notice Gas testing contract for PartyPool - contains all gas measurement tests
|
||||||
contract GasTest is Test {
|
contract GasTest is Test {
|
||||||
using ABDKMath64x64 for int128;
|
using ABDKMath64x64 for int128;
|
||||||
|
using SafeERC20 for TestERC20;
|
||||||
|
|
||||||
PartyPlanner planner;
|
PartyPlanner internal planner;
|
||||||
PartyPool pool2;
|
PartyPool internal pool2;
|
||||||
PartyPool pool10;
|
PartyPool internal pool10;
|
||||||
PartyPool pool20;
|
PartyPool internal pool20;
|
||||||
PartyPool pool50;
|
PartyPool internal pool50;
|
||||||
|
|
||||||
address alice;
|
address internal alice;
|
||||||
address bob;
|
address internal bob;
|
||||||
|
|
||||||
// Common parameters
|
// Common parameters
|
||||||
int128 tradeFrac;
|
int128 internal tradeFrac;
|
||||||
int128 targetSlippage;
|
int128 internal targetSlippage;
|
||||||
|
|
||||||
uint256 constant INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
|
uint256 constant internal INIT_BAL = 1_000_000; // initial token units for each token (internal==amount when base==1)
|
||||||
uint256 constant BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
uint256 constant internal BASE = 1; // use base=1 so internal amounts correspond to raw integers (Q64.64 units)
|
||||||
|
|
||||||
/// @notice Helper function to create a pool with the specified number of tokens
|
/// @notice Helper function to create a pool with the specified number of tokens
|
||||||
function createPool(uint256 numTokens) internal returns (PartyPool) {
|
function createPool(uint256 numTokens) internal returns (PartyPool) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "forge-std/console.sol";
|
import "forge-std/console2.sol";
|
||||||
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
import "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
|
||||||
import "../src/LMSRStabilized.sol";
|
import "../src/LMSRStabilized.sol";
|
||||||
import "../src/LMSRStabilizedBalancedPair.sol";
|
import "../src/LMSRStabilizedBalancedPair.sol";
|
||||||
@@ -711,7 +711,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Path 1: Direct swap from asset 0 to asset 2
|
// Path 1: Direct swap from asset 0 to asset 2
|
||||||
(int128 directAmountIn, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
(/*int128 directAmountIn*/, int128 directAmountOut) = s.swapAmountsForExactInput(0, 2, directSwapAmount, 0);
|
||||||
|
|
||||||
// Restore original state for second path
|
// Restore original state for second path
|
||||||
_updateCachedQInternal(backupQ);
|
_updateCachedQInternal(backupQ);
|
||||||
@@ -724,7 +724,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
s.qInternal[1] = s.qInternal[1].add(indirectAmountOut1);
|
||||||
|
|
||||||
// Second swap: asset 1 -> asset 2
|
// Second swap: asset 1 -> asset 2
|
||||||
(int128 indirectAmountIn2, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
(/*int128 indirectAmountIn2*/, int128 indirectAmountOut2) = s.swapAmountsForExactInput(1, 2, indirectAmountOut1, 0);
|
||||||
|
|
||||||
// The path independence property isn't perfect due to discrete swap mechanics,
|
// The path independence property isn't perfect due to discrete swap mechanics,
|
||||||
// but the difference should be within reasonable bounds
|
// but the difference should be within reasonable bounds
|
||||||
@@ -765,7 +765,7 @@ contract LMSRStabilizedTest is Test {
|
|||||||
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
s.qInternal[1] = s.qInternal[1].add(amountOut1);
|
||||||
|
|
||||||
// Step 2: Swap back asset 1 -> asset 0
|
// Step 2: Swap back asset 1 -> asset 0
|
||||||
(int128 amountIn2, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
(/*int128 amountIn2*/, int128 amountOut2) = s.swapAmountsForExactInput(1, 0, amountOut1, 0);
|
||||||
|
|
||||||
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
// Calculate round-trip slippage: (initial amount - final amount) / initial amount
|
||||||
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
int128 roundTripSlippage = (amountIn1.sub(amountOut2)).div(amountIn1);
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ pragma solidity ^0.8.30;
|
|||||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||||
|
|
||||||
contract MockERC20 is ERC20 {
|
contract MockERC20 is ERC20 {
|
||||||
uint8 private immutable _decimals;
|
uint8 private immutable DECIMALS;
|
||||||
|
|
||||||
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {_decimals = decimals_;}
|
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {DECIMALS = decimals_;}
|
||||||
|
|
||||||
function decimals() public view virtual override returns (uint8) {return _decimals;}
|
function decimals() public view virtual override returns (uint8) {return DECIMALS;}
|
||||||
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
function mint(address account, uint256 amount) external {_mint(account, amount);}
|
||||||
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
function burn(address account, uint256 amount) external {_burn(account, amount);}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
/* solhint-disable */
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.8.30;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
@@ -323,7 +324,7 @@ contract PartyPoolTest is Test {
|
|||||||
token2.approve(address(pool), type(uint256).max);
|
token2.approve(address(pool), type(uint256).max);
|
||||||
|
|
||||||
// Inspect the deposit amounts that the pool will require (these are rounded up)
|
// Inspect the deposit amounts that the pool will require (these are rounded up)
|
||||||
uint256[] memory deposits = pool.mintDepositAmounts(1);
|
uint256[] memory deposits = pool.mintAmounts(1);
|
||||||
|
|
||||||
// Basic sanity: deposits array length must match token count and not all zero necessarily
|
// Basic sanity: deposits array length must match token count and not all zero necessarily
|
||||||
assertEq(deposits.length, 3);
|
assertEq(deposits.length, 3);
|
||||||
@@ -365,7 +366,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 totalLpBefore = pool.totalSupply();
|
uint256 totalLpBefore = pool.totalSupply();
|
||||||
|
|
||||||
// Compute required deposits and perform mint for 1 wei
|
// Compute required deposits and perform mint for 1 wei
|
||||||
uint256[] memory deposits = pool.mintDepositAmounts(1);
|
uint256[] memory deposits = pool.mintAmounts(1);
|
||||||
|
|
||||||
// Sum deposits as deposited_value
|
// Sum deposits as deposited_value
|
||||||
uint256 depositedValue = 0;
|
uint256 depositedValue = 0;
|
||||||
@@ -399,14 +400,14 @@ contract PartyPoolTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice mintDepositAmounts should round up deposit amounts to protect the pool.
|
/// @notice mintAmounts should round up deposit amounts to protect the pool.
|
||||||
function testMintDepositAmountsRoundingUp() public view {
|
function testMintDepositAmountsRoundingUp() public view {
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
assertTrue(totalLp > 0, "precondition: total supply > 0");
|
assertTrue(totalLp > 0, "precondition: total supply > 0");
|
||||||
|
|
||||||
// Request half of LP supply
|
// Request half of LP supply
|
||||||
uint256 want = totalLp / 2;
|
uint256 want = totalLp / 2;
|
||||||
uint256[] memory deposits = pool.mintDepositAmounts(want);
|
uint256[] memory deposits = pool.mintAmounts(want);
|
||||||
|
|
||||||
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
|
// We expect each deposit to be roughly half the pool balance, but due to rounding up it should satisfy:
|
||||||
// deposits[i] * 2 >= cached balance (i.e., rounding up)
|
// deposits[i] * 2 >= cached balance (i.e., rounding up)
|
||||||
@@ -423,7 +424,7 @@ contract PartyPoolTest is Test {
|
|||||||
assertTrue(totalLp > 0, "precondition: LP > 0");
|
assertTrue(totalLp > 0, "precondition: LP > 0");
|
||||||
|
|
||||||
// Compute amounts required to redeem entire supply (should be current balances)
|
// Compute amounts required to redeem entire supply (should be current balances)
|
||||||
uint256[] memory withdrawAmounts = pool.burnReceiveAmounts(totalLp);
|
uint256[] memory withdrawAmounts = pool.burnAmounts(totalLp);
|
||||||
|
|
||||||
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
|
// Sanity: withdrawAmounts should equal pool balances (or very close due to rounding)
|
||||||
for (uint i = 0; i < withdrawAmounts.length; i++) {
|
for (uint i = 0; i < withdrawAmounts.length; i++) {
|
||||||
@@ -513,7 +514,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint()
|
/// @notice Verify mintAmounts matches the actual token transfers performed by mint()
|
||||||
function testMintDepositAmountsMatchesMint_3TokenPool() public {
|
function testMintDepositAmountsMatchesMint_3TokenPool() public {
|
||||||
// Use a range of LP requests (tiny to large fraction)
|
// Use a range of LP requests (tiny to large fraction)
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
@@ -527,7 +528,7 @@ contract PartyPoolTest is Test {
|
|||||||
if (req == 0) req = 1;
|
if (req == 0) req = 1;
|
||||||
|
|
||||||
// Compute expected deposit amounts via view
|
// Compute expected deposit amounts via view
|
||||||
uint256[] memory expected = pool.mintDepositAmounts(req);
|
uint256[] memory expected = pool.mintAmounts(req);
|
||||||
|
|
||||||
// Ensure alice has tokens and approve pool
|
// Ensure alice has tokens and approve pool
|
||||||
vm.startPrank(alice);
|
vm.startPrank(alice);
|
||||||
@@ -541,7 +542,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 a2Before = token2.balanceOf(alice);
|
uint256 a2Before = token2.balanceOf(alice);
|
||||||
|
|
||||||
// Perform mint (may revert for zero-request; ensure req>0 above)
|
// Perform mint (may revert for zero-request; ensure req>0 above)
|
||||||
// Guard: if mintDepositAmounts returned all zeros, skip (nothing to transfer)
|
// Guard: if mintAmounts returned all zeros, skip (nothing to transfer)
|
||||||
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
|
bool allZero = (expected[0] == 0 && expected[1] == 0 && expected[2] == 0);
|
||||||
if (!allZero) {
|
if (!allZero) {
|
||||||
uint256 lpBefore = pool.balanceOf(alice);
|
uint256 lpBefore = pool.balanceOf(alice);
|
||||||
@@ -560,7 +561,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify mintDepositAmounts matches the actual token transfers performed by mint() for 10-token pool
|
/// @notice Verify mintAmounts matches the actual token transfers performed by mint() for 10-token pool
|
||||||
function testMintDepositAmountsMatchesMint_10TokenPool() public {
|
function testMintDepositAmountsMatchesMint_10TokenPool() public {
|
||||||
uint256 totalLp = pool10.totalSupply();
|
uint256 totalLp = pool10.totalSupply();
|
||||||
uint256[] memory requests = new uint256[](4);
|
uint256[] memory requests = new uint256[](4);
|
||||||
@@ -572,7 +573,7 @@ contract PartyPoolTest is Test {
|
|||||||
uint256 req = requests[k];
|
uint256 req = requests[k];
|
||||||
if (req == 0) req = 1;
|
if (req == 0) req = 1;
|
||||||
|
|
||||||
uint256[] memory expected = pool10.mintDepositAmounts(req);
|
uint256[] memory expected = pool10.mintAmounts(req);
|
||||||
|
|
||||||
// Approve all tokens from alice
|
// Approve all tokens from alice
|
||||||
vm.startPrank(alice);
|
vm.startPrank(alice);
|
||||||
@@ -623,7 +624,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 3-token pool
|
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 3-token pool
|
||||||
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
|
function testBurnReceiveAmountsMatchesBurn_3TokenPool() public {
|
||||||
// Use address(this) as payer (holds initial LP from setUp)
|
// Use address(this) as payer (holds initial LP from setUp)
|
||||||
uint256 totalLp = pool.totalSupply();
|
uint256 totalLp = pool.totalSupply();
|
||||||
@@ -650,7 +651,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recompute withdraw amounts via view after any top-up
|
// Recompute withdraw amounts via view after any top-up
|
||||||
uint256[] memory expected = pool.burnReceiveAmounts(req);
|
uint256[] memory expected = pool.burnAmounts(req);
|
||||||
|
|
||||||
// If expected withdraws are all zero (rounding edge), skip this iteration
|
// If expected withdraws are all zero (rounding edge), skip this iteration
|
||||||
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
|
if (expected[0] == 0 && expected[1] == 0 && expected[2] == 0) {
|
||||||
@@ -676,7 +677,7 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verify burnReceiveAmounts matches actual transfers performed by burn() for 10-token pool
|
/// @notice Verify burnAmounts matches actual transfers performed by burn() for 10-token pool
|
||||||
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
|
function testBurnReceiveAmountsMatchesBurn_10TokenPool() public {
|
||||||
uint256 totalLp = pool10.totalSupply();
|
uint256 totalLp = pool10.totalSupply();
|
||||||
uint256[] memory burns = new uint256[](4);
|
uint256[] memory burns = new uint256[](4);
|
||||||
@@ -707,7 +708,7 @@ contract PartyPoolTest is Test {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint256[] memory expected = pool10.burnReceiveAmounts(req);
|
uint256[] memory expected = pool10.burnAmounts(req);
|
||||||
|
|
||||||
// If expected withdraws are all zero (rounding edge), skip this iteration
|
// If expected withdraws are all zero (rounding edge), skip this iteration
|
||||||
bool allZero = true;
|
bool allZero = true;
|
||||||
@@ -1360,8 +1361,8 @@ contract PartyPoolTest is Test {
|
|||||||
token2.approve(address(poolCustom), type(uint256).max);
|
token2.approve(address(poolCustom), type(uint256).max);
|
||||||
|
|
||||||
// Get required deposit amounts for both pools
|
// Get required deposit amounts for both pools
|
||||||
uint256[] memory depositsDefault = poolDefault.mintDepositAmounts(lpRequestDefault);
|
uint256[] memory depositsDefault = poolDefault.mintAmounts(lpRequestDefault);
|
||||||
uint256[] memory depositsCustom = poolCustom.mintDepositAmounts(lpRequestCustom);
|
uint256[] memory depositsCustom = poolCustom.mintAmounts(lpRequestCustom);
|
||||||
|
|
||||||
// Deposits should be identical (same proportion of identical balances)
|
// Deposits should be identical (same proportion of identical balances)
|
||||||
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
|
assertEq(depositsDefault[0], depositsCustom[0], "Token0 deposits should be identical");
|
||||||
@@ -1389,3 +1390,4 @@ contract PartyPoolTest is Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
/* solhint-enable */
|
||||||
|
|||||||
Reference in New Issue
Block a user