693 lines
31 KiB
Solidity
693 lines
31 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.30;
|
|
|
|
import "forge-std/console2.sol";
|
|
import "@abdk/ABDKMath64x64.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/utils/Address.sol";
|
|
import "./LMSRStabilized.sol";
|
|
import "./LMSRStabilizedBalancedPair.sol";
|
|
import "./IPartyPool.sol";
|
|
import "./IPartyFlashCallback.sol";
|
|
import "./PartyPoolBase.sol";
|
|
import {PartyPoolSwapMintImpl} from "./PartyPoolSwapMintImpl.sol";
|
|
|
|
/// @title PartyPool - LMSR-backed multi-asset pool with LP ERC20 token
|
|
/// @notice A multi-asset liquidity pool backed by the LMSRStabilized pricing model.
|
|
/// The pool issues an ERC20 LP token representing proportional ownership.
|
|
/// It supports:
|
|
/// - Proportional minting and burning of LP tokens,
|
|
/// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap),
|
|
/// - Exact-input swaps and swaps-to-price-limits,
|
|
/// - Flash loans via a callback interface.
|
|
///
|
|
/// @dev The contract stores per-token uint "bases" used to scale token units into the internal Q64.64
|
|
/// representation used by the LMSR library. Cached on-chain uint balances are kept to reduce balanceOf calls.
|
|
/// The contract uses ceiling/floor rules described in function comments to bias rounding in favor of the pool
|
|
/// (i.e., floor outputs to users, ceil inputs/fees where appropriate).
|
|
contract PartyPool is PartyPoolBase, IPartyPool {
|
|
using ABDKMath64x64 for int128;
|
|
using LMSRStabilized for LMSRStabilized.State;
|
|
using SafeERC20 for IERC20;
|
|
|
|
/// @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
|
|
/// should use LMSRStabilized.computeKappaFromSlippage(...) to derive κ and pass it here.
|
|
int128 public immutable kappa; // kappa in Q64.64
|
|
|
|
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
|
|
uint256 public immutable swapFeePpm;
|
|
|
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
|
uint256 public immutable flashFeePpm;
|
|
|
|
/// @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
|
|
|
|
/// @notice Address of the SwapMint implementation contract for delegatecall
|
|
address public immutable swapMintImpl;
|
|
|
|
/// @notice Address of the Mint implementation contract for delegatecall
|
|
address public immutable mintImpl;
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function getToken(uint256 i) external view returns (IERC20) { return tokens[i]; }
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function numTokens() external view returns (uint256) { return tokens.length; }
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function allTokens() external view returns (IERC20[] memory) { return tokens; }
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function denominators() external view returns (uint256[] memory) { return bases; }
|
|
|
|
/// @param name_ LP token name
|
|
/// @param symbol_ LP token symbol
|
|
/// @param tokens_ token addresses (n)
|
|
/// @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 _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 _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 _mintImpl address of the Mint implementation contract
|
|
constructor(
|
|
string memory name_,
|
|
string memory symbol_,
|
|
IERC20[] memory tokens_,
|
|
uint256[] memory bases_,
|
|
int128 _kappa,
|
|
uint256 _swapFeePpm,
|
|
uint256 _flashFeePpm,
|
|
bool _stable,
|
|
PartyPoolSwapMintImpl _swapMintImpl,
|
|
address _mintImpl
|
|
) PartyPoolBase(name_, symbol_) {
|
|
require(tokens_.length > 1, "Pool: need >1 asset");
|
|
require(tokens_.length == bases_.length, "Pool: lengths mismatch");
|
|
tokens = tokens_;
|
|
bases = bases_;
|
|
kappa = _kappa;
|
|
require(_swapFeePpm < 1_000_000, "Pool: fee >= ppm");
|
|
swapFeePpm = _swapFeePpm;
|
|
require(_flashFeePpm < 1_000_000, "Pool: flash fee >= ppm");
|
|
flashFeePpm = _flashFeePpm;
|
|
_stablePair = _stable && tokens_.length == 2;
|
|
require(address(_swapMintImpl) != address(0), "Pool: swapMintImpl address zero");
|
|
swapMintImpl = address(_swapMintImpl);
|
|
require(_mintImpl != address(0), "Pool: mintImpl address zero");
|
|
mintImpl = _mintImpl;
|
|
|
|
uint256 n = tokens_.length;
|
|
|
|
// Initialize LMSR state nAssets; full init occurs on first mint when quantities are known.
|
|
lmsr.nAssets = n;
|
|
|
|
// Initialize token address to index mapping
|
|
for (uint i = 0; i < n;) {
|
|
tokenAddressToIndexPlusOne[tokens_[i]] = i + 1;
|
|
unchecked {i++;}
|
|
}
|
|
|
|
// Initialize caches to zero
|
|
cachedUintBalances = new uint256[](n);
|
|
}
|
|
|
|
|
|
/* ----------------------
|
|
Initialization / Mint / Burn (LP token managed)
|
|
---------------------- */
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function mintDepositAmounts(uint256 lpTokenAmount) public view returns (uint256[] memory depositAmounts) {
|
|
return _mintDepositAmounts(lpTokenAmount);
|
|
}
|
|
|
|
function _mintDepositAmounts(uint256 lpTokenAmount) internal 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
|
|
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);
|
|
}
|
|
|
|
/// @notice Proportional mint for existing pool.
|
|
/// @dev This function forwards the call to the mint implementation via delegatecall
|
|
/// @param payer address that provides the input tokens
|
|
/// @param receiver address that receives the LP tokens
|
|
/// @param lpTokenAmount desired amount of LP tokens to mint
|
|
/// @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
|
|
returns (uint256 lpMinted) {
|
|
bytes memory data = abi.encodeWithSignature(
|
|
"mint(address,address,uint256,uint256)",
|
|
payer,
|
|
receiver,
|
|
lpTokenAmount,
|
|
deadline
|
|
);
|
|
|
|
bytes memory result = Address.functionDelegateCall(mintImpl, data);
|
|
return abi.decode(result, (uint256));
|
|
}
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function burnReceiveAmounts(uint256 lpTokenAmount) external view returns (uint256[] memory withdrawAmounts) {
|
|
return _burnReceiveAmounts(lpTokenAmount);
|
|
}
|
|
|
|
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.
|
|
/// @dev This function forwards the call to the burn implementation via delegatecall
|
|
/// @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 nonReentrant {
|
|
bytes memory data = abi.encodeWithSignature(
|
|
"burn(address,address,uint256,uint256)",
|
|
payer,
|
|
receiver,
|
|
lpAmount,
|
|
deadline
|
|
);
|
|
|
|
Address.functionDelegateCall(mintImpl, data);
|
|
}
|
|
|
|
/* ----------------------
|
|
Swaps
|
|
---------------------- */
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function swapAmounts(
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
int128 limitPrice
|
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
|
return (grossIn, outUint, feeUint);
|
|
}
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function swapToLimitAmounts(
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
int128 limitPrice
|
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
return (grossIn, outUint, feeUint);
|
|
}
|
|
|
|
|
|
/// @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.
|
|
/// Non-standard tokens (fee-on-transfer, rebasers) are rejected via balance checks.
|
|
/// @param payer address of the account that pays for the swap
|
|
/// @param receiver address that will receive the output tokens
|
|
/// @param inputTokenIndex index of input asset
|
|
/// @param outputTokenIndex index of output asset
|
|
/// @param maxAmountIn maximum amount of token i (uint256) to transfer in (inclusive of fees)
|
|
/// @param limitPrice maximum acceptable marginal price (64.64 fixed point). Pass 0 to ignore.
|
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
|
/// @return amountIn actual input used (uint256), amountOut actual output sent (uint256), fee fee taken from the input (uint256)
|
|
function swap(
|
|
address payer,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
int128 limitPrice,
|
|
uint256 deadline
|
|
) external nonReentrant returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
|
require(maxAmountIn > 0, "swap: input zero");
|
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
|
|
|
// Read previous balances for affected assets
|
|
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
|
|
// Compute amounts using the same path as views
|
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
|
_quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
|
|
|
|
// Transfer the exact amount from payer and require exact receipt (revert on fee-on-transfer)
|
|
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
require(balIAfter == prevBalI + totalTransferAmount, "swap: non-standard tokenIn");
|
|
|
|
// Transfer output to receiver and verify exact decrease
|
|
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
require(balJAfter == prevBalJ - amountOutUint, "swap: non-standard tokenOut");
|
|
|
|
// Update cached uint balances for i and j using actual balances
|
|
cachedUintBalances[inputTokenIndex] = balIAfter;
|
|
cachedUintBalances[outputTokenIndex] = balJAfter;
|
|
|
|
// Apply swap to LMSR state with the internal amounts actually used
|
|
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
|
|
|
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], totalTransferAmount, amountOutUint);
|
|
|
|
return (totalTransferAmount, amountOutUint, feeUint);
|
|
}
|
|
|
|
/// @notice Swap up to the price limit; computes max input to reach limit then performs swap.
|
|
/// @dev If balances prevent fully reaching the limit, the function caps and returns actuals.
|
|
/// The payer must transfer the exact gross input computed by the view.
|
|
/// @param deadline timestamp after which the transaction will revert. Pass 0 to ignore.
|
|
function swapToLimit(
|
|
address payer,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
int128 limitPrice,
|
|
uint256 deadline
|
|
) external returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
require(deadline == 0 || block.timestamp <= deadline, "swapToLimit: deadline exceeded");
|
|
|
|
// Read previous balances for affected assets
|
|
uint256 prevBalI = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
uint256 prevBalJ = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
|
|
// Compute amounts using the same path as views
|
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalMax, int128 amountOutInternal, uint256 amountInUsedUint, uint256 feeUint) =
|
|
_quoteSwapToLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
|
|
// Transfer the exact amount needed from payer and require exact receipt (revert on fee-on-transfer)
|
|
tokens[inputTokenIndex].safeTransferFrom(payer, address(this), totalTransferAmount);
|
|
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
|
require(balIAfter == prevBalI + totalTransferAmount, "swapToLimit: non-standard tokenIn");
|
|
|
|
// Transfer output to receiver and verify exact decrease
|
|
tokens[outputTokenIndex].safeTransfer(receiver, amountOutUint);
|
|
uint256 balJAfter = IERC20(tokens[outputTokenIndex]).balanceOf(address(this));
|
|
require(balJAfter == prevBalJ - amountOutUint, "swapToLimit: non-standard tokenOut");
|
|
|
|
// Update caches to actual balances
|
|
cachedUintBalances[inputTokenIndex] = balIAfter;
|
|
cachedUintBalances[outputTokenIndex] = balJAfter;
|
|
|
|
// Apply swap to LMSR state with the internal amounts
|
|
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
|
|
|
// Maintain original event semantics (logs input without fee)
|
|
emit Swap(payer, receiver, tokens[inputTokenIndex], tokens[outputTokenIndex], amountInUsedUint, amountOutUint);
|
|
|
|
return (amountInUsedUint, amountOutUint, feeUint);
|
|
}
|
|
|
|
/// @notice Internal quote for exact-input swap that mirrors swap() rounding and fee application
|
|
/// @dev Returns amounts consistent with swap() semantics: grossIn includes fees (ceil), amountOut is floored.
|
|
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
|
/// amountInInternalUsed and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
|
/// feeUint fee taken from the gross input (uint)
|
|
function _quoteSwapExactIn(
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
int128 limitPrice
|
|
)
|
|
internal
|
|
view
|
|
returns (
|
|
uint256 grossIn,
|
|
uint256 amountOutUint,
|
|
int128 amountInInternalUsed,
|
|
int128 amountOutInternal,
|
|
uint256 amountInUintNoFee,
|
|
uint256 feeUint
|
|
)
|
|
{
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n && outputTokenIndex < n, "swap: idx");
|
|
require(maxAmountIn > 0, "swap: input zero");
|
|
require(lmsr.nAssets > 0, "swap: empty pool");
|
|
|
|
// Estimate max net input (fee on gross rounded up, then subtract)
|
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, swapFeePpm);
|
|
|
|
// Convert to internal (floor)
|
|
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, bases[inputTokenIndex]);
|
|
require(deltaInternalI > int128(0), "swap: input too small after fee");
|
|
|
|
// Compute internal amounts using LMSR (exact-input with price limit)
|
|
// if _stablePair is true, use the optimized path
|
|
console2.log('stablepair optimization?', _stablePair);
|
|
(amountInInternalUsed, amountOutInternal) =
|
|
_stablePair ? LMSRStabilizedBalancedPair.swapAmountsForExactInput(lmsr, inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice)
|
|
: lmsr.swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
|
|
|
// Convert actual used input internal -> uint (ceil)
|
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, bases[inputTokenIndex]);
|
|
require(amountInUintNoFee > 0, "swap: input zero");
|
|
|
|
// Compute gross transfer including fee on the used input (ceil)
|
|
feeUint = 0;
|
|
grossIn = amountInUintNoFee;
|
|
if (swapFeePpm > 0) {
|
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
grossIn += feeUint;
|
|
}
|
|
|
|
// Ensure within user max
|
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
|
|
|
// Compute output (floor)
|
|
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
require(amountOutUint > 0, "swap: output zero");
|
|
}
|
|
|
|
/// @notice Internal quote for swap-to-limit that mirrors swapToLimit() rounding and fee application
|
|
/// @dev Computes the input required to reach limitPrice and the resulting output; all rounding matches swapToLimit.
|
|
/// @return grossIn amount to transfer in (inclusive of fee), amountOutUint output amount (uint),
|
|
/// amountInInternal and amountOutInternal (64.64), amountInUintNoFee input amount excluding fee (uint),
|
|
/// feeUint fee taken from the gross input (uint)
|
|
function _quoteSwapToLimit(
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
int128 limitPrice
|
|
)
|
|
internal
|
|
view
|
|
returns (
|
|
uint256 grossIn,
|
|
uint256 amountOutUint,
|
|
int128 amountInInternal,
|
|
int128 amountOutInternal,
|
|
uint256 amountInUintNoFee,
|
|
uint256 feeUint
|
|
)
|
|
{
|
|
uint256 n = tokens.length;
|
|
require(inputTokenIndex < n && outputTokenIndex < n, "swapToLimit: idx");
|
|
require(limitPrice > int128(0), "swapToLimit: limit <= 0");
|
|
require(lmsr.nAssets > 0, "swapToLimit: pool uninitialized");
|
|
|
|
// Compute internal maxima at the price limit
|
|
(amountInInternal, amountOutInternal) = lmsr.swapAmountsForPriceLimit(inputTokenIndex, outputTokenIndex, limitPrice);
|
|
|
|
// Convert input to uint (ceil) and output to uint (floor)
|
|
amountInUintNoFee = _internalToUintCeil(amountInInternal, bases[inputTokenIndex]);
|
|
require(amountInUintNoFee > 0, "swapToLimit: input zero");
|
|
|
|
feeUint = 0;
|
|
grossIn = amountInUintNoFee;
|
|
if (swapFeePpm > 0) {
|
|
feeUint = _ceilFee(amountInUintNoFee, swapFeePpm);
|
|
grossIn += feeUint;
|
|
}
|
|
|
|
amountOutUint = _internalToUintFloor(amountOutInternal, bases[outputTokenIndex]);
|
|
require(amountOutUint > 0, "swapToLimit: output zero");
|
|
}
|
|
|
|
/// @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)
|
|
function _computeFee(uint256 gross) internal view returns (uint256 feeUint, uint256 netUint) {
|
|
if (swapFeePpm == 0) {
|
|
return (0, gross);
|
|
}
|
|
feeUint = _ceilFee(gross, swapFeePpm);
|
|
netUint = gross - feeUint;
|
|
}
|
|
|
|
/// @notice Convenience: return gross = net + fee(net) using ceiling for fee.
|
|
function _addFee(uint256 netUint) internal view returns (uint256 gross) {
|
|
if (swapFeePpm == 0) return netUint;
|
|
uint256 fee = _ceilFee(netUint, swapFeePpm);
|
|
return netUint + fee;
|
|
}
|
|
|
|
// --- New events for single-token mint/burn flows ---
|
|
// Note: events intentionally avoid exposing internal ΔS and avoid duplicating LP mint/burn data
|
|
// which is already present in the standard Mint/Burn events.
|
|
|
|
// todo swapMintAmounts and burnSwapAmounts
|
|
|
|
/// @notice Single-token mint: deposit a single token, charge swap-LMSR cost, and mint LP.
|
|
/// @dev This function forwards the call to the swapMint implementation via delegatecall
|
|
/// @param payer who transfers the input token
|
|
/// @param receiver who receives the minted LP tokens
|
|
/// @param inputTokenIndex index of the input token
|
|
/// @param maxAmountIn maximum uint token input (inclusive of fee)
|
|
/// @param deadline optional deadline
|
|
/// @return lpMinted actual LP minted (uint)
|
|
function swapMint(
|
|
address payer,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
uint256 deadline
|
|
) external returns (uint256 lpMinted) {
|
|
bytes memory data = abi.encodeWithSignature(
|
|
"swapMint(address,address,uint256,uint256,uint256,uint256)",
|
|
payer,
|
|
receiver,
|
|
inputTokenIndex,
|
|
maxAmountIn,
|
|
deadline,
|
|
swapFeePpm
|
|
);
|
|
|
|
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
|
return abi.decode(result, (uint256));
|
|
}
|
|
|
|
/// @notice Burn LP tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
|
|
/// @dev This function forwards the call to the burnSwap implementation via delegatecall
|
|
/// @param payer who burns LP tokens
|
|
/// @param receiver who receives the single asset
|
|
/// @param lpAmount amount of LP tokens to burn
|
|
/// @param inputTokenIndex index of target asset to receive
|
|
/// @param deadline optional deadline
|
|
/// @return amountOutUint uint amount of asset i sent to receiver
|
|
function burnSwap(
|
|
address payer,
|
|
address receiver,
|
|
uint256 lpAmount,
|
|
uint256 inputTokenIndex,
|
|
uint256 deadline
|
|
) external returns (uint256 amountOutUint) {
|
|
bytes memory data = abi.encodeWithSignature(
|
|
"burnSwap(address,address,uint256,uint256,uint256,uint256)",
|
|
payer,
|
|
receiver,
|
|
lpAmount,
|
|
inputTokenIndex,
|
|
deadline,
|
|
swapFeePpm
|
|
);
|
|
|
|
bytes memory result = Address.functionDelegateCall(swapMintImpl, data);
|
|
return abi.decode(result, (uint256));
|
|
}
|
|
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function flashRepaymentAmounts(uint256[] memory loanAmounts) external view
|
|
returns (uint256[] memory repaymentAmounts) {
|
|
repaymentAmounts = new uint256[](tokens.length);
|
|
for (uint256 i = 0; i < tokens.length; i++) {
|
|
uint256 amount = loanAmounts[i];
|
|
if (amount > 0) {
|
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// @notice Receive token amounts and require them to be repaid plus a fee inside a callback.
|
|
/// @dev The caller must implement IPartyFlashCallback#partyFlashCallback which receives (amounts, repaymentAmounts, data).
|
|
/// This function verifies that, after the callback returns, the pool's balances have increased by at least the fees
|
|
/// for each borrowed token. Reverts if repayment (including fee) did not occur.
|
|
/// @param recipient The address which will receive the token amounts
|
|
/// @param amounts The amount of each token to send (array length must equal pool size)
|
|
/// @param data Any data to be passed through to the callback
|
|
// todo gas-efficient single-asset flash
|
|
// todo fix this func's gas
|
|
function flash(
|
|
address recipient,
|
|
uint256[] memory amounts,
|
|
bytes calldata data
|
|
) external nonReentrant {
|
|
require(recipient != address(0), "flash: zero recipient");
|
|
require(amounts.length == tokens.length, "flash: amounts length mismatch");
|
|
|
|
// Calculate repayment amounts for each token including fee
|
|
uint256[] memory repaymentAmounts = new uint256[](tokens.length);
|
|
|
|
// Store initial balances to verify repayment later
|
|
uint256[] memory initialBalances = new uint256[](tokens.length);
|
|
|
|
// Track if any token amount is non-zero
|
|
bool hasNonZeroAmount = false;
|
|
|
|
// Process each token, skipping those with zero amounts
|
|
for (uint256 i = 0; i < tokens.length; i++) {
|
|
uint256 amount = amounts[i];
|
|
|
|
if (amount > 0) {
|
|
hasNonZeroAmount = true;
|
|
|
|
// Calculate repayment amount with fee (ceiling)
|
|
repaymentAmounts[i] = amount + _ceilFee(amount, flashFeePpm);
|
|
|
|
// Record initial balance
|
|
initialBalances[i] = IERC20(tokens[i]).balanceOf(address(this));
|
|
|
|
// Transfer token to recipient
|
|
tokens[i].safeTransfer(recipient, amount);
|
|
}
|
|
}
|
|
|
|
// Ensure at least one token is being borrowed
|
|
require(hasNonZeroAmount, "flash: no tokens requested");
|
|
|
|
// Call flash callback with expected repayment amounts
|
|
IPartyFlashCallback(msg.sender).partyFlashCallback(amounts, repaymentAmounts, data);
|
|
|
|
// Verify repayment amounts for tokens that were borrowed
|
|
for (uint256 i = 0; i < tokens.length; i++) {
|
|
if (amounts[i] > 0) {
|
|
uint256 currentBalance = IERC20(tokens[i]).balanceOf(address(this));
|
|
|
|
// Verify repayment: current balance must be at least (initial balance + fee)
|
|
require(
|
|
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], flashFeePpm),
|
|
"flash: repayment failed"
|
|
);
|
|
|
|
// Update cached balance
|
|
cachedUintBalances[i] = currentBalance;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Conversion helpers moved to PartyPoolBase (abstract) to centralize internal helpers and storage. */
|
|
|
|
/// @notice Marginal price of `base` in terms of `quote` (p_quote / p_base) as Q64.64
|
|
/// @dev Returns the LMSR marginal price directly (raw 64.64) for use by off-chain quoting logic.
|
|
function price(uint256 baseTokenIndex, uint256 quoteTokenIndex) external view returns (int128) {
|
|
uint256 n = tokens.length;
|
|
require(baseTokenIndex < n && quoteTokenIndex < n, "price: idx");
|
|
require(lmsr.nAssets > 0, "price: uninit");
|
|
return lmsr.price(baseTokenIndex, quoteTokenIndex);
|
|
}
|
|
|
|
/// @notice Price of one LP token denominated in `quote` asset as Q64.64
|
|
/// @dev Computes LMSR poolPrice (quote per unit qTotal) and scales it by totalSupply() / qTotal
|
|
/// to return price per LP token unit in quote asset (raw 64.64).
|
|
function poolPrice(uint256 quoteTokenIndex) external view returns (int128) {
|
|
uint256 n = tokens.length;
|
|
require(quoteTokenIndex < n, "poolPrice: idx");
|
|
require(lmsr.nAssets > 0, "poolPrice: uninit");
|
|
|
|
// price per unit of qTotal (Q64.64) from LMSR
|
|
int128 pricePerQ = lmsr.poolPrice(quoteTokenIndex);
|
|
|
|
// total internal q (qTotal) as Q64.64
|
|
int128 qTotal = _computeSizeMetric(lmsr.qInternal);
|
|
require(qTotal > int128(0), "poolPrice: qTotal zero");
|
|
|
|
// totalSupply as Q64.64
|
|
uint256 supply = totalSupply();
|
|
require(supply > 0, "poolPrice: zero supply");
|
|
int128 supplyQ64 = ABDKMath64x64.fromUInt(supply);
|
|
|
|
// factor = totalSupply / qTotal (Q64.64)
|
|
int128 factor = supplyQ64.div(qTotal);
|
|
|
|
// price per LP token = pricePerQ * factor (Q64.64)
|
|
return pricePerQ.mul(factor);
|
|
}
|
|
|
|
}
|