protocol fees
This commit is contained in:
@@ -12,7 +12,9 @@ library Deploy {
|
||||
function newPartyPlanner() internal returns (PartyPlanner) {
|
||||
return new PartyPlanner(
|
||||
new PartyPoolSwapMintImpl(),
|
||||
new PartyPoolMintImpl()
|
||||
new PartyPoolMintImpl(),
|
||||
0, // protocolFeePpm = 0 for deploy helper
|
||||
address(0) // protocolFeeAddress = address(0) for deploy helper
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +28,21 @@ library Deploy {
|
||||
uint256 _flashFeePpm,
|
||||
bool _stable
|
||||
) 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)
|
||||
uint256 protocolFeePpm = 0;
|
||||
address protocolAddr = address(0);
|
||||
|
||||
return new PartyPool(
|
||||
name_,
|
||||
symbol_,
|
||||
tokens_,
|
||||
bases_,
|
||||
_kappa,
|
||||
_swapFeePpm,
|
||||
_flashFeePpm,
|
||||
protocolFeePpm,
|
||||
protocolAddr,
|
||||
_stable,
|
||||
new PartyPoolSwapMintImpl(),
|
||||
new PartyPoolMintImpl()
|
||||
);
|
||||
|
||||
@@ -75,6 +75,16 @@ interface IPartyPool is IERC20Metadata {
|
||||
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
|
||||
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)
|
||||
/// @dev Pools are constructed with a κ value; this getter exposes the κ used by the pool.
|
||||
function kappa() external view returns (int128);
|
||||
|
||||
@@ -23,6 +23,14 @@ contract PartyPlanner is IPartyPlanner {
|
||||
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
|
||||
PartyPool[] private _allPools;
|
||||
IERC20[] private _allTokens;
|
||||
@@ -32,11 +40,22 @@ contract PartyPlanner is IPartyPlanner {
|
||||
|
||||
/// @param _swapMintImpl address of the SwapMint implementation contract to be used by all pools
|
||||
/// @param _mintImpl address of the Mint implementation contract to be used by all pools
|
||||
constructor(PartyPoolSwapMintImpl _swapMintImpl, PartyPoolMintImpl _mintImpl) {
|
||||
/// @param _protocolFeePpm protocol fee share (ppm) to be used for pools created by this planner
|
||||
/// @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).
|
||||
@@ -75,6 +94,8 @@ contract PartyPlanner is IPartyPlanner {
|
||||
_kappa,
|
||||
_swapFeePpm,
|
||||
_flashFeePpm,
|
||||
PROTOCOL_FEE_PPM,
|
||||
PROTOCOL_FEE_ADDRESS,
|
||||
_stable,
|
||||
PartyPoolSwapMintImpl(SWAP_MINT_IMPL),
|
||||
MINT_IMPL
|
||||
|
||||
@@ -46,6 +46,14 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
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.
|
||||
bool immutable private IS_STABLE_PAIR; // if true, the optimized LMSRStabilizedBalancedPair optimization path is enabled
|
||||
|
||||
@@ -88,6 +96,8 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
int128 kappa_,
|
||||
uint256 swapFeePpm_,
|
||||
uint256 flashFeePpm_,
|
||||
uint256 protocolFeePpm_, // NEW: protocol share of fees (ppm)
|
||||
address protocolFeeAddress_, // NEW: recipient for collected protocol tokens
|
||||
bool stable_,
|
||||
PartyPoolSwapMintImpl swapMintImpl_,
|
||||
PartyPoolMintImpl mintImpl_
|
||||
@@ -101,6 +111,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
SWAP_FEE_PPM = swapFeePpm_;
|
||||
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
|
||||
FLASH_FEE_PPM = flashFeePpm_;
|
||||
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
|
||||
PROTOCOL_FEE_PPM = protocolFeePpm_;
|
||||
PROTOCOL_FEE_ADDRESS = protocolFeeAddress_;
|
||||
IS_STABLE_PAIR = stable_ && tokens_.length == 2;
|
||||
SWAP_MINT_IMPL = swapMintImpl_;
|
||||
MINT_IMPL = mintImpl_;
|
||||
@@ -116,8 +129,9 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
unchecked {i++;}
|
||||
}
|
||||
|
||||
// Initialize caches to zero
|
||||
// Initialize caches to zero and protocol ledger
|
||||
cachedUintBalances = new uint256[](n);
|
||||
protocolFeesOwed = new uint256[](n);
|
||||
}
|
||||
|
||||
|
||||
@@ -212,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.
|
||||
/// @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.
|
||||
@@ -255,9 +299,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
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;
|
||||
// Accrue protocol share (floor) from the fee on input token
|
||||
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||
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
|
||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalUsed, amountOutInternal);
|
||||
@@ -302,9 +354,17 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
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;
|
||||
// Accrue protocol share (floor) from the fee on input token
|
||||
if (PROTOCOL_FEE_PPM > 0 && feeUint > 0 && PROTOCOL_FEE_ADDRESS != address(0)) {
|
||||
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
|
||||
lmsr.applySwap(inputTokenIndex, outputTokenIndex, amountInInternalMax, amountOutInternal);
|
||||
@@ -487,7 +547,22 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
);
|
||||
|
||||
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.
|
||||
@@ -516,7 +591,22 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -588,14 +678,25 @@ contract PartyPool is PartyPoolBase, IPartyPool {
|
||||
if (amounts[i] > 0) {
|
||||
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)
|
||||
require(
|
||||
currentBalance >= initialBalances[i] + _ceilFee(amounts[i], FLASH_FEE_PPM),
|
||||
currentBalance >= initialBalances[i] + feeExpected,
|
||||
"flash: repayment failed"
|
||||
);
|
||||
|
||||
// Update cached balance
|
||||
cachedUintBalances[i] = currentBalance;
|
||||
// Accrue protocol share (floor) of the flash fee
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
uint256 maxAmountIn,
|
||||
uint256 deadline,
|
||||
uint256 swapFeePpm
|
||||
) external returns (uint256 lpMinted) {
|
||||
) external returns (uint256 lpMinted, uint256 feeUintActual) {
|
||||
uint256 n = tokens.length;
|
||||
require(inputTokenIndex < n, "swapMint: idx");
|
||||
require(maxAmountIn > 0, "swapMint: input zero");
|
||||
@@ -62,7 +62,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
require(amountInUint > 0, "swapMint: input zero after internal conversion");
|
||||
|
||||
// Compute fee on the actual used input and total transfer amount (ceiling)
|
||||
uint256 feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||
feeUintActual = _ceilFee(amountInUint, swapFeePpm);
|
||||
uint256 totalTransfer = amountInUint + feeUintActual;
|
||||
require(totalTransfer > 0 && totalTransfer <= maxAmountIn, "swapMint: transfer exceeds max");
|
||||
|
||||
@@ -72,7 +72,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
uint256 balIAfter = IERC20(tokens[inputTokenIndex]).balanceOf(address(this));
|
||||
require(balIAfter == prevBalI + totalTransfer, "swapMint: non-standard tokenIn");
|
||||
|
||||
// Update cached uint balances for token inputTokenIndex (only inputTokenIndex changed externally)
|
||||
// Update cached uint balances for token inputTokenIndex (implementation writes onchain value; wrapper will set effective)
|
||||
cachedUintBalances[inputTokenIndex] = balIAfter;
|
||||
|
||||
// Compute old and new scaled size metrics to determine LP minted
|
||||
@@ -122,7 +122,8 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
emit Mint(payer, receiver, new uint256[](n), actualLpToMint);
|
||||
// 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
|
||||
@@ -252,7 +253,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
uint256 inputTokenIndex,
|
||||
uint256 deadline,
|
||||
uint256 swapFeePpm
|
||||
) external returns (uint256 amountOutUint) {
|
||||
) external returns (uint256 amountOutUint, uint256 feeTokenUint) {
|
||||
uint256 n = tokens.length;
|
||||
require(inputTokenIndex < n, "burnSwap: idx");
|
||||
require(lpAmount > 0, "burnSwap: zero lp");
|
||||
@@ -262,7 +263,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
require(supply > 0, "burnSwap: empty supply");
|
||||
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) // fraction of total supply to burn
|
||||
.mul(ABDKMath64x64.divu(1000000-swapFeePpm, 1000000)); // adjusted for fee
|
||||
|
||||
@@ -273,6 +274,12 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
amountOutUint = _internalToUintFloor(payoutInternal, bases[inputTokenIndex]);
|
||||
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
|
||||
tokens[inputTokenIndex].safeTransfer(receiver, amountOutUint);
|
||||
|
||||
@@ -288,6 +295,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
int128[] memory newQInternal = new int128[](n);
|
||||
for (uint256 idx = 0; idx < n; idx++) {
|
||||
uint256 bal = IERC20(tokens[idx]).balanceOf(address(this));
|
||||
// implementation writes raw onchain values; wrapper will set effective cached values
|
||||
cachedUintBalances[idx] = bal;
|
||||
newQInternal[idx] = _uintToInternalFloor(bal, bases[idx]);
|
||||
}
|
||||
@@ -307,7 +315,7 @@ contract PartyPoolSwapMintImpl is PartyPoolBase {
|
||||
}
|
||||
|
||||
emit Burn(payer, receiver, new uint256[](n), lpAmount);
|
||||
return amountOutUint;
|
||||
return (amountOutUint, feeTokenUint);
|
||||
}
|
||||
|
||||
/// @notice Pure version of _uintToInternalFloor for use in view functions
|
||||
|
||||
Reference in New Issue
Block a user