protocol fees

This commit is contained in:
tim
2025-10-02 16:43:02 -04:00
parent b7e1b1cac2
commit c002d26daf
5 changed files with 178 additions and 22 deletions

View File

@@ -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()
);

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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