diff --git a/src/Deploy.sol b/src/Deploy.sol index 52e6f96..0f4121c 100644 --- a/src/Deploy.sol +++ b/src/Deploy.sol @@ -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() ); diff --git a/src/IPartyPool.sol b/src/IPartyPool.sol index f63059d..746f524 100644 --- a/src/IPartyPool.sol +++ b/src/IPartyPool.sol @@ -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); diff --git a/src/PartyPlanner.sol b/src/PartyPlanner.sol index 5180296..6797b23 100644 --- a/src/PartyPlanner.sol +++ b/src/PartyPlanner.sol @@ -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 diff --git a/src/PartyPool.sol b/src/PartyPool.sol index 3bf2a61..cf13337 100644 --- a/src/PartyPool.sol +++ b/src/PartyPool.sol @@ -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); } } } diff --git a/src/PartyPoolSwapMintImpl.sol b/src/PartyPoolSwapMintImpl.sol index 0db1d6d..c0168af 100644 --- a/src/PartyPoolSwapMintImpl.sol +++ b/src/PartyPoolSwapMintImpl.sol @@ -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