From f5ee5994fd641ca47939ca96ac3e0daeef53c2ab Mon Sep 17 00:00:00 2001 From: cucupac <46691282+cucupac@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:20:56 -0600 Subject: [PATCH] test: test fee collector with clientTakeRate --- README.md | 8 +- src/FeeCollector.sol | 18 ++-- src/interfaces/IFeeCollector.sol | 7 +- src/libraries/FeeLib.sol | 10 +-- test/FeeCollector.t.sol | 75 ++++++++++++++-- test/common/utils/FeeUtils.t.sol | 24 +++++ test/integration/FeeCollector.short.t.sol | 103 ++++++++++++++------- test/libraries/FeeLib.t.sol | 105 ++++++++++++++++++++++ 8 files changed, 298 insertions(+), 52 deletions(-) create mode 100644 test/common/utils/FeeUtils.t.sol create mode 100644 test/libraries/FeeLib.t.sol diff --git a/README.md b/README.md index 23bc601..4007833 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ Tests: - [ ] Invariant: the clientTakeRate + userTakeRate = clientRate - [ ] Invariant: the totalTokenAmt - sum(clientFeesToken) = (1 - clientRate) \* totalTokenAmt -- [ ] Unit test setClientTakeRate() -- [ ] Unit test getUserSavings() -- [ ] Unit test FeeLib via Test Harness -- [ ] Account for userSavings in all affected FeeCollector unit and integration tests +- [x] Unit test setClientTakeRate() +- [x] Unit test getClientAllocations() +- [x] Unit test FeeLib via Test Harness +- [x] Account for userSavings in all affected FeeCollector unit and integration tests Considerations: diff --git a/src/FeeCollector.sol b/src/FeeCollector.sol index 4f1b033..fa4e332 100644 --- a/src/FeeCollector.sol +++ b/src/FeeCollector.sol @@ -33,15 +33,14 @@ contract FeeCollector is Ownable { * @param _token The token to collect fees in (the collateral token of the calling Position contract). * @param _amt The total amount of fees to collect. */ - function collectFees(address _client, address _token, uint256 _amt) external payable { + function collectFees(address _client, address _token, uint256 _amt, uint256 _clientFee) external payable { // 1. Transfer tokens to this contract SafeTransferLib.safeTransferFrom(ERC20(_token), msg.sender, address(this), _amt); // 2. Update client balances if (_client != address(0)) { - uint256 clientFee = (_amt * clientRate) / 100; - balances[_client][_token] += clientFee; - totalClientBalances[_token] += clientFee; + balances[_client][_token] += _clientFee; + totalClientBalances[_token] += _clientFee; } } @@ -81,10 +80,19 @@ contract FeeCollector is Ownable { * @param _client The address where a client operator will receive protocols fees. * @param _maxFee The maximum amount of fees the protocol will collect. */ - function getUserSavings(address _client, uint256 _maxFee) public view returns (uint256 userSavings) { + function getClientAllocations(address _client, uint256 _maxFee) + public + view + returns (uint256 userSavings, uint256 clientFee) + { + // 1. Calculate user savings uint256 userTakeRate = 100 - clientTakeRates[_client]; uint256 userPercentOfProtocolFee = (userTakeRate * clientRate) / 100; userSavings = (userPercentOfProtocolFee * _maxFee) / 100; + + // 2. Calculate client fee + uint256 maxClientFee = (_maxFee * clientRate) / 100; + clientFee = maxClientFee - userSavings; } /* **************************************************************************** diff --git a/src/interfaces/IFeeCollector.sol b/src/interfaces/IFeeCollector.sol index b8397ac..195b60e 100644 --- a/src/interfaces/IFeeCollector.sol +++ b/src/interfaces/IFeeCollector.sol @@ -39,7 +39,7 @@ interface IFeeCollector { * @param _token The token to collect fees in (the collateral token of the calling Position contract). * @param _amt The total amount of fees to collect. */ - function collectFees(address _client, address _token, uint256 _amt) external payable; + function collectFees(address _client, address _token, uint256 _amt, uint256 _clientFee) external payable; /** * @notice Withdraw collected fees from this contract. * @param _token The token address to withdraw. @@ -64,7 +64,10 @@ interface IFeeCollector { * @param _client The address where a client operator will receive protocols fees. * @param _protocolFee The maximum amount of fees the protocol will collect. */ - function getUserSavings(address _client, uint256 _protocolFee) external view returns (uint256 userSavings); + function getClientAllocations(address _client, uint256 _protocolFee) + external + view + returns (uint256 userSavings, uint256 clientFee); /* **************************************************************************** ** diff --git a/src/libraries/FeeLib.sol b/src/libraries/FeeLib.sol index 35a73f2..b634e91 100644 --- a/src/libraries/FeeLib.sol +++ b/src/libraries/FeeLib.sol @@ -20,10 +20,10 @@ library FeeLib { */ function takeProtocolFee(address _token, uint256 _cAmt, address _client) internal returns (uint256 cAmtNet) { uint256 maxFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; - uint256 userSavings = IFeeCollector(FEE_COLLECTOR).getUserSavings(_client, maxFee); - uint256 fee = maxFee - userSavings; - cAmtNet = _cAmt - fee; - SafeTransferLib.safeApprove(ERC20(_token), FEE_COLLECTOR, fee); - IFeeCollector(FEE_COLLECTOR).collectFees(_client, _token, fee); + (uint256 userSavings, uint256 clientFee) = IFeeCollector(FEE_COLLECTOR).getClientAllocations(_client, maxFee); + uint256 totalFee = maxFee - userSavings; + cAmtNet = _cAmt - totalFee; + SafeTransferLib.safeApprove(ERC20(_token), FEE_COLLECTOR, totalFee); + IFeeCollector(FEE_COLLECTOR).collectFees(_client, _token, totalFee, clientFee); } } diff --git a/test/FeeCollector.t.sol b/test/FeeCollector.t.sol index cc59f9d..d75d513 100644 --- a/test/FeeCollector.t.sol +++ b/test/FeeCollector.t.sol @@ -9,9 +9,10 @@ import { PositionFactory } from "src/PositionFactory.sol"; import { FeeCollector } from "src/FeeCollector.sol"; import { Assets, CONTRACT_DEPLOYER, TEST_CLIENT, CLIENT_RATE, USDC, WETH, WBTC } from "test/common/Constants.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { FeeUtils } from "test/common/utils/FeeUtils.t.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; -contract FeeCollectorTest is Test, TokenUtils { +contract FeeCollectorTest is Test, TokenUtils, FeeUtils { /* solhint-disable func-name-mixedcase */ struct TestPosition { @@ -106,7 +107,7 @@ contract FeeCollectorTest is Test, TokenUtils { uint256 preClientFeeBalance = feeCollector.balances(TEST_CLIENT, feeToken); // Act: collect fees - feeCollector.collectFees(TEST_CLIENT, feeToken, _protocolFee); + feeCollector.collectFees(TEST_CLIENT, feeToken, _protocolFee, clientFee); // Post-act balances uint256 postContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); @@ -143,7 +144,8 @@ contract FeeCollectorTest is Test, TokenUtils { uint256 preTotalClientBalances = feeCollector.totalClientBalances(feeToken); // Act: collect fees - feeCollector.collectFees(address(0), feeToken, _protocolFee); + uint256 clientFee = (_protocolFee * CLIENT_RATE) / 100; + feeCollector.collectFees(address(0), feeToken, _protocolFee, clientFee); // Post-act balances uint256 postContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); @@ -175,7 +177,8 @@ contract FeeCollectorTest is Test, TokenUtils { IERC20(feeToken).approve(feeCollectorAddr, _amount); // Collect fees - feeCollector.collectFees(TEST_CLIENT, feeToken, _amount); + uint256 clientFee = (_amount * CLIENT_RATE) / 100; + feeCollector.collectFees(TEST_CLIENT, feeToken, _amount, clientFee); // Pre-act balances uint256 preContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); @@ -249,6 +252,64 @@ contract FeeCollectorTest is Test, TokenUtils { feeCollector.setClientRate(_clientRate); } + /// @dev + // - The current client rate should be updated to new client rate + function testFuzz_SetClientTakeRate(uint256 _clientTakeRate) public { + // Assumptions + _clientTakeRate = bound(_clientTakeRate, 0, 100); + + // Pre-act data + uint256 preClientTakeRate = feeCollector.clientTakeRates(TEST_CLIENT); + + // Assertions + assertEq(preClientTakeRate, 0); + + // Act + vm.prank(TEST_CLIENT); + feeCollector.setClientTakeRate(_clientTakeRate); + + // Post-act data + uint256 postClientTakeRate = feeCollector.clientTakeRates(TEST_CLIENT); + + // Assertions + assertEq(postClientTakeRate, _clientTakeRate); + } + + /// @dev + // - The user savings should be correct according to what's calculated in expectations + // - The user savings should be <= maxClientFee + // - The above should be true for all fee tokens + // - The above should be true for fuzzed _maxFee and _clientTakeRate + function testFuzz_GetClientAllocations(uint256 _maxFee, uint256 _clientTakeRate) public { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address feeToken = positions[i].cToken; + + // Bound fuzzed variables + _maxFee = bound(_maxFee, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + _clientTakeRate = bound(_clientTakeRate, 0, 100); + + // Setup + vm.prank(TEST_CLIENT); + feeCollector.setClientTakeRate(_clientTakeRate); + + // Expectations + uint256 maxClientFee = (CLIENT_RATE * _maxFee) / 100; + (uint256 expectedUserSavings, uint256 expectedClientFee) = + _getExpectedClientAllocations(_maxFee, _clientTakeRate); + + // Act + (uint256 userSavings, uint256 clientFee) = feeCollector.getClientAllocations(TEST_CLIENT, _maxFee); + + // Assertions + assertEq(userSavings, expectedUserSavings); + assertEq(clientFee, expectedClientFee); + assertEq(userSavings + clientFee, maxClientFee); + assertLe(userSavings, maxClientFee); + assertLe(clientFee, maxClientFee); + } + } + /// @dev // - The FeeCollector's native balance should decrease by the amount transferred. // - The owner's native balance should increase by the amount transferred. @@ -310,7 +371,8 @@ contract FeeCollectorTest is Test, TokenUtils { IERC20(token).approve(feeCollectorAddr, _amount); // Collect fees - feeCollector.collectFees(TEST_CLIENT, token, _amount); + uint256 clientFee = (_amount * CLIENT_RATE) / 100; + feeCollector.collectFees(TEST_CLIENT, token, _amount, clientFee); // Pre-act balances uint256 preContractTokenBalance = IERC20(token).balanceOf(feeCollectorAddr); @@ -353,7 +415,8 @@ contract FeeCollectorTest is Test, TokenUtils { IERC20(token).approve(feeCollectorAddr, _amount); // Collect fees - feeCollector.collectFees(TEST_CLIENT, token, _amount); + uint256 clientFee = (_amount * CLIENT_RATE) / 100; + feeCollector.collectFees(TEST_CLIENT, token, _amount, clientFee); // Act: attempt to extract ERC20 token vm.prank(_sender); diff --git a/test/common/utils/FeeUtils.t.sol b/test/common/utils/FeeUtils.t.sol new file mode 100644 index 0000000..53345ee --- /dev/null +++ b/test/common/utils/FeeUtils.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; + +// Local Imports +import { CLIENT_RATE } from "test/common/Constants.t.sol"; + +contract FeeUtils is Test { + function _getExpectedClientAllocations(uint256 _maxFee, uint256 _clientTakeRate) + internal + pure + returns (uint256 userSavings, uint256 clientFee) + { + uint256 userTakeRate = 100 - _clientTakeRate; + uint256 userPercentOfProtocolFee = (userTakeRate * CLIENT_RATE) / 100; + userSavings = (userPercentOfProtocolFee * _maxFee) / 100; + + // 2. Calculate client fee + uint256 maxClientFee = (_maxFee * CLIENT_RATE) / 100; + clientFee = maxClientFee - userSavings; + } +} diff --git a/test/integration/FeeCollector.short.t.sol b/test/integration/FeeCollector.short.t.sol index d87651e..68da743 100644 --- a/test/integration/FeeCollector.short.t.sol +++ b/test/integration/FeeCollector.short.t.sol @@ -8,24 +8,26 @@ import { Test } from "forge-std/Test.sol"; import { PositionFactory } from "src/PositionFactory.sol"; import { FeeCollector } from "src/FeeCollector.sol"; import { - Assets, AAVE_ORACLE, + Assets, CONTRACT_DEPLOYER, + CLIENT_RATE, FEE_COLLECTOR, - TEST_CLIENT, PROTOCOL_FEE_RATE, - CLIENT_RATE, + TEST_CLIENT, USDC, - WETH, - WBTC + WBTC, + WETH } from "test/common/Constants.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { FeeUtils } from "test/common/utils/FeeUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IPosition } from "src/interfaces/IPosition.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; -contract FeeCollectorShortTest is Test, TokenUtils { +contract FeeCollectorShortTest is Test, TokenUtils, FeeUtils, DebtUtils { /* solhint-disable func-name-mixedcase */ struct TestPosition { @@ -35,6 +37,15 @@ contract FeeCollectorShortTest is Test, TokenUtils { address bToken; } + struct FeeCollectorBalances { + uint256 preFeeTokenBal; + uint256 postFeeTokenBal; + uint256 preClientFeeTokenBal; + uint256 postClientFeeTokenBal; + uint256 preTotalClientsFeeTokenBal; + uint256 postTotalClientsFeeTokenBal; + } + // Test contracts PositionFactory public positionFactory; Assets public assets; @@ -97,10 +108,21 @@ contract FeeCollectorShortTest is Test, TokenUtils { } /// @dev - // - The FeeCollector's cToken balance should increase by protocolFee - // - The cToken totalClientBalances should increase by clientFee - // - The client's cToken balance on the FeeCollector contract should increase by clientFee - function testFuzz_ShortCollectFeesWithClient(uint256 _cAmt) external payable { + // - The FeeCollector's cToken balance should increase by (maxFee - userSavings). + // - The cToken amount supplied as collateral should be cAmt - (maxFee - userSavings). + // - The cToken totalClientBalances should increase by clientFee. + // - The client's cToken balance on the FeeCollector contract should increase by clientFee. + function testFuzz_ShortCollectFeesWithClient(uint256 _cAmt, uint256 _clientTakeRate) external payable { + // Setup + FeeCollectorBalances memory feeCollectorBalances; + + // Bound fuzzed inputs + _clientTakeRate = bound(_clientTakeRate, 0, 100); + + // Setup + vm.prank(TEST_CLIENT); + IFeeCollector(FEE_COLLECTOR).setClientTakeRate(_clientTakeRate); + for (uint256 i; i < positions.length; i++) { // Test Variables address positionAddr = positions[i].addr; @@ -110,8 +132,9 @@ contract FeeCollectorShortTest is Test, TokenUtils { _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); // Expectations - uint256 protocolFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; - uint256 clientFee = (protocolFee * CLIENT_RATE) / 100; + uint256 maxFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; + (uint256 userSavings, uint256 clientFee) = _getExpectedClientAllocations(maxFee, _clientTakeRate); + uint256 protocolFee = maxFee - userSavings; // Fund positionOwner with _cAmt of cToken _fund(positionOwner, cToken, _cAmt); @@ -120,30 +143,44 @@ contract FeeCollectorShortTest is Test, TokenUtils { IERC20(cToken).approve(positionAddr, _cAmt); // Pre-act balances - uint256 preContractBalance = IERC20(cToken).balanceOf(FEE_COLLECTOR); - uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); - uint256 preClientFeeBalance = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, cToken); + feeCollectorBalances.preFeeTokenBal = IERC20(cToken).balanceOf(FEE_COLLECTOR); + feeCollectorBalances.preClientFeeTokenBal = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, cToken); + feeCollectorBalances.preTotalClientsFeeTokenBal = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 prePositionATokenBal = _getATokenBalance(positionAddr, cToken); + assertEq(prePositionATokenBal, 0); // Act: increase short position IPosition(positionAddr).short(_cAmt, 50, 0, 3000, TEST_CLIENT); // Post-act balances - uint256 postContractBalance = IERC20(cToken).balanceOf(FEE_COLLECTOR); - uint256 postTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); - uint256 postClientFeeBalance = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, cToken); + feeCollectorBalances.postFeeTokenBal = IERC20(cToken).balanceOf(FEE_COLLECTOR); + feeCollectorBalances.postClientFeeTokenBal = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, cToken); + feeCollectorBalances.postTotalClientsFeeTokenBal = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 postPositionATokenBal = _getATokenBalance(positionAddr, cToken); // Assertions - assertEq(postContractBalance, preContractBalance + protocolFee); - assertEq(postTotalClientBalances, preTotalClientBalances + clientFee); - assertEq(postClientFeeBalance, preClientFeeBalance + clientFee); + assertEq(feeCollectorBalances.postFeeTokenBal, feeCollectorBalances.preFeeTokenBal + protocolFee); + assertEq(feeCollectorBalances.postClientFeeTokenBal, feeCollectorBalances.preClientFeeTokenBal + clientFee); + assertEq( + feeCollectorBalances.postTotalClientsFeeTokenBal, + feeCollectorBalances.preTotalClientsFeeTokenBal + clientFee + ); + assertApproxEqAbs(postPositionATokenBal, _cAmt - protocolFee, 1); } } /// @dev - // - The FeeCollector's cToken balance should increase by protocolFee + // - The FeeCollector's cToken balance should increase by (maxFee - userSavings). + // - The cToken amount supplied as collateral should be cAmt - (maxFee - userSavings). // - The cToken totalClientBalances should not change // - The above should be true when _client is sent as address(0) - function testFuzz_ShortCollectFeesNoClient(uint256 _cAmt) external payable { + function testFuzz_ShortCollectFeesNoClient(uint256 _cAmt, uint256 _clientTakeRate) external payable { + // Setup + FeeCollectorBalances memory feeCollectorBalances; + + // Bound fuzzed inputs + _clientTakeRate = bound(_clientTakeRate, 0, 100); + for (uint256 i; i < positions.length; i++) { // Test Variables address positionAddr = positions[i].addr; @@ -153,7 +190,9 @@ contract FeeCollectorShortTest is Test, TokenUtils { _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); // Expectations - uint256 protocolFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; + uint256 maxFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; + (uint256 userSavings,) = _getExpectedClientAllocations(maxFee, 0); + uint256 protocolFee = maxFee - userSavings; // Fund positionOwner with _cAmt of cToken _fund(positionOwner, cToken, _cAmt); @@ -162,19 +201,23 @@ contract FeeCollectorShortTest is Test, TokenUtils { IERC20(cToken).approve(positionAddr, _cAmt); // Pre-act balances - uint256 preContractBalance = IERC20(cToken).balanceOf(FEE_COLLECTOR); - uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + feeCollectorBalances.preFeeTokenBal = IERC20(cToken).balanceOf(FEE_COLLECTOR); + feeCollectorBalances.preTotalClientsFeeTokenBal = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 prePositionATokenBal = _getATokenBalance(positionAddr, cToken); + assertEq(prePositionATokenBal, 0); // Act: increase short position IPosition(positionAddr).short(_cAmt, 50, 0, 3000, address(0)); // Post-act balances - uint256 postContractBalance = IERC20(cToken).balanceOf(FEE_COLLECTOR); - uint256 postTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + feeCollectorBalances.postFeeTokenBal = IERC20(cToken).balanceOf(FEE_COLLECTOR); + feeCollectorBalances.postTotalClientsFeeTokenBal = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 postPositionATokenBal = _getATokenBalance(positionAddr, cToken); // Assertions - assertEq(postContractBalance, preContractBalance + protocolFee); - assertEq(postTotalClientBalances, preTotalClientBalances); + assertEq(feeCollectorBalances.postFeeTokenBal, feeCollectorBalances.preFeeTokenBal + protocolFee); + assertEq(feeCollectorBalances.postTotalClientsFeeTokenBal, feeCollectorBalances.preTotalClientsFeeTokenBal); + assertApproxEqAbs(postPositionATokenBal, _cAmt - protocolFee, 1); } } } diff --git a/test/libraries/FeeLib.t.sol b/test/libraries/FeeLib.t.sol new file mode 100644 index 0000000..9e5d3bc --- /dev/null +++ b/test/libraries/FeeLib.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; + +// Local Imports +import { + Assets, + CLIENT_RATE, + CONTRACT_DEPLOYER, + FEE_COLLECTOR, + PROTOCOL_FEE_RATE, + TEST_CLIENT +} from "test/common/Constants.t.sol"; +import { FeeLib } from "src/libraries/FeeLib.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { FeeUtils } from "test/common/utils/FeeUtils.t.sol"; +import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract FeeLibTest is Test, TokenUtils, FeeUtils { + /* solhint-disable func-name-mixedcase */ + + // Test Contracts + Assets public assets; + address[] public supportedAssets; + + // Test Storage + uint256 public mainnetFork; + + 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); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); + + // Set client rate + vm.prank(CONTRACT_DEPLOYER); + IFeeCollector(FEE_COLLECTOR).setClientRate(CLIENT_RATE); + } + + /// @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 + // - This contract's token balance should decrease by: (max fee - user savings). + // - The balance of the FeeCollector should increase by: (max fee - user savings). + // - The client's balance of the FeeCollector should increase by the correct amount. + // - The above should be true for all supported tokens. + // - The above should be true for fuzzed cAmts and clientTakeRates. + function testFuzz_TakeProtocolFee(uint256 _cAmt, uint256 _clientTakeRate) public { + // Bound fuzzed inputs + _clientTakeRate = bound(_clientTakeRate, 0, 100); + + // Setup + vm.prank(TEST_CLIENT); + IFeeCollector(FEE_COLLECTOR).setClientTakeRate(_clientTakeRate); + + for (uint256 i; i < supportedAssets.length; i++) { + // Test Variables + address feeToken = supportedAssets[i]; + + // Bound fuzzed inputs + _cAmt = bound(_cAmt, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + + // Fund this contract with collateral + _fund(address(this), supportedAssets[i], _cAmt); + + // Expectations + uint256 _maxFee = (_cAmt * PROTOCOL_FEE_RATE) / 1000; + (uint256 expectedUserSavings, uint256 expectedClientFee) = + _getExpectedClientAllocations(_maxFee, _clientTakeRate); + uint256 expectedFee = _maxFee - expectedUserSavings; + + // Pre-act Data + uint256 preFeeTokenBal = IERC20(feeToken).balanceOf(address(this)); + uint256 preFeeCollectoreFeeTokenBal = IERC20(feeToken).balanceOf(FEE_COLLECTOR); + uint256 preClientFeeTokenBal = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, feeToken); + + // Act + FeeLib.takeProtocolFee(feeToken, _cAmt, TEST_CLIENT); + + // Post-act Data + uint256 postFeeTokenBal = IERC20(feeToken).balanceOf(address(this)); + uint256 postFeeCollectoreFeeTokenBal = IERC20(feeToken).balanceOf(FEE_COLLECTOR); + uint256 postClientFeeTokenBal = IFeeCollector(FEE_COLLECTOR).balances(TEST_CLIENT, feeToken); + + // Assertions + assertEq(postFeeTokenBal, preFeeTokenBal - expectedFee); + assertEq(postFeeCollectoreFeeTokenBal, preFeeCollectoreFeeTokenBal + expectedFee); + assertEq(postClientFeeTokenBal, preClientFeeTokenBal + expectedClientFee); + } + } +}