diff --git a/README.md b/README.md index 79282a4..792f8fb 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,16 @@ The fee function is designed to discourage monopolizing the pool with one asset. The fee functions for both operations are based on dominance coefficients `a` and `b`, which designate the ratio of how dominant a particular asset is before (`a`) and after (`b`) the operation. +## Single Asset or No Assets in the Pool +In the case where there is only one asset in the pool or the pool is empty, the fee structure is simplified to a flat rate. This is designed to encourage diversification in the pool and discourage monopolization by a single asset. + +For both deposit and redemption operations, a flat fee of 10% is applied. This means that regardless of the amount deposited or redeemed, the fee will always be 10% of that amount. +This flat fee structure serves two purposes: +1. Simplicity: It provides a straightforward and predictable fee calculation for users when there is only one asset or no assets in the pool. +2. Encouragement of Diversification: The flat fee encourages users to diversify the assets in the pool. If there are multiple assets in the pool, the fee calculation becomes more complex (as described in the sections below), potentially leading to lower fees for less dominant assets. + +Remember, the goal of this fee structure is to maintain a balanced composition in the pool and discourage monopolization by any single asset. + ## Mathematical Expressions ### Dominance Coefficients diff --git a/src/FeeCalculator.sol b/src/FeeCalculator.sol index 0cfd87f..f76ef39 100644 --- a/src/FeeCalculator.sol +++ b/src/FeeCalculator.sol @@ -20,10 +20,12 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { SD59x18 private depositFeeScale = sd(0.18 * 1e18); SD59x18 private depositFeeRatioScale = sd(0.99 * 1e18); + SD59x18 private singleAssetDepositRelativeFee = sd(0.1 * 1e18); SD59x18 private redemptionFeeScale = sd(0.3 * 1e18); - SD59x18 private redemptionFeeShift = sd(0.1 * 1e18);//-log10(0+0.1)=1 -> 10^-1 + SD59x18 private redemptionFeeShift = sd(0.1 * 1e18); //-log10(0+0.1)=1 -> 10^-1 SD59x18 private redemptionFeeConstant = redemptionFeeScale * (one + redemptionFeeShift).log10(); //0.0413926851582251=log10(1+0.1) + SD59x18 private singleAssetRedemptionRelativeFee = sd(0.1 * 1e18); address[] private _recipients; uint256[] private _shares; @@ -36,7 +38,7 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { require(recipients.length > 0, "Recipients and shares arrays must not be empty"); uint256 totalShares = 0; - for (uint i = 0; i < shares.length; i++) { + for (uint256 i = 0; i < shares.length; i++) { totalShares += shares[i]; } require(totalShares == 100, "Total shares must equal 100"); @@ -51,10 +53,18 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { /// @param depositAmount The amount to be deposited. /// @return recipients The addresses of the fee recipients. /// @return feesDenominatedInPoolTokens The amount of fees each recipient should receive. - function calculateDepositFees(address tco2, address pool, uint256 depositAmount) external override returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) { + function calculateDepositFees(address tco2, address pool, uint256 depositAmount) + external + override + returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) + { require(depositAmount > 0, "depositAmount must be > 0"); uint256 totalFee = getDepositFee(depositAmount, getTokenBalance(pool, tco2), getTotalSupply(pool)); + + require(totalFee <= depositAmount, "Fee must be lower or equal to deposit amount"); + require(totalFee > 0, "Fee must be greater than 0"); + return distributeFeeAmongShares(totalFee); } @@ -62,19 +72,23 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { /// @param totalFee The total fee to be distributed. /// @return recipients The addresses of the fee recipients. /// @return feesDenominatedInPoolTokens The amount of fees each recipient should receive. - function distributeFeeAmongShares(uint256 totalFee) private view returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) { + function distributeFeeAmongShares(uint256 totalFee) + private + view + returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) + { feesDenominatedInPoolTokens = new uint256[](_recipients.length); uint256 restFee = totalFee; - for (uint i = 0; i < _recipients.length; i++) { + for (uint256 i = 0; i < _recipients.length; i++) { feesDenominatedInPoolTokens[i] = (totalFee * _shares[i]) / 100; restFee -= feesDenominatedInPoolTokens[i]; } require(restFee >= 0); recipients = _recipients; - feesDenominatedInPoolTokens[0] += restFee;//we give rest of the fee (if any) to the first recipient + feesDenominatedInPoolTokens[0] += restFee; //we give rest of the fee (if any) to the first recipient } /// @notice Calculates the redemption fees for a given amount. @@ -83,10 +97,18 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { /// @param depositAmount The amount to be redeemed. /// @return recipients The addresses of the fee recipients. /// @return feesDenominatedInPoolTokens The amount of fees each recipient should receive. - function calculateRedemptionFee(address tco2, address pool, uint256 depositAmount) external override returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) { + function calculateRedemptionFee(address tco2, address pool, uint256 depositAmount) + external + override + returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens) + { require(depositAmount > 0, "depositAmount must be > 0"); uint256 totalFee = getRedemptionFee(depositAmount, getTokenBalance(pool, tco2), getTotalSupply(pool)); + + require(totalFee <= depositAmount, "Fee must be lower or equal to redemption amount"); + require(totalFee > 0, "Fee must be greater than 0"); + return distributeFeeAmongShares(totalFee); } @@ -112,8 +134,7 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { /// @param current The current balance of the pool. /// @param total The total supply of the pool. /// @return The calculated ratios. - function getRatiosDeposit(SD59x18 amount, SD59x18 current, SD59x18 total) private view returns (SD59x18, SD59x18) - { + function getRatiosDeposit(SD59x18 amount, SD59x18 current, SD59x18 total) private view returns (SD59x18, SD59x18) { SD59x18 a = total == zero ? zero : current / total; SD59x18 b = (current + amount) / (total + amount); @@ -125,7 +146,10 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { /// @param current The current balance of the pool. /// @param total The total supply of the pool. /// @return The calculated ratios. - function getRatiosRedemption(SD59x18 amount, SD59x18 current, SD59x18 total) private view returns (SD59x18, SD59x18) + function getRatiosRedemption(SD59x18 amount, SD59x18 current, SD59x18 total) + private + view + returns (SD59x18, SD59x18) { SD59x18 a = total == zero ? zero : current / total; SD59x18 b = (total - amount) == zero ? zero : (current - amount) / (total - amount); @@ -142,6 +166,14 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { require(total >= current); SD59x18 amount_float = sd(int256(amount)); + + if ( + current == total //single asset (or no assets) special case + ) { + uint256 fee = intoUint256(amount_float * singleAssetDepositRelativeFee); + return fee; + } + SD59x18 ta = sd(int256(current)); SD59x18 tb = ta + amount_float; @@ -153,10 +185,6 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { SD59x18 fee_float = depositFeeScale * (ta_log_a - tb_log_b); uint256 fee = intoUint256(fee_float); - - require(fee <= amount, "Fee must be lower or equal to deposit amount"); - require(fee > 0, "Fee must be greater than 0"); - return fee; } @@ -170,6 +198,14 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { require(amount <= current); SD59x18 amount_float = sd(int256(amount)); + + if ( + current == total //single asset (or no assets) special case + ) { + uint256 fee = intoUint256(amount_float * (singleAssetRedemptionRelativeFee)); + return fee; + } + SD59x18 ta = sd(int256(current)); SD59x18 tb = ta - amount_float; @@ -180,19 +216,16 @@ contract FeeCalculator is IDepositFeeCalculator, IRedemptionFeeCalculator { SD59x18 i_b = tb * (db + redemptionFeeShift).log10(); SD59x18 fee_float = redemptionFeeScale * (i_b - i_a) + redemptionFeeConstant * amount_float; - if (fee_float < zero) - { - if (fee_float / amount_float < sd(1e-6 * 1e18)) + if (fee_float < zero) { + if (fee_float / amount_float < sd(1e-6 * 1e18)) { //fee_float=zero_signed;//if the fee is negative but is less than 0.0001% of amount than it's basically 0 require(fee_float > zero, "Fee must be greater than 0"); - else + } else { require(fee_float > zero, "Total failure. Fee must be greater than 0 or at least close to it."); + } } uint256 fee = intoUint256(fee_float); - - require(fee <= amount, "Fee must be lower or equal to redemption amount"); - return fee; } } diff --git a/src/interfaces/IDepositFeeCalculator.sol b/src/interfaces/IDepositFeeCalculator.sol index 96ec6f8..faa99fe 100644 --- a/src/interfaces/IDepositFeeCalculator.sol +++ b/src/interfaces/IDepositFeeCalculator.sol @@ -15,5 +15,7 @@ interface IDepositFeeCalculator { /// @param depositAmount The amount to be deposited. /// @return recipients The addresses of the fee recipients. /// @return feesDenominatedInPoolTokens The amount of fees each recipient should receive. - function calculateDepositFees(address tco2, address pool, uint256 depositAmount) external returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens); + function calculateDepositFees(address tco2, address pool, uint256 depositAmount) + external + returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens); } diff --git a/src/interfaces/IRedemptionFeeCalculator.sol b/src/interfaces/IRedemptionFeeCalculator.sol index 5829e99..0af4ae5 100644 --- a/src/interfaces/IRedemptionFeeCalculator.sol +++ b/src/interfaces/IRedemptionFeeCalculator.sol @@ -15,5 +15,7 @@ interface IRedemptionFeeCalculator { /// @param depositAmount The amount to be redeemed. /// @return recipients The addresses of the fee recipients. /// @return feesDenominatedInPoolTokens The amount of fees each recipient should receive. - function calculateRedemptionFee(address tco2, address pool, uint256 depositAmount) external returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens); + function calculateRedemptionFee(address tco2, address pool, uint256 depositAmount) + external + returns (address[] memory recipients, uint256[] memory feesDenominatedInPoolTokens); } diff --git a/test/FeeCalculator.t.sol b/test/FeeCalculator.t.sol index 9d1d551..20b8939 100644 --- a/test/FeeCalculator.t.sol +++ b/test/FeeCalculator.t.sol @@ -7,13 +7,13 @@ pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; import {FeeCalculator} from "../src/FeeCalculator.sol"; -import { UD60x18, ud, intoUint256 } from "@prb/math/src/UD60x18.sol"; +import {UD60x18, ud, intoUint256} from "@prb/math/src/UD60x18.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MockPool is IERC20 { uint256 private _totalSupply; - function totalSupply() external view returns (uint256){ + function totalSupply() external view returns (uint256) { return _totalSupply; } @@ -37,7 +37,7 @@ contract MockPool is IERC20 { return true; } - function balanceOf(address account) external view returns (uint256){ + function balanceOf(address account) external view returns (uint256) { return 0; } } @@ -65,7 +65,7 @@ contract MockToken is IERC20 { return true; } - function totalSupply() external view returns (uint256){ + function totalSupply() external view returns (uint256) { return 0; } } @@ -74,8 +74,8 @@ contract FeeCalculatorTest is Test { UD60x18 private zero = ud(0); UD60x18 private one = ud(1e18); UD60x18 private redemptionFeeScale = ud(0.3 * 1e18); - UD60x18 private redemptionFeeShift = ud(0.1 * 1e18);//-log10(0+0.1)=1 -> 10^-1 - UD60x18 private redemptionFeeConstant = redemptionFeeScale.mul((one+redemptionFeeShift).log10()); //0.0413926851582251=log10(1+0.1) + UD60x18 private redemptionFeeShift = ud(0.1 * 1e18); //-log10(0+0.1)=1 -> 10^-1 + UD60x18 private redemptionFeeConstant = redemptionFeeScale * (one + redemptionFeeShift).log10(); //0.0413926851582251=log10(1+0.1) FeeCalculator public feeCalculator; MockPool public mockPool; @@ -99,14 +99,15 @@ contract FeeCalculatorTest is Test { function testCalculateDepositFeesNormalCase() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); - mockToken.setTokenBalance(address(mockPool), 500*1e18); + mockPool.setTotalSupply(1000 * 1e18); + mockToken.setTokenBalance(address(mockPool), 500 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -116,14 +117,15 @@ contract FeeCalculatorTest is Test { function testCalculateRedemptionFeesNormalCase() public { // Arrange // Set up your test data - uint256 redemptionAmount = 100*1e18; + uint256 redemptionAmount = 100 * 1e18; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); - mockToken.setTokenBalance(address(mockPool), 500*1e18); + mockPool.setTotalSupply(1000 * 1e18); + mockToken.setTokenBalance(address(mockPool), 500 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -137,17 +139,20 @@ contract FeeCalculatorTest is Test { // Set up mock pool mockPool.setTotalSupply(1e6 * 1e18); - mockToken.setTokenBalance(address(mockPool), 1*1e18); + mockToken.setTokenBalance(address(mockPool), 1 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); // Assert assertEq(recipients[0], feeRecipient); - assertApproxEqRel(fees[0], intoUint256((redemptionFeeScale + redemptionFeeConstant).mul(ud(redemptionAmount))), 1e15);//we allow 0.1% discrepancy + assertApproxEqRel( + fees[0], intoUint256((redemptionFeeScale + redemptionFeeConstant) * (ud(redemptionAmount))), 1e15 + ); //we allow 0.1% discrepancy } - function testCalculateRedemptionFees_FullMonopolization_ZeroFees() public { + function testCalculateRedemptionFees_FullMonopolization_FeesCappedAt10Percent() public { // Arrange // Set up your test data uint256 redemptionAmount = 1 * 1e18; @@ -157,33 +162,55 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e6 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); // Assert assertEq(recipients[0], feeRecipient); - assertEq(fees[0], 0); + assertEq(fees[0], redemptionAmount / 10); + } + + function testCalculateRedemptionFees_AlmostFullMonopolization_ZeroFees() public { + // Arrange + // Set up your test data + uint256 redemptionAmount = 1 * 1e18; + + // Set up mock pool + mockPool.setTotalSupply(1e6 * 1e18 + 1); + mockToken.setTokenBalance(address(mockPool), 1e6 * 1e18); + + // Act + try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { + // Assert + assertEq(recipients[0], feeRecipient); + assertEq(fees[0], 0); + fail("Exception should be thrown"); + } catch Error(string memory reason) { + assertEq("Fee must be greater than 0", reason); + } } function testCalculateRedemptionFees_CurrentSlightLessThanTotal_AmountSuperSmall_ShouldResultInException() public { //this test was producing negative redemption fees before rounding extremely small negative redemption fees to zero // Arrange // Set up your test data - uint256 redemptionAmount = 186843141273221600445448244614;//1.868e29 + uint256 redemptionAmount = 186843141273221600445448244614; //1.868e29 // Set up mock pool - mockPool.setTotalSupply(11102230246251565404236316680908203126);//1.11e37 - mockToken.setTokenBalance(address(mockPool), 11102230246251565403820829061134812052);//1.11e37 + mockPool.setTotalSupply(11102230246251565404236316680908203126); //1.11e37 + mockToken.setTokenBalance(address(mockPool), 11102230246251565403820829061134812052); //1.11e37 // Act - try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); assertEq(fees[0], 0); fail("Exception should be thrown"); - } - catch Error(string memory reason) - { + } catch Error(string memory reason) { assertEq("Fee must be greater than 0", reason); } } @@ -191,13 +218,13 @@ contract FeeCalculatorTest is Test { function testCalculateDepositFeesNormalCase_TwoFeeRecipientsSplitEqually() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; address feeRecipient1 = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; address feeRecipient2 = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); - mockToken.setTokenBalance(address(mockPool), 500*1e18); + mockPool.setTotalSupply(1000 * 1e18); + mockToken.setTokenBalance(address(mockPool), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -208,26 +235,27 @@ contract FeeCalculatorTest is Test { feeCalculator.feeSetup(_recipients, _feeShares); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient1); assertEq(recipients[1], feeRecipient2); assertEq(sumOf(fees), 9718378209069523938); - assertEq(fees[0], 9718378209069523938/2); - assertEq(fees[1], 9718378209069523938/2); + assertEq(fees[0], 9718378209069523938 / 2); + assertEq(fees[1], 9718378209069523938 / 2); } function testCalculateDepositFeesNormalCase_TwoFeeRecipientsSplit30To70() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; address feeRecipient1 = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; address feeRecipient2 = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); - mockToken.setTokenBalance(address(mockPool), 500*1e18); + mockPool.setTotalSupply(1000 * 1e18); + mockToken.setTokenBalance(address(mockPool), 500 * 1e18); address[] memory _recipients = new address[](2); _recipients[0] = feeRecipient1; @@ -238,27 +266,29 @@ contract FeeCalculatorTest is Test { feeCalculator.feeSetup(_recipients, _feeShares); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient1); assertEq(recipients[1], feeRecipient2); assertEq(sumOf(fees), 9718378209069523938); - assertEq(fees[0], uint256(9718378209069523938) * 30 / 100 + 1);//first recipient gets rest from division + assertEq(fees[0], uint256(9718378209069523938) * 30 / 100 + 1); //first recipient gets rest from division assertEq(fees[1], uint256(9718378209069523938) * 70 / 100); } function testCalculateDepositFeesComplicatedCase() public { // Arrange // Set up your test data - uint256 depositAmount = 932*1e18; + uint256 depositAmount = 932 * 1e18; // Set up mock pool - mockPool.setTotalSupply(53461*1e18); - mockToken.setTokenBalance(address(mockPool), 15462*1e18); + mockPool.setTotalSupply(53461 * 1e18); + mockToken.setTokenBalance(address(mockPool), 15462 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -275,15 +305,14 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); // Act - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); assertEq(fees[0], depositAmount); fail("Exception should be thrown"); - } - catch Error(string memory reason) - { + } catch Error(string memory reason) { assertEq("Fee must be greater than 0", reason); } } @@ -301,18 +330,21 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); // Act - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); assertEq(fees[0], depositAmount); fail("Exception should be thrown"); - } - catch Error(string memory reason) { + } catch Error(string memory reason) { assertEq("Fee must be greater than 0", reason); } } - function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) public { + function testCalculateDepositFees_FuzzyExtremelySmallDepositsToLargePool_ShouldThrowError(uint256 depositAmount) + public + { vm.assume(depositAmount <= 1e-14 * 1e18); vm.assume(depositAmount >= 10); @@ -322,19 +354,18 @@ contract FeeCalculatorTest is Test { // Arrange // Set up your test data - // Set up mock pool mockPool.setTotalSupply(1e12 * 1e18); mockToken.setTokenBalance(address(mockPool), 1e9 * 1e18); - - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); assertEq(fees[0], depositAmount); fail("Exception should be thrown"); - } - catch Error(string memory reason) { + } catch Error(string memory reason) { assertEq("Fee must be greater than 0", reason); } } @@ -349,7 +380,8 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -366,7 +398,8 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -387,7 +420,6 @@ contract FeeCalculatorTest is Test { mockPool.setTotalSupply(1e5 * 1e18); mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); - address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; _recipients[1] = feeRecipient2; @@ -403,7 +435,8 @@ contract FeeCalculatorTest is Test { feeCalculator.feeSetup(_recipients, _feeShares); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient1); @@ -433,7 +466,6 @@ contract FeeCalculatorTest is Test { mockPool.setTotalSupply(1e5 * 1e18); mockToken.setTokenBalance(address(mockPool), 1e4 * 1e18); - address[] memory _recipients = new address[](5); _recipients[0] = feeRecipient1; _recipients[1] = feeRecipient2; @@ -449,7 +481,8 @@ contract FeeCalculatorTest is Test { feeCalculator.feeSetup(_recipients, _feeShares); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient1); @@ -458,7 +491,7 @@ contract FeeCalculatorTest is Test { assertEq(recipients[3], feeRecipient4); assertEq(recipients[4], feeRecipient5); assertEq(sumOf(fees), 15880809772898785); - assertEq(fees[0], uint256(15880809772898785) * 15 / 100 + 3);//first recipient gets rest of fee + assertEq(fees[0], uint256(15880809772898785) * 15 / 100 + 3); //first recipient gets rest of fee assertEq(fees[1], uint256(15880809772898785) * 30 / 100); assertEq(fees[2], uint256(15880809772898785) * 50 / 100); assertEq(fees[3], uint256(15880809772898785) * 3 / 100); @@ -475,7 +508,8 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 1e6 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -488,66 +522,124 @@ contract FeeCalculatorTest is Test { uint256 depositAmount = 0; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); - mockToken.setTokenBalance(address(mockPool), 500*1e18); + mockPool.setTotalSupply(1000 * 1e18); + mockToken.setTokenBalance(address(mockPool), 500 * 1e18); // Act - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); assertEq(fees[0], 0); fail("Exception should be thrown"); - } - catch Error(string memory reason) { + } catch Error(string memory reason) { assertEq("depositAmount must be > 0", reason); } } - function testCalculateDepositFees_EmptyPool_FeeCappedAt36Percent() public { + function testCalculateDepositFees_EmptyPool_FeeCappedAt10Percent() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; // Set up mock pool mockPool.setTotalSupply(0); mockToken.setTokenBalance(address(mockPool), 0); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); - assertEq(fees[0], 36 * 1e18); + assertEq(fees[0], depositAmount / 10); } - function testCalculateDepositFees_TotalEqualCurrent_FeeCappedAt36Percent() public { + function testCalculateDepositFees_AlmostEmptyPool_FeeAlmostCappedAt36Percent() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; + + // Set up mock pool + mockPool.setTotalSupply(1); + mockToken.setTokenBalance(address(mockPool), 0); + + // Act + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + + // Assert + assertEq(recipients[0], feeRecipient); + assertEq(fees[0], 35999999999999999154); + } + + function testCalculateRedemptionFees_TotalEqualCurrent_FeeCappedAt10Percent() public { + // Arrange + // Set up your test data + uint256 redemptionAmount = 100; + + // Set up mock pool + mockPool.setTotalSupply(1000); + mockToken.setTokenBalance(address(mockPool), 1000); + + // Act + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount); + + // Assert + assertEq(recipients[0], feeRecipient); + assertEq(fees[0], redemptionAmount / 10); + } + + function testCalculateDepositFees_TotalEqualCurrent_FeeCappedAt10Percent() public { + // Arrange + // Set up your test data + uint256 depositAmount = 100 * 1e18; // Set up mock pool mockPool.setTotalSupply(1000); mockToken.setTokenBalance(address(mockPool), 1000); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); - assertEq(fees[0], 36*1e18); + assertEq(fees[0], depositAmount / 10); + } + + function testCalculateDepositFees_TotalAlmostEqualCurrent_FeeAlmostCappedAt36Percent() public { + // Arrange + // Set up your test data + uint256 depositAmount = 100 * 1e18; + + // Set up mock pool + mockPool.setTotalSupply(1000); + mockToken.setTokenBalance(address(mockPool), 999); + + // Act + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + + // Assert + assertEq(recipients[0], feeRecipient); + assertEq(fees[0], 35999999999999999161); } function testCalculateDepositFees_ZeroCurrent_NormalFees() public { // Arrange // Set up your test data - uint256 depositAmount = 100*1e18; + uint256 depositAmount = 100 * 1e18; // Set up mock pool - mockPool.setTotalSupply(1000*1e18); + mockPool.setTotalSupply(1000 * 1e18); mockToken.setTokenBalance(address(mockPool), 0); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert assertEq(recipients[0], feeRecipient); @@ -571,25 +663,37 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), current); // Act - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { // Assert assertEq(recipients[0], feeRecipient); - } - catch Error(string memory reason){ - assertTrue(keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) || - keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'"); + } catch Error(string memory reason) { + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + ); } } - function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoOneChunkFeesGreaterOrEqualToOneRedemption(uint128 _redemptionAmount, uint128 _current, uint128 _total) public { + function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoOneChunkFeesGreaterOrEqualToOneRedemption( + uint128 _redemptionAmount, + uint128 _current, + uint128 _total + ) public { //just a sanity check - testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption(1, _redemptionAmount, _current, _total); + testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( + 1, _redemptionAmount, _current, _total + ); } - function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption(uint8 numberOfRedemptions, uint128 _redemptionAmount, uint128 _current, uint128 _total) public { - + function testCalculateRedemptionFeesFuzzy_RedemptionDividedIntoMultipleChunksFeesGreaterOrEqualToOneRedemption( + uint8 numberOfRedemptions, + uint128 _redemptionAmount, + uint128 _current, + uint128 _total + ) public { vm.assume(0 < numberOfRedemptions); vm.assume(_total >= _current); vm.assume(_redemptionAmount <= _current); @@ -613,59 +717,74 @@ contract FeeCalculatorTest is Test { uint256 multipleTimesRedemptionFailedCount = 0; // Act - try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemptionAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { oneTimeFee = fees[0]; // Assert assertEq(recipients[0], feeRecipient); - } - catch Error(string memory reason) - { - oneTimeRedemptionFailed=true; - assertTrue(keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) || - keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'"); + } catch Error(string memory reason) { + oneTimeRedemptionFailed = true; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + ); } - uint256 equalRedemption = redemptionAmount / numberOfRedemptions; uint256 restRedemption = redemptionAmount % numberOfRedemptions; uint256 feeFromDividedRedemptions = 0; for (uint256 i = 0; i < numberOfRedemptions; i++) { - uint256 redemption = equalRedemption + (i==0 ? restRedemption : 0); - try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemption) returns (address[] memory recipients, uint256[] memory fees) - { + uint256 redemption = equalRedemption + (i == 0 ? restRedemption : 0); + try feeCalculator.calculateRedemptionFee(address(mockToken), address(mockPool), redemption) returns ( + address[] memory recipients, uint256[] memory fees + ) { feeFromDividedRedemptions += fees[0]; - total-=redemption; - current-=redemption; + total -= redemption; + current -= redemption; mockPool.setTotalSupply(total); mockToken.setTokenBalance(address(mockPool), current); - } - catch Error(string memory reason) { + } catch Error(string memory reason) { multipleTimesRedemptionFailedCount++; - assertTrue(keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) || - keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'"); + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + ); } } // Assert - if(multipleTimesRedemptionFailedCount==0 && !oneTimeRedemptionFailed) - { + if (multipleTimesRedemptionFailedCount == 0 && !oneTimeRedemptionFailed) { uint256 maximumAllowedErrorPercentage = (numberOfRedemptions <= 1) ? 0 : 1; - if(oneTimeFee + feeFromDividedRedemptions > 1e-8 * 1e18) // we skip assertion for extremely small fees (basically zero fees) because of numerical errors - assertGe((maximumAllowedErrorPercentage + 100)*feeFromDividedRedemptions/100, oneTimeFee);//we add 1% tolerance for numerical errors + if ( + oneTimeFee + feeFromDividedRedemptions > 1e-8 * 1e18 // we skip assertion for extremely small fees (basically zero fees) because of numerical errors + ) { + assertGe((maximumAllowedErrorPercentage + 100) * feeFromDividedRedemptions / 100, oneTimeFee); + } //we add 1% tolerance for numerical errors } } - function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit(uint256 depositAmount, uint256 current, uint256 total) public { + function testCalculateDepositFeesFuzzy_DepositDividedIntoOneChunkFeesGreaterOrEqualToOneDeposit( + uint256 depositAmount, + uint256 current, + uint256 total + ) public { //just a sanity check - testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit(1, depositAmount, current, total); + testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + 1, depositAmount, current, total + ); } - function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit(uint8 numberOfDeposits, uint256 depositAmount, uint256 current, uint256 total) public { + function testCalculateDepositFeesFuzzy_DepositDividedIntoMultipleChunksFeesGreaterOrEqualToOneDeposit( + uint8 numberOfDeposits, + uint256 depositAmount, + uint256 current, + uint256 total + ) public { vm.assume(0 < numberOfDeposits); vm.assume(total >= current); @@ -685,57 +804,61 @@ contract FeeCalculatorTest is Test { uint256 oneTimeFee = 0; // Act - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount) returns ( + address[] memory recipients, uint256[] memory fees + ) { oneTimeFee = fees[0]; // Assert assertEq(recipients[0], feeRecipient); + } catch Error(string memory reason) { + oneTimeDepositFailed = true; + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + ); } - catch Error(string memory reason) - { - oneTimeDepositFailed=true; - assertTrue(keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) || - keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'"); - } - uint256 equalDeposit = depositAmount / numberOfDeposits; uint256 restDeposit = depositAmount % numberOfDeposits; uint256 feeFromDividedDeposits = 0; for (uint256 i = 0; i < numberOfDeposits; i++) { - uint256 deposit = equalDeposit + (i==0 ? restDeposit : 0); + uint256 deposit = equalDeposit + (i == 0 ? restDeposit : 0); - try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), deposit) returns (address[] memory recipients, uint256[] memory fees) - { + try feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), deposit) returns ( + address[] memory recipients, uint256[] memory fees + ) { feeFromDividedDeposits += fees[0]; - total+=deposit; - current+=deposit; + total += deposit; + current += deposit; mockPool.setTotalSupply(total); mockToken.setTokenBalance(address(mockPool), current); - } - catch Error(string memory reason) { + } catch Error(string memory reason) { multipleTimesDepositFailedCount++; - assertTrue(keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) || - keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), - "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'"); + assertTrue( + keccak256(bytes("Fee must be greater than 0")) == keccak256(bytes(reason)) + || keccak256(bytes("Fee must be lower or equal to deposit amount")) == keccak256(bytes(reason)), + "error should be 'Fee must be greater than 0' or 'Fee must be lower or equal to deposit amount'" + ); } } // Assert - if(multipleTimesDepositFailedCount==0 && !oneTimeDepositFailed) - { + if (multipleTimesDepositFailedCount == 0 && !oneTimeDepositFailed) { uint256 maximumAllowedErrorPercentage = (numberOfDeposits <= 1) ? 0 : 2; - if(oneTimeFee + feeFromDividedDeposits > 1e-8 * 1e18) // we skip assertion for extremely small fees (basically zero fees) because of numerical errors - assertGe((maximumAllowedErrorPercentage + 100)*feeFromDividedDeposits/100, oneTimeFee);//we add 1% tolerance for numerical errors + if ( + oneTimeFee + feeFromDividedDeposits > 1e-8 * 1e18 // we skip assertion for extremely small fees (basically zero fees) because of numerical errors + ) { + assertGe((maximumAllowedErrorPercentage + 100) * feeFromDividedDeposits / 100, oneTimeFee); + } //we add 1% tolerance for numerical errors } } function sumOf(uint256[] memory numbers) public pure returns (uint256) { uint256 sum = 0; - for (uint i = 0; i < numbers.length; i++) { + for (uint256 i = 0; i < numbers.length; i++) { sum += numbers[i]; } return sum; @@ -743,22 +866,21 @@ contract FeeCalculatorTest is Test { function testFeeSetupFuzzy(address[] memory recipients, uint8 firstShare) public { vm.assume(recipients.length <= 100); - vm.assume(recipients.length > 1);//at least two recipients + vm.assume(recipients.length > 1); //at least two recipients vm.assume(firstShare <= 100); vm.assume(firstShare > 0); - uint256[] memory feeShares = new uint256[](recipients.length); uint256 shareLeft = 100 - firstShare; feeShares[0] = firstShare; - uint256 equalShare = shareLeft / (recipients.length-1); - uint256 leftShare = shareLeft % (recipients.length-1); + uint256 equalShare = shareLeft / (recipients.length - 1); + uint256 leftShare = shareLeft % (recipients.length - 1); - for(uint i=1; i < recipients.length; i++) { + for (uint256 i = 1; i < recipients.length; i++) { feeShares[i] = equalShare; } - feeShares[recipients.length-1] += leftShare;//last one gets additional share + feeShares[recipients.length - 1] += leftShare; //last one gets additional share feeCalculator.feeSetup(recipients, feeShares); uint256 depositAmount = 100 * 1e18; @@ -767,21 +889,21 @@ contract FeeCalculatorTest is Test { mockToken.setTokenBalance(address(mockPool), 100 * 1e18); // Act - (address[] memory recipients, uint256[] memory fees) = feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); + (address[] memory recipients, uint256[] memory fees) = + feeCalculator.calculateDepositFees(address(mockToken), address(mockPool), depositAmount); // Assert - for(uint i=0; i < recipients.length; i++) { + for (uint256 i = 0; i < recipients.length; i++) { assertEq(recipients[i], recipients[i]); } assertEq(sumOf(fees), 11526003792614720250); - assertApproxEqAbs(fees[0], 11526003792614720250 * uint256(firstShare) / 100, - recipients.length-1 + 1);//first fee might get the rest from division + assertApproxEqAbs(fees[0], 11526003792614720250 * uint256(firstShare) / 100, recipients.length - 1 + 1); //first fee might get the rest from division - for(uint i=1; i < recipients.length-1; i++) { + for (uint256 i = 1; i < recipients.length - 1; i++) { assertApproxEqAbs(fees[i], 11526003792614720250 * equalShare / 100, 1); } - assertApproxEqAbs(fees[recipients.length-1], 11526003792614720250 * (equalShare+leftShare) / 100, 1); + assertApproxEqAbs(fees[recipients.length - 1], 11526003792614720250 * (equalShare + leftShare) / 100, 1); } }