461 lines
19 KiB
Solidity
461 lines
19 KiB
Solidity
// SPDX-License-Identifier: UNLICENSED
|
|
pragma solidity ^0.8.30;
|
|
|
|
import {ABDKMath64x64} from "../lib/abdk-libraries-solidity/ABDKMath64x64.sol";
|
|
import {IERC3156FlashBorrower} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
|
|
import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
|
|
import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import {Address} from "../lib/openzeppelin-contracts/contracts/utils/Address.sol";
|
|
import {ReentrancyGuard} from "../lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
|
|
import {ERC20External} from "./ERC20External.sol";
|
|
import {Funding} from "./Funding.sol";
|
|
import {IPartyPool} from "./IPartyPool.sol";
|
|
import {LMSRStabilized} from "./LMSRStabilized.sol";
|
|
import {NativeWrapper} from "./NativeWrapper.sol";
|
|
import {OwnableExternal} from "./OwnableExternal.sol";
|
|
import {OwnableInternal} from "./OwnableInternal.sol";
|
|
import {PartyPoolBase} from "./PartyPoolBase.sol";
|
|
import {PartyPoolMintImpl} from "./PartyPoolMintImpl.sol";
|
|
import {PartyPoolSwapImpl} from "./PartyPoolSwapImpl.sol";
|
|
import {IPartyPoolDeployer} from "./IPartyPoolDeployer.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,
|
|
/// - Exact-input swaps and swaps-to-price-limits,
|
|
/// - Single-token mint (swapMint) and single-asset withdrawal (burnSwap),
|
|
/// - ERC-3156 flash loans
|
|
///
|
|
/// @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). Mutating methods have re-entrancy locks.
|
|
/// The contract may be "killed" by the admin in case any security issue is discovered, in which case all swaps and
|
|
/// mints are disabled, and only the burn() method remains functional to allow LP's to withdraw their assets.
|
|
contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool {
|
|
using ABDKMath64x64 for int128;
|
|
using LMSRStabilized for LMSRStabilized.State;
|
|
using SafeERC20 for IERC20;
|
|
|
|
receive() external payable {}
|
|
|
|
/// @notice If true, the vault has been disabled by the owner and only burns (withdrawals) are allowed.
|
|
function killed() external view returns (bool) { return _killed; }
|
|
|
|
function wrapperToken() external view returns (NativeWrapper) { return WRAPPER; }
|
|
|
|
/// @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 private immutable KAPPA; // kappa in Q64.64
|
|
function kappa() external view returns (int128) { return KAPPA; }
|
|
|
|
/// @notice Per-asset swap fees in ppm.
|
|
function fees() external view returns (uint256[] memory) { return _fees; }
|
|
|
|
/// @notice Effective combined fee in ppm for (i as input, j as output)
|
|
function fee(uint256 i, uint256 j) external view returns (uint256) { return _pairFeePpmView(i,j); }
|
|
|
|
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
|
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 public protocolFeeAddress;
|
|
|
|
// @inheritdoc IPartyPool
|
|
function allProtocolFeesOwed() external view returns (uint256[] memory) { return _protocolFeesOwed; }
|
|
|
|
/// @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
|
|
PartyPoolSwapImpl private immutable SWAP_IMPL;
|
|
function swapMintImpl() external view returns (PartyPoolSwapImpl) { return SWAP_IMPL; }
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function token(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; }
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function LMSR() external view returns (LMSRStabilized.State memory) { return _lmsr; }
|
|
|
|
constructor()
|
|
{
|
|
IPartyPoolDeployer.DeployParams memory p = IPartyPoolDeployer(msg.sender).params();
|
|
uint256 n = p.tokens.length;
|
|
require(n > 1, "Pool: need >1 asset");
|
|
|
|
_nonce = p.nonce;
|
|
WRAPPER = p.wrapper;
|
|
_name = p.name;
|
|
_symbol = p.symbol;
|
|
|
|
ownableConstructor(p.owner);
|
|
|
|
_tokens = p.tokens;
|
|
KAPPA = p.kappa;
|
|
require(p.fees.length == p.tokens.length, "Pool: fees length");
|
|
// validate ppm bounds and assign
|
|
_fees = new uint256[](p.fees.length);
|
|
for (uint256 i = 0; i < p.fees.length; i++) {
|
|
// Cap all fees at 1%
|
|
require(p.fees[i] < 10_000, "Pool: fee >= 1%");
|
|
_fees[i] = p.fees[i];
|
|
}
|
|
require(p.flashFeePpm < 10_000, "Pool: flash fee >= 1%");
|
|
FLASH_FEE_PPM = p.flashFeePpm;
|
|
require(p.protocolFeePpm < 400_000, "Pool: protocol fee >= 40%");
|
|
// If the p.protocolFeePpm is set, then also require the fee address to be nonzero
|
|
require(p.protocolFeePpm == 0 || p.protocolFeeAddress != address(0));
|
|
PROTOCOL_FEE_PPM = p.protocolFeePpm;
|
|
protocolFeeAddress = p.protocolFeeAddress;
|
|
SWAP_IMPL = p.swapImpl;
|
|
MINT_IMPL = p.mintImpl;
|
|
|
|
// Initialize token address to index mapping
|
|
for (uint i = 0; i < n;) {
|
|
_tokenAddressToIndexPlusOne[p.tokens[i]] = i + 1;
|
|
unchecked {i++;}
|
|
}
|
|
|
|
// Allocate denominators (bases) to be computed during initialMint from initial deposits
|
|
_bases = new uint256[](n);
|
|
|
|
// Initialize caches to zero and protocol ledger
|
|
_cachedUintBalances = new uint256[](n);
|
|
_protocolFeesOwed = new uint256[](n);
|
|
}
|
|
|
|
//
|
|
// Admin operations
|
|
//
|
|
|
|
function setProtocolFeeAddress( address feeAddress ) external onlyOwner {
|
|
protocolFeeAddress = feeAddress;
|
|
}
|
|
|
|
/// @notice If a security problem is found, the vault owner may call this function to permanently disable swap and
|
|
/// mint functionality, leaving only burns (withdrawals) working.
|
|
function kill() external onlyOwner {
|
|
if( !_killed ) {
|
|
_killed = true;
|
|
emit Killed();
|
|
}
|
|
}
|
|
|
|
/* ----------------------
|
|
Initialization / Mint / Burn (LP token managed)
|
|
---------------------- */
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function initialMint(address receiver, uint256 lpTokens) external payable
|
|
returns (uint256 lpMinted) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolMintImpl.initialMint.selector,
|
|
receiver,
|
|
lpTokens,
|
|
KAPPA
|
|
);
|
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
return abi.decode(result, (uint256));
|
|
}
|
|
|
|
/// @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 payable
|
|
returns (uint256 lpMinted) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolMintImpl.mint.selector,
|
|
payer,
|
|
receiver,
|
|
lpTokenAmount,
|
|
deadline
|
|
);
|
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
return abi.decode(result, (uint256));
|
|
}
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function burn(address payer, address receiver, uint256 lpAmount, uint256 deadline, bool unwrap) external
|
|
returns (uint256[] memory withdrawAmounts) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolMintImpl.burn.selector,
|
|
payer,
|
|
receiver,
|
|
lpAmount,
|
|
deadline,
|
|
unwrap
|
|
);
|
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
return abi.decode(result, (uint256[]));
|
|
}
|
|
|
|
/* ----------------------
|
|
Swaps
|
|
---------------------- */
|
|
|
|
function swapAmounts(
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
int128 limitPrice
|
|
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
|
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, _pairFeePpmView(inputTokenIndex, outputTokenIndex));
|
|
return (grossIn, outUint, feeUint);
|
|
}
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function swap(
|
|
address payer,
|
|
bytes4 fundingSelector,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
int128 limitPrice,
|
|
uint256 deadline,
|
|
bool unwrap,
|
|
bytes memory cbData
|
|
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
|
|
require(deadline == 0 || block.timestamp <= deadline, "swap: deadline exceeded");
|
|
|
|
// Compute amounts using the same path as views
|
|
(uint256 totalTransferAmount, uint256 amountOutUint, int128 amountInInternalUsed, int128 amountOutInternal, , uint256 feeUint) =
|
|
_quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice, _pairFeePpm(inputTokenIndex, outputTokenIndex));
|
|
|
|
// Cache token references for fewer SLOADs
|
|
IERC20 tokenIn = _tokens[inputTokenIndex];
|
|
IERC20 tokenOut = _tokens[outputTokenIndex];
|
|
|
|
_receiveTokenFrom(payer, fundingSelector, inputTokenIndex, tokenIn, totalTransferAmount, limitPrice, cbData);
|
|
|
|
// Compute on-chain balances as: onchain = cached + owed (+/- transfer)
|
|
uint256 balIAfter = _cachedUintBalances[inputTokenIndex] + _protocolFeesOwed[inputTokenIndex] + totalTransferAmount;
|
|
uint256 balJAfter = _cachedUintBalances[outputTokenIndex] + _protocolFeesOwed[outputTokenIndex] - amountOutUint;
|
|
|
|
// Accrue protocol share (floor) from the fee on input token
|
|
uint256 protoShare = 0;
|
|
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0) {
|
|
protoShare = (feeUint * PROTOCOL_FEE_PPM) / 1_000_000; // floor
|
|
if (protoShare > 0) {
|
|
_protocolFeesOwed[inputTokenIndex] += protoShare;
|
|
}
|
|
}
|
|
|
|
// Inline _recordCachedBalance: ensure onchain >= owed then set cached = onchain - owed
|
|
require(balIAfter >= _protocolFeesOwed[inputTokenIndex], "balance < protocol owed");
|
|
_cachedUintBalances[inputTokenIndex] = balIAfter - _protocolFeesOwed[inputTokenIndex];
|
|
|
|
require(balJAfter >= _protocolFeesOwed[outputTokenIndex], "balance < protocol owed");
|
|
_cachedUintBalances[outputTokenIndex] = balJAfter - _protocolFeesOwed[outputTokenIndex];
|
|
|
|
// Apply swap to LMSR state with the internal amounts actually used
|
|
_lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
|
|
|
// Transfer output to receiver near the end
|
|
_sendTokenTo(tokenOut, receiver, amountOutUint, unwrap);
|
|
|
|
emit Swap(payer, receiver, tokenIn, tokenOut, totalTransferAmount,
|
|
amountOutUint, feeUint - protoShare, protoShare);
|
|
|
|
return (totalTransferAmount, 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,
|
|
uint256 feePpm
|
|
) internal view
|
|
returns (
|
|
uint256 grossIn,
|
|
uint256 amountOutUint,
|
|
int128 amountInInternalUsed,
|
|
int128 amountOutInternal,
|
|
uint256 amountInUintNoFee,
|
|
uint256 feeUint
|
|
)
|
|
{
|
|
// Estimate max net input (fee on gross rounded up, then subtract)
|
|
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, feePpm);
|
|
|
|
// 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)
|
|
// use the virtual method call so that the balanced pair optimization can override
|
|
(amountInInternalUsed, amountOutInternal) = _swapAmountsForExactInput(inputTokenIndex, outputTokenIndex, deltaInternalI, limitPrice);
|
|
|
|
// Convert actual used input internal -> uint (ceil)
|
|
amountInUintNoFee = _internalToUintCeil(amountInInternalUsed, _bases[inputTokenIndex]);
|
|
|
|
// Compute gross transfer including fee on the used input (ceil)
|
|
feeUint = 0;
|
|
grossIn = amountInUintNoFee;
|
|
if (feePpm > 0) {
|
|
feeUint = _ceilFee(amountInUintNoFee, feePpm);
|
|
grossIn += feeUint;
|
|
}
|
|
|
|
// Ensure within user max
|
|
require(grossIn <= maxAmountIn, "swap: transfer exceeds max");
|
|
|
|
// Compute output (floor)
|
|
amountOutUint = _internalToUintFloor(amountOutInternal, _bases[outputTokenIndex]);
|
|
}
|
|
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function swapToLimit(
|
|
address payer,
|
|
bytes4 fundingSelector,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 outputTokenIndex,
|
|
int128 limitPrice,
|
|
uint256 deadline,
|
|
bool unwrap,
|
|
bytes memory cbData
|
|
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolSwapImpl.swapToLimit.selector,
|
|
payer,
|
|
fundingSelector,
|
|
receiver,
|
|
inputTokenIndex,
|
|
outputTokenIndex,
|
|
limitPrice,
|
|
deadline,
|
|
unwrap,
|
|
cbData,
|
|
_pairFeePpm(inputTokenIndex, outputTokenIndex),
|
|
PROTOCOL_FEE_PPM
|
|
);
|
|
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
|
|
return abi.decode(result, (uint256,uint256,uint256));
|
|
}
|
|
|
|
|
|
/// @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 amountInUsed actual input used (uint256), lpMinted actual LP minted (uint256), inFee fee taken from the input (uint256)
|
|
function swapMint(
|
|
address payer,
|
|
address receiver,
|
|
uint256 inputTokenIndex,
|
|
uint256 maxAmountIn,
|
|
uint256 deadline
|
|
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolMintImpl.swapMint.selector,
|
|
payer,
|
|
receiver,
|
|
inputTokenIndex,
|
|
maxAmountIn,
|
|
deadline,
|
|
_assetFeePpm(inputTokenIndex),
|
|
PROTOCOL_FEE_PPM
|
|
);
|
|
|
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
return abi.decode(result, (uint256, uint256, uint256));
|
|
}
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function burnSwap(
|
|
address payer,
|
|
address receiver,
|
|
uint256 lpAmount,
|
|
uint256 outputTokenIndex,
|
|
uint256 deadline,
|
|
bool unwrap
|
|
) external returns (uint256 amountOut, uint256 outFee) {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolMintImpl.burnSwap.selector,
|
|
payer,
|
|
receiver,
|
|
lpAmount,
|
|
outputTokenIndex,
|
|
deadline,
|
|
unwrap,
|
|
_assetFeePpm(outputTokenIndex),
|
|
PROTOCOL_FEE_PPM
|
|
);
|
|
|
|
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
|
|
return abi.decode(result, (uint256,uint256));
|
|
}
|
|
|
|
|
|
/// @inheritdoc IPartyPool
|
|
function flashLoan(
|
|
IERC3156FlashBorrower receiver,
|
|
address tokenAddr,
|
|
uint256 amount,
|
|
bytes calldata data
|
|
) external returns (bool)
|
|
{
|
|
bytes memory payload = abi.encodeWithSelector(
|
|
PartyPoolSwapImpl.flashLoan.selector,
|
|
receiver,
|
|
tokenAddr,
|
|
amount,
|
|
data,
|
|
FLASH_FEE_PPM,
|
|
PROTOCOL_FEE_PPM
|
|
);
|
|
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), payload);
|
|
return abi.decode(result, (bool));
|
|
}
|
|
|
|
|
|
/// @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 {
|
|
bytes memory data = abi.encodeWithSelector(
|
|
PartyPoolSwapImpl.collectProtocolFees.selector,
|
|
protocolFeeAddress
|
|
);
|
|
Address.functionDelegateCall(address(SWAP_IMPL), data);
|
|
}
|
|
|
|
|
|
function _swapAmountsForExactInput(uint256 i, uint256 j, int128 a, int128 limitPrice) internal virtual view
|
|
returns (int128 amountIn, int128 amountOut) {
|
|
return _lmsr.swapAmountsForExactInput(i, j, a, limitPrice);
|
|
}
|
|
|
|
}
|