diff --git a/README.md b/README.md index 40078339..37d5db83 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,15 @@ The following outlines principles for core protocol funcitonality. Logic: -- Move PositionAdmin to services, rename +- [ ] Move PositionAdmin to services, rename - [ ] Consider changing short() to add() - [ ] Emit event when a position is created (get clear on whether or not an implicit event is emitted when creating a contract) +- [ ] Add comment in permit functions specifying that only ERC-2612 compliant tokens can use this functionality. Tests: -- [ ] Invariant: the clientTakeRate + userTakeRate = clientRate -- [ ] Invariant: the totalTokenAmt - sum(clientFeesToken) = (1 - clientRate) \* totalTokenAmt +- [x] Invariant: netProtocolFees = 1 - clientRate _ sum(clientTakeRate_i _ maxFee_i) ➡️ Test that netProtocolFees >= (1 - clientRate) \* totalBal +- [x] Integration: Test that the sum of all calculated client fees is equal to totalClientBalances - [x] Unit test setClientTakeRate() - [x] Unit test getClientAllocations() - [x] Unit test FeeLib via Test Harness diff --git a/src/FeeCollector.sol b/src/FeeCollector.sol index fa4e3321..cc35437b 100644 --- a/src/FeeCollector.sol +++ b/src/FeeCollector.sol @@ -62,12 +62,10 @@ contract FeeCollector is Ownable { /** * @notice Allows clients to set the percentage of the clientRate they will receive each revenue-generating tx. * Amounts less than 100 will give the calling client's users a protocol fee discount: - * clientTakeRateOfProtocolFee = clientRate * _clientTakeRate - * ex: _clientTakeRate = 50% -> clientTakeRate = clientRate * 0.5 - * userTakeRateOfProtocolFee = clientRate * (1 - _clientTakeRate) - * ex: _clientTakeRate = 50% -> userTakeRate = clientRate * (1 - 0.5) - * clientFee = protocolFee * clientTakeRateOfProtocolFee - * userSavings = protocolFee * userTakeRateOfProtocolFee + * clientPercentOfProtocolFee = clientRate * _clientTakeRate + * userPercentOfProtocolFee = clientRate * (1 - _clientTakeRate) + * clientFee = protocolFee * clientPercentOfProtocolFee + * userSavings = protocolFee * userPercentOfProtocolFee * @param _clientTakeRate The percentage of the clientRate the client will receive each revenue-generating tx (100 = 100%). */ function setClientTakeRate(uint256 _clientTakeRate) public payable { @@ -76,9 +74,12 @@ contract FeeCollector is Ownable { } /** - * @notice Returns the amount discounted from the protocol fee by using the provided client. + * @notice Returns the amount discounted from the protocol fee for using the provided client, + * and the amount of fees the client will receive. * @param _client The address where a client operator will receive protocols fees. * @param _maxFee The maximum amount of fees the protocol will collect. + * @return userSavings The amount of fees discounted from the protocol fee. + * @return clientFee The amount of fees the client will receive. */ function getClientAllocations(address _client, uint256 _maxFee) public @@ -87,8 +88,8 @@ contract FeeCollector is Ownable { { // 1. Calculate user savings uint256 userTakeRate = 100 - clientTakeRates[_client]; - uint256 userPercentOfProtocolFee = (userTakeRate * clientRate) / 100; - userSavings = (userPercentOfProtocolFee * _maxFee) / 100; + uint256 userPercentOfProtocolFee = (userTakeRate * clientRate); + userSavings = (userPercentOfProtocolFee * _maxFee) / 1e4; // 2. Calculate client fee uint256 maxClientFee = (_maxFee * clientRate) / 100; diff --git a/src/Position.sol b/src/Position.sol index f6ddbbf1..7f445e25 100644 --- a/src/Position.sol +++ b/src/Position.sol @@ -28,11 +28,11 @@ contract Position is DebtService, SwapService { /** * @notice Adds to this contract's short position. - * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _cAmt The amount of collateral token to be supplied for this transaction-specific loan (units: C_DECIMALS). * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). + * @param _client The address of the client operator. Use address(0) if not using a client. */ function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client) public @@ -57,15 +57,15 @@ contract Position is DebtService, SwapService { /** * @notice Adds to this contract's short position with permit, obviating the need for a separate approve tx. - * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _cAmt The amount of collateral token to be supplied for this transaction-specific loan (units: C_DECIMALS). * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _client The address of the client operator. Use address(0) if not using a client. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function shortWithPermit( uint256 _cAmt, diff --git a/src/interfaces/IFeeCollector.sol b/src/interfaces/IFeeCollector.sol index 195b60e2..90ccdb07 100644 --- a/src/interfaces/IFeeCollector.sol +++ b/src/interfaces/IFeeCollector.sol @@ -49,22 +49,23 @@ interface IFeeCollector { /** * @notice Allows clients to set the percentage of the clientRate they will receive each revenue-generating tx. * Amounts less than 100 will give the calling client's users a protocol fee discount: - * clientTakeRateOfProtocolFee = clientRate * _clientTakeRate - * ex: _clientTakeRate = 50% -> clientTakeRate = clientRate * 0.5 - * userTakeRateOfProtocolFee = clientRate * (1 - _clientTakeRate) - * ex: _clientTakeRate = 50% -> userTakeRate = clientRate * (1 - 0.5) - * clientFee = protocolFee * clientTakeRateOfProtocolFee - * userSavings = protocolFee * userTakeRateOfProtocolFee + * clientPercentOfProtocolFee = clientRate * _clientTakeRate + * userPercentOfProtocolFee = clientRate * (1 - _clientTakeRate) + * clientFee = protocolFee * clientPercentOfProtocolFee + * userSavings = protocolFee * userPercentOfProtocolFee * @param _clientTakeRate The percentage of the clientRate the client will receive each revenue-generating tx (100 = 100%). */ function setClientTakeRate(uint256 _clientTakeRate) external payable; /** - * @notice Returns the amount discounted from the protocol fee by using the provided client. + * @notice Returns the amount discounted from the protocol fee for using the provided client, + * and the amount of fees the client will receive. * @param _client The address where a client operator will receive protocols fees. - * @param _protocolFee The maximum amount of fees the protocol will collect. + * @param _maxFee The maximum amount of fees the protocol will collect. + * @return userSavings The amount of fees discounted from the protocol fee. + * @return clientFee The amount of fees the client will receive. */ - function getClientAllocations(address _client, uint256 _protocolFee) + function getClientAllocations(address _client, uint256 _maxFee) external view returns (uint256 userSavings, uint256 clientFee); diff --git a/src/interfaces/IPosition.sol b/src/interfaces/IPosition.sol index 29763478..0c88b225 100644 --- a/src/interfaces/IPosition.sol +++ b/src/interfaces/IPosition.sol @@ -32,11 +32,11 @@ interface IPosition { ******************************************************************************/ /** * @notice Adds to this contract's short position. - * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _cAmt The amount of collateral token to be supplied for this transaction-specific loan (units: C_DECIMALS). * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). + * @param _client The address of the client operator. Use address(0) if not using a client. */ function short(uint256 _cAmt, uint256 _ltv, uint256 _swapAmtOutMin, uint24 _poolFee, address _client) external @@ -44,15 +44,15 @@ interface IPosition { /** * @notice Adds to this contract's short position with permit, obviating the need for a separate approve tx. - * @param _cAmt The amount of collateral to be supplied for this transaction-specific loan (units: C_DECIMALS). + * @param _cAmt The amount of collateral token to be supplied for this transaction-specific loan (units: C_DECIMALS). * @param _ltv The desired loan-to-value ratio for this transaction-specific loan (ex: 75 is 75%). * @param _swapAmtOutMin The minimum amount of output tokens from swap for the tx to go through. * @param _poolFee The fee of the Uniswap pool. - * @param _client The address where a client operator will receive protocols fees. (use address(0) if no client). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _client The address of the client operator. Use address(0) if not using a client. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function shortWithPermit( uint256 _cAmt, @@ -78,7 +78,7 @@ interface IPosition { payable; /** - * @notice Increases the collateral amount for this contract's loan. + * @notice Increases the collateral amount backing this contract's loan. * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). */ function addCollateral(uint256 _cAmt) external payable; @@ -86,10 +86,10 @@ interface IPosition { /** * @notice Increases the collateral amount for this contract's loan with permit, obviating the need for a separate approve tx. * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function addCollateralWithPermit(uint256 _cAmt, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) external @@ -109,10 +109,10 @@ interface IPosition { * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). * To pay off entire debt, _dAmt = debtOwed + smallBuffer (to account for interest). * @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function repayAfterCloseWithPermit( uint256 _dAmt, diff --git a/src/interfaces/aave/IPool.sol b/src/interfaces/aave/IPool.sol index 5116053f..485f2d75 100644 --- a/src/interfaces/aave/IPool.sol +++ b/src/interfaces/aave/IPool.sol @@ -254,12 +254,12 @@ interface IPool { * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens * is a different wallet - * @param deadline The deadline timestamp that the permit is valid. + * @param deadline The expiration timestamp of the permit. * @param referralCode Code used to register the integrator originating the operation, for potential rewards. * 0 if the action is executed directly by the user, without any middle-man - * @param permitV The V parameter of ERC712 permit signature. - * @param permitR The R parameter of ERC712 permit signature. - * @param permitS The S parameter of ERC712 permit signature. + * @param permitV The V parameter of ERC712 signature for the permit. + * @param permitR The R parameter of ERC712 signature for the permit. + * @param permitS The S parameter of ERC712 signature for the permit. * */ function supplyWithPermit( @@ -333,10 +333,10 @@ interface IPool { * @param onBehalfOf Address of the user who will get his debt reduced/removed. Should be the address of the * user calling the function if he wants to reduce/remove his own debt, or the address of any other * other borrower whose debt should be removed - * @param deadline The deadline timestamp that the permit is valid. - * @param permitV The V parameter of ERC712 permit signature. - * @param permitR The R parameter of ERC712 permit signature. - * @param permitS The S parameter of ERC712 permit signature. + * @param deadline The expiration timestamp of the permit. + * @param permitV The V parameter of ERC712 signature for the permit. + * @param permitR The R parameter of ERC712 signature for the permit. + * @param permitS The S parameter of ERC712 signature for the permit. * @return The final amount repaid * */ diff --git a/src/services/DebtService.sol b/src/services/DebtService.sol index 2408af4c..ca6cee6e 100644 --- a/src/services/DebtService.sol +++ b/src/services/DebtService.sol @@ -112,7 +112,7 @@ contract DebtService is PositionAdmin { } /** - * @notice Increases the collateral amount for this contract's loan. + * @notice Increases the collateral amount backing this contract's loan. * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). */ function addCollateral(uint256 _cAmt) public payable onlyOwner { @@ -129,10 +129,10 @@ contract DebtService is PositionAdmin { /** * @notice Increases the collateral amount for this contract's loan with permit, obviating the need for a separate approve tx. * @param _cAmt The amount of collateral to be supplied (units: C_DECIMALS). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function addCollateralWithPermit(uint256 _cAmt, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s) public @@ -166,10 +166,10 @@ contract DebtService is PositionAdmin { * @param _dAmt The amount of debt token to repay to Aave (units: D_DECIMALS). * To pay off entire debt, _dAmt = debtOwed + smallBuffer (to account for interest). * @param _withdrawBuffer The amount of collateral left as safety buffer for tx to go through (default = 100_000, units: 8 decimals). - * @param _deadline The deadline timestamp that the permit is valid. - * @param _v The V parameter of ERC712 permit signature. - * @param _r The R parameter of ERC712 permit signature. - * @param _s The S parameter of ERC712 permit signature. + * @param _deadline The expiration timestamp of the permit. + * @param _v The V parameter of ERC712 signature for the permit. + * @param _r The R parameter of ERC712 signature for the permit. + * @param _s The S parameter of ERC712 signature for the permit. */ function repayAfterCloseWithPermit( uint256 _dAmt, diff --git a/test/FeeCollector.t.sol b/test/FeeCollector.t.sol index d75d5137..a183768c 100644 --- a/test/FeeCollector.t.sol +++ b/test/FeeCollector.t.sol @@ -295,15 +295,16 @@ contract FeeCollectorTest is Test, TokenUtils, FeeUtils { // Expectations uint256 maxClientFee = (CLIENT_RATE * _maxFee) / 100; - (uint256 expectedUserSavings, uint256 expectedClientFee) = - _getExpectedClientAllocations(_maxFee, _clientTakeRate); + uint256 userTakeRate = 100 - _clientTakeRate; + uint256 expectedClientFee = (_clientTakeRate * CLIENT_RATE * _maxFee) / 1e4; + uint256 expectedUserSavings = (userTakeRate * CLIENT_RATE * _maxFee) / 1e4; // Act (uint256 userSavings, uint256 clientFee) = feeCollector.getClientAllocations(TEST_CLIENT, _maxFee); // Assertions - assertEq(userSavings, expectedUserSavings); - assertEq(clientFee, expectedClientFee); + assertApproxEqAbs(userSavings, expectedUserSavings, 1); + assertApproxEqAbs(clientFee, expectedClientFee, 1); assertEq(userSavings + clientFee, maxClientFee); assertLe(userSavings, maxClientFee); assertLe(clientFee, maxClientFee); diff --git a/test/common/utils/FeeUtils.t.sol b/test/common/utils/FeeUtils.t.sol index 53345ee1..562adb83 100644 --- a/test/common/utils/FeeUtils.t.sol +++ b/test/common/utils/FeeUtils.t.sol @@ -14,8 +14,8 @@ contract FeeUtils is Test { returns (uint256 userSavings, uint256 clientFee) { uint256 userTakeRate = 100 - _clientTakeRate; - uint256 userPercentOfProtocolFee = (userTakeRate * CLIENT_RATE) / 100; - userSavings = (userPercentOfProtocolFee * _maxFee) / 100; + uint256 userPercentOfProtocolFee = (userTakeRate * CLIENT_RATE); + userSavings = (userPercentOfProtocolFee * _maxFee) / 1e4; // 2. Calculate client fee uint256 maxClientFee = (_maxFee * CLIENT_RATE) / 100; diff --git a/test/integration/FeeCollector.clientFees.t.sol b/test/integration/FeeCollector.clientFees.t.sol new file mode 100644 index 00000000..5fe73470 --- /dev/null +++ b/test/integration/FeeCollector.clientFees.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; + +// Local Imports +import { FeeCollector } from "src/FeeCollector.sol"; +import { Assets, CONTRACT_DEPLOYER } from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract FeeCollectorClientFeesTest is Test, TokenUtils { + /* solhint-disable func-name-mixedcase */ + + // Test contracts + FeeCollector public feeCollector; + Assets public assets; + + // Test Storage + address[] public supportedAssets; + uint256 public mainnetFork; + address public owner = address(this); + address public feeCollectorAddr; + + function setUp() public { + // Setup: use mainnet fork + mainnetFork = vm.createFork(vm.envString("RPC_URL")); + vm.selectFork(mainnetFork); + + // Deploy assets + assets = new Assets(); + supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + feeCollectorAddr = address(feeCollector); + } + + /// @dev + // - The active fork should be the forked network created in the setup + function test_ActiveFork() public { + assertEq(vm.activeFork(), mainnetFork, "vm.activeFork() != mainnetFork"); + } + + /// @dev + // - The sum of all client fees should equal totalClientBalances + // - The expectedClientFee should always be equal to or 1 less than clientFee, due to integer division + function testFuzz_ClientProtocolFeeBalances( + uint256 _feeAmount, + uint256 _time, + uint256 _clientRate, + uint256 _clientTakeRate, + address _client + ) public { + for (uint256 i; i < supportedAssets.length; i++) { + // Bounds + _clientRate = bound(_clientRate, 30, 100); + _clientTakeRate = bound(_clientTakeRate, 0, 100); + vm.assume(_client != address(0)); + + // Setup + address feeToken = supportedAssets[i]; + // uint256 clientTakeRate = 50; + + // Set client rate + vm.prank(CONTRACT_DEPLOYER); + feeCollector.setClientRate(_clientRate); + + // Set client take rate + vm.prank(_client); + feeCollector.setClientTakeRate(_clientTakeRate); + + // uint256 clientFeeSum; + uint256 expClientsBal = 0; + for (uint256 j; j < 100; j++) { + // Bounds + _feeAmount = bound(_feeAmount, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + _time = bound(_time, 1 minutes, 52 weeks); + + // Setup + _fund(owner, feeToken, _feeAmount); + + // Take fees + IERC20(feeToken).approve(feeCollectorAddr, _feeAmount); + (, uint256 clientFee) = feeCollector.getClientAllocations(_client, _feeAmount); + feeCollector.collectFees(_client, feeToken, _feeAmount, clientFee); + + // Calculate expected client fee + uint256 expectedClientFee = (_clientTakeRate * _clientRate * _feeAmount) / 1e4; + + // The expectedClientFee should always be equal to or 1 less than clientFee, due to integer division + assertApproxEqAbs(expectedClientFee, clientFee, 1); + expClientsBal += clientFee; + + // Go forward in time (should be time invariant) + skip(_time); + } + + // Get balances + uint256 clientsBal = feeCollector.totalClientBalances(feeToken); + + // Assertions + assertEq(expClientsBal, clientsBal, "expClientsBal != clientsBal"); + } + } +} diff --git a/test/invariant/FeeCollector.netProtocolFees.t.sol b/test/invariant/FeeCollector.netProtocolFees.t.sol new file mode 100644 index 00000000..a6080e9e --- /dev/null +++ b/test/invariant/FeeCollector.netProtocolFees.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; + +// Local Imports +import { FeeCollector } from "src/FeeCollector.sol"; +import { Assets, CONTRACT_DEPLOYER } from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract FeeCollectorNetProtocolFeesTest is Test, TokenUtils { + /* solhint-disable func-name-mixedcase */ + + // Test contracts + FeeCollector public feeCollector; + Assets public assets; + + // Test Storage + address[] public supportedAssets; + uint256 public mainnetFork; + address public owner = address(this); + address public feeCollectorAddr; + + function setUp() public { + // Setup: use mainnet fork + mainnetFork = vm.createFork(vm.envString("RPC_URL")); + vm.selectFork(mainnetFork); + + // Deploy assets + assets = new Assets(); + supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + feeCollectorAddr = address(feeCollector); + } + + /// @dev + // - The active fork should be the forked network created in the setup + function test_ActiveFork() public { + assertEq(vm.activeFork(), mainnetFork, "vm.activeFork() != mainnetFork"); + } + + /// @dev + // - Invariant: netProtocolFees = 1 - clientRate * sum(clientTakeRate_i * maxFee_i) + // - Ensure netProtocolFees >= (1 - clientRate) * totalBal + // - The above should be true for a large range of maxFee, clienRate, clientTakeRate, time, and clients. + function testFuzz_NetProtocolFeesInvariant( + uint256 _maxFee, + uint256 _clientRate, + uint256 _clientTakeRate, + uint256 _time, + address _client + ) public { + for (uint256 i; i < supportedAssets.length; i++) { + // Bounds + _clientRate = bound(_clientRate, 30, 100); + _clientTakeRate = bound(_clientTakeRate, 0, 100); + + // Setup + address feeToken = supportedAssets[i]; + + // Set client rates + vm.prank(CONTRACT_DEPLOYER); + feeCollector.setClientRate(_clientRate); + + vm.prank(_client); + feeCollector.setClientTakeRate(_clientTakeRate); + + for (uint256 j; j < 100; j++) { + // Bounds + _maxFee = bound(_maxFee, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + _time = bound(_time, 1 minutes, 52 weeks); + + // Setup + _fund(owner, feeToken, _maxFee); + + // Take fees + IERC20(feeToken).approve(feeCollectorAddr, _maxFee); + (, uint256 clientFee) = feeCollector.getClientAllocations(_client, _maxFee); + feeCollector.collectFees(_client, feeToken, _maxFee, clientFee); + + // Go forward in time (should be time invariant) + skip(_time); + } + + // Get balances + uint256 totalBal = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 clientsBal = feeCollector.totalClientBalances(feeToken); + + // Calculated expected and gather actual + uint256 netProtocolBal = totalBal - clientsBal; + uint256 netProtocolMinPercent = 100 - _clientRate; + uint256 netProtocolMinFees = (netProtocolMinPercent * totalBal) / 100; + + // Assertions + assertGe(netProtocolBal, netProtocolMinFees, "netProtocolBal < netProtocolMinFees"); + } + } +}