per-asset fees

This commit is contained in:
tim
2025-10-29 18:22:23 -04:00
parent 86410c9a91
commit 20758cfb35
18 changed files with 475 additions and 164 deletions

View File

@@ -54,9 +54,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 private immutable KAPPA; // kappa in Q64.64
function kappa() external view returns (int128) { return KAPPA; }
/// @notice Per-swap fee in parts-per-million (ppm). Fee is taken from input amounts before LMSR computations.
uint256 private immutable SWAP_FEE_PPM;
function swapFeePpm() external view returns (uint256) { return SWAP_FEE_PPM; }
/// @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 _pairFeePpm(i,j); }
/// @notice Flash-loan fee in parts-per-million (ppm) applied to flash borrow amounts.
uint256 private immutable FLASH_FEE_PPM;
@@ -100,7 +102,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @param symbol_ LP token symbol
/// @param tokens_ token addresses (n)
/// @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 fees_ per-asset swap fees in ppm (length must equal tokens_.length)
/// @param flashFeePpm_ fee in parts-per-million, taken for flash loans
/// @param swapImpl_ address of the SwapMint implementation contract
/// @param mintImpl_ address of the Mint implementation contract
@@ -110,7 +112,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
string memory symbol_,
IERC20[] memory tokens_,
int128 kappa_,
uint256 swapFeePpm_,
uint256[] memory fees_,
uint256 flashFeePpm_,
uint256 protocolFeePpm_,
address protocolFeeAddress_,
@@ -126,11 +128,17 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
require(tokens_.length > 1, "Pool: need >1 asset");
_tokens = tokens_;
KAPPA = kappa_;
require(swapFeePpm_ < 1_000_000, "Pool: fee >= ppm");
SWAP_FEE_PPM = swapFeePpm_;
require(flashFeePpm_ < 1_000_000, "Pool: flash fee >= ppm");
require(fees_.length == tokens_.length, "Pool: fees length");
// validate ppm bounds and assign
_fees = new uint256[](fees_.length);
for (uint256 i = 0; i < fees_.length; i++) {
// Cap all fees at 1%
require(fees_[i] < 10_000, "Pool: fee >= 1%");
_fees[i] = fees_[i];
}
require(flashFeePpm_ < 10_000, "Pool: flash fee >= 1%");
FLASH_FEE_PPM = flashFeePpm_;
require(protocolFeePpm_ < 1_000_000, "Pool: protocol fee >= ppm");
require(protocolFeePpm_ < 400_000, "Pool: protocol fee >= 40%");
// If the protocolFeePpm_ is set, then also require the fee address to be nonzero
require(protocolFeePpm_ == 0 || protocolFeeAddress_ != address(0));
PROTOCOL_FEE_PPM = protocolFeePpm_;
@@ -168,8 +176,10 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @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 {
_killed = true;
emit Killed();
if( !_killed ) {
_killed = true;
emit Killed();
}
}
/* ----------------------
@@ -232,7 +242,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
) external view returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) external view returns (uint256 amountIn, uint256 amountOut, uint256 inFee) {
(uint256 grossIn, uint256 outUint,,,, uint256 feeUint) = _quoteSwapExactIn(inputTokenIndex, outputTokenIndex, maxAmountIn, limitPrice);
return (grossIn, outUint, feeUint);
}
@@ -247,7 +257,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable native nonReentrant killable returns (uint256 amountIn, uint256 amountOut, uint256 fee) {
) 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
@@ -303,10 +313,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
uint256 outputTokenIndex,
uint256 maxAmountIn,
int128 limitPrice
)
internal
view
returns (
) internal view
returns (
uint256 grossIn,
uint256 amountOutUint,
int128 amountInInternalUsed,
@@ -316,7 +324,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
)
{
// Estimate max net input (fee on gross rounded up, then subtract)
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, SWAP_FEE_PPM);
uint256 pairFeePpm = _pairFeePpm(inputTokenIndex, outputTokenIndex);
(, uint256 netUintForSwap) = _computeFee(maxAmountIn, pairFeePpm);
// Convert to internal (floor)
int128 deltaInternalI = _uintToInternalFloor(netUintForSwap, _bases[inputTokenIndex]);
@@ -332,8 +341,8 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
// Compute gross transfer including fee on the used input (ceil)
feeUint = 0;
grossIn = amountInUintNoFee;
if (SWAP_FEE_PPM > 0) {
feeUint = _ceilFee(amountInUintNoFee, SWAP_FEE_PPM);
if (pairFeePpm > 0) {
feeUint = _ceilFee(amountInUintNoFee, pairFeePpm);
grossIn += feeUint;
}
@@ -354,7 +363,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
int128 limitPrice,
uint256 deadline,
bool unwrap
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 fee) {
) external payable returns (uint256 amountInUsed, uint256 amountOut, uint256 inFee) {
bytes memory data = abi.encodeWithSelector(
PartyPoolSwapImpl.swapToLimit.selector,
payer,
@@ -364,7 +373,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
limitPrice,
deadline,
unwrap,
SWAP_FEE_PPM,
_pairFeePpm(inputTokenIndex, outputTokenIndex),
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(SWAP_IMPL), data);
@@ -379,14 +388,14 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
/// @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)
/// @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 lpMinted) {
) external payable returns (uint256 amountInUsed, uint256 lpMinted, uint256 inFee) {
bytes memory data = abi.encodeWithSelector(
PartyPoolMintImpl.swapMint.selector,
payer,
@@ -394,12 +403,12 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
inputTokenIndex,
maxAmountIn,
deadline,
SWAP_FEE_PPM,
_assetFeePpm(inputTokenIndex),
PROTOCOL_FEE_PPM
);
bytes memory result = Address.functionDelegateCall(address(MINT_IMPL), data);
return abi.decode(result, (uint256));
return abi.decode(result, (uint256, uint256, uint256));
}
/// @notice Burn LP _tokens then swap the redeemed proportional basket into a single asset `inputTokenIndex` and send to receiver.
@@ -426,7 +435,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
outputTokenIndex,
deadline,
unwrap,
SWAP_FEE_PPM,
_assetFeePpm(outputTokenIndex),
PROTOCOL_FEE_PPM
);
@@ -435,13 +444,11 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
}
bytes32 internal constant FLASH_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
/**
* @dev Loan `amount` _tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the _tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
* @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the callback.
* @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
* @param tokenAddr The loan currency.
* @param amount The amount of _tokens lent.
* @param amount The amount of tokens lent.
* @param data A data parameter to be passed on to the `receiver` for any custom use.
*/
function flashLoan(
@@ -449,37 +456,19 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
address tokenAddr,
uint256 amount,
bytes calldata data
) external nonReentrant killable returns (bool)
) external returns (bool)
{
IERC20 token = IERC20(tokenAddr);
require(amount <= token.balanceOf(address(this)));
uint256 tokenIndex = _tokenAddressToIndexPlusOne[token];
require(tokenIndex != 0, 'flash: token not in pool');
tokenIndex -= 1;
(uint256 fee, ) = _computeFee(amount, FLASH_FEE_PPM);
// Compute protocol share of flash fee
uint256 protoShare = 0;
if (PROTOCOL_FEE_PPM > 0 && fee > 0) {
protoShare = (fee * PROTOCOL_FEE_PPM) / 1_000_000; // floor
if (protoShare > 0) {
_protocolFeesOwed[tokenIndex] += protoShare;
}
}
_sendTokenTo(token, address(receiver), amount, false);
require(receiver.onFlashLoan(msg.sender, address(token), amount, fee, data) == FLASH_CALLBACK_SUCCESS);
_receiveTokenFrom(address(receiver), token, amount + fee);
// Update cached balance for the borrowed token
uint256 balAfter = token.balanceOf(address(this));
// Inline _recordCachedBalance logic
require(balAfter >= _protocolFeesOwed[tokenIndex], "balance < protocol owed");
_cachedUintBalances[tokenIndex] = balAfter - _protocolFeesOwed[tokenIndex];
emit Flash(msg.sender, receiver, token, amount, fee-protoShare, protoShare);
return true;
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));
}
@@ -490,7 +479,7 @@ contract PartyPool is PartyPoolBase, OwnableExternal, ERC20External, IPartyPool
PartyPoolSwapImpl.collectProtocolFees.selector,
protocolFeeAddress
);
Address.functionDelegateCall(address(MINT_IMPL), data);
Address.functionDelegateCall(address(SWAP_IMPL), data);
}