From e4458dec55322b84f637103656caf0ecb8ba7aa9 Mon Sep 17 00:00:00 2001 From: "ernestor.eth" <78889960+0xernesto@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:11:47 -0600 Subject: [PATCH] test: add FeeCollector unit and integration tests (#8) * feat: make FEE_COLLECTOR an immutable variable * test: add FeeCollector unit tests * test: add collectFees unit tests * test: address requested changes --- README.md | 46 +-- src/Position.sol | 5 +- src/PositionFactory.sol | 8 +- src/interfaces/IFeeCollector.sol | 10 + src/interfaces/IPositionFactory.sol | 2 +- test/FeeCollector.t.sol | 519 ++++++++++++++++++++++++++++ test/Position.t.sol | 18 +- test/PositionAdmin.t.sol | 10 +- test/PositionFactory.t.sol | 8 +- test/common/Constants.t.sol | 4 +- test/common/utils/TokenUtils.t.sol | 4 +- test/services/DebtService.t.sol | 11 +- 12 files changed, 590 insertions(+), 55 deletions(-) create mode 100644 test/FeeCollector.t.sol diff --git a/README.md b/README.md index 4acb20af..ba4b49f8 100644 --- a/README.md +++ b/README.md @@ -14,28 +14,30 @@ On-chain shorting via Aave and Uniswap. ## To-Do -- [x] Impelemnt protocol fee -- [x] Implement frontend incentive -- [x] Account for case where there is no client (if no frontend, they would be able to pass their own address...) -- [x] Should not repay more to Aave than what is owed when closing a position - -Unit Tests: - -- [x] Mock feel collector in tests -- [x] Should not repay more to Aave than what is owed when closing a position -- [ ] Test that the client's collected fee balance increased by the correct amount -- [ ] Test that the totalClientBalance increased by the correct amount -- [ ] Test that an admin you can set a clientRate -- [ ] Test that a non-admin cannot set a clientRate -- [ ] Test that a client can withdraw their collected fees -- [ ] Test that extractERC20 works correctly on FeeCollector (it has different logic than the other contracts) -- [ ] Test that a non-admin cannot extractERC20 -- [ ] Test that an admin can withdraw native -- [ ] Test that a non-admin cannot withdraw native -- [ ] Test that FeeCollector can recieve native -- [ ] Test Fallback on FeeCollector -- [ ] Test that the correct protocol fee is collected during a short (proabably separate test from Position.t.sol) +Logic: + +- [x] Make `FEE_COLLECTOR` an immutable variable in Position.sol and PositionFactory.sol +- [x] Add `owner()` and `clientRate()` to IFeeCollector + +Tests: + +- [ ] Separate integration tests from unit tests (separate PR) +- [x] testFuzz_CollectFeesWithClient +- [x] testFuzz_CollectFeesNoClient +- [x] testFuzz_CollectFeesWithClientIntegrated +- [x] testFuzz_CollectFeesNoClientIntegrated +- [x] testFuzz_ClientWithdraw +- [x] testFuzz_SetClientRate +- [x] testFuzz_CannotSetClientRateOutOfRange +- [x] testFuzz_CannotSetClientRateUnauthorized +- [x] testFuzz_ExtractNative +- [x] testFuzz_CannotExtractNative +- [x] testFuzz_ExtractERC20 +- [x] testFuzz_CannotExtractERC20 +- [x] testFuzz_Receive +- [x] testFuzz_Fallback Considerations: -- [ ] Consider emitting events through position factory +- [ ] Consider emitting Position events through another contract +- [ ] Consider adding a function to short with signatures, via `ERCRecover`. diff --git a/src/Position.sol b/src/Position.sol index f29fb2fa..44010af2 100644 --- a/src/Position.sol +++ b/src/Position.sol @@ -14,18 +14,19 @@ import { IFeeCollector } from "src/interfaces/IFeeCollector.sol"; contract Position is DebtService, SwapService { // Constants: no SLOAD to save gas uint256 public constant PROTOCOL_FEE = 3; - address private constant FEE_COLLECTOR = 0x2cD6D948263F20C3c27f181f14647840fC64b488; // Immutables: no SLOAD to save gas + address public immutable FEE_COLLECTOR; address public immutable B_TOKEN; // Events event Short(uint256 cAmt, uint256 dAmt, uint256 bAmt); event Close(uint256 gains); - constructor(address _owner, address _cToken, address _dToken, address _bToken) + constructor(address _owner, address _feeCollector, address _cToken, address _dToken, address _bToken) DebtService(_owner, _cToken, _dToken) { + FEE_COLLECTOR = _feeCollector; B_TOKEN = _bToken; } diff --git a/src/PositionFactory.sol b/src/PositionFactory.sol index b02e0bf3..652f0ed8 100644 --- a/src/PositionFactory.sol +++ b/src/PositionFactory.sol @@ -14,6 +14,9 @@ contract PositionFactory is Ownable { // Constants: no SLOAD to save gas address private constant CONTRACT_DEPLOYER = 0x0a5B347509621337cDDf44CBCf6B6E7C9C908CD2; + // Immutables: no SLOAD to save gas + address public immutable FEE_COLLECTOR; + // Factory Storage /// @dev Mapping from owner to cToken to dToken to bToken to position mapping(address => mapping(address => mapping(address => mapping(address => address)))) public positions; @@ -23,8 +26,9 @@ contract PositionFactory is Ownable { error Unauthorized(); error PositionExists(); - constructor(address _owner) Ownable(_owner) { + constructor(address _owner, address _feeCollector) Ownable(_owner) { if (msg.sender != CONTRACT_DEPLOYER) revert Unauthorized(); + FEE_COLLECTOR = _feeCollector; } /** @@ -40,7 +44,7 @@ contract PositionFactory is Ownable { { if (positions[msg.sender][_cToken][_dToken][_bToken] != address(0)) revert PositionExists(); - position = address(new Position(msg.sender, _cToken, _dToken, _bToken)); + position = address(new Position(msg.sender, FEE_COLLECTOR, _cToken, _dToken, _bToken)); positionsLookup[msg.sender].push(position); positions[msg.sender][_cToken][_dToken][_bToken] = position; diff --git a/src/interfaces/IFeeCollector.sol b/src/interfaces/IFeeCollector.sol index a46bd82c..913bcfc7 100644 --- a/src/interfaces/IFeeCollector.sol +++ b/src/interfaces/IFeeCollector.sol @@ -8,6 +8,16 @@ interface IFeeCollector { ** CORE FUNCTIONS ** ******************************************************************************/ + /** + * @notice Returns the owner of this contract. + */ + function owner() external returns (address); + + /** + * @notice Returns the current client rate. + */ + function clientRate() external returns (uint256); + /** * @notice Returns the total balance for the specified token across all client operators. * @param _token The token address to check. diff --git a/src/interfaces/IPositionFactory.sol b/src/interfaces/IPositionFactory.sol index 8a974701..67a15846 100644 --- a/src/interfaces/IPositionFactory.sol +++ b/src/interfaces/IPositionFactory.sol @@ -12,7 +12,7 @@ interface IPositionFactory { /** * @notice Returns the owner of this contract. */ - function OWNER() external returns (address); + function owner() external returns (address); /** * @notice Returns the address of an owner's specified Position contract. diff --git a/test/FeeCollector.t.sol b/test/FeeCollector.t.sol new file mode 100644 index 00000000..e0980b4c --- /dev/null +++ b/test/FeeCollector.t.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { FeeCollector } from "src/FeeCollector.sol"; +import { + Assets, + AAVE_ORACLE, + CONTRACT_DEPLOYER, + TEST_CLIENT, + PROTOCOL_FEE, + CLIENT_RATE, + USDC, + WETH, + WBTC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.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 FeeCollectorTest is Test, TokenUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + // Errors + error OwnableUnauthorizedAccount(address account); + + // Test contracts + PositionFactory public positionFactory; + FeeCollector public feeCollector; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + uint256 public mainnetFork; + address public feeCollectorAddr; + address public positionOwner = address(this); + + // Events + event Short(uint256 cAmt, uint256 dAmt, uint256 bAmt); + + function setUp() public { + // Setup: use mainnet fork + mainnetFork = vm.createFork(vm.envString("RPC_URL")); + vm.selectFork(mainnetFork); + + // Deploy assets + assets = new Assets(); + address[4] memory supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + feeCollectorAddr = address(feeCollector); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER, feeCollectorAddr); + + // Set client rate + vm.prank(CONTRACT_DEPLOYER); + feeCollector.setClientRate(CLIENT_RATE); + + // Deploy and store four position contracts - one for each supported asset as collateral + address positionAddr; + TestPosition memory newPosition; + for (uint256 i; i < supportedAssets.length; i++) { + if (supportedAssets[i] != WETH) { + positionAddr = positionFactory.createPosition(supportedAssets[i], WETH, WBTC); + newPosition = + TestPosition({ addr: positionAddr, cToken: supportedAssets[i], dToken: WETH, bToken: WBTC }); + positions.push(newPosition); + } + } + positionAddr = positionFactory.createPosition(WETH, USDC, WETH); + newPosition = TestPosition({ addr: positionAddr, cToken: WETH, dToken: USDC, bToken: WETH }); + positions.push(newPosition); + + // Mock AaveOracle + for (uint256 i; i < supportedAssets.length; i++) { + vm.mockCall( + AAVE_ORACLE, + abi.encodeWithSelector(IAaveOracle(AAVE_ORACLE).getAssetPrice.selector, supportedAssets[i]), + abi.encode(assets.prices(supportedAssets[i])) + ); + } + } + + /// @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 FeeCollector's feeToken balance should increase by _protocolFee + // - The feeToken totalClientBalances should increase by clientFee + // - The client's feeToken balance on the FeeCollector contract should increase by clientFee + function testFuzz_CollectFeesWithClient(uint256 _protocolFee) external payable { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address feeToken = positions[i].cToken; + + // Bound fuzzed variables + _protocolFee = bound(_protocolFee, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + + // Expectations + uint256 clientFee = (_protocolFee * CLIENT_RATE) / 100; + + // Fund positionOwner with _protocolFee amount of feeToken + _fund(positionOwner, feeToken, _protocolFee); + + // Approve FeeCollector contract to spend feeToken + IERC20(feeToken).approve(feeCollectorAddr, _protocolFee); + + // Pre-act balances + uint256 preContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(feeToken); + uint256 preClientFeeBalance = feeCollector.balances(TEST_CLIENT, feeToken); + + // Act: collect fees + feeCollector.collectFees(TEST_CLIENT, feeToken, _protocolFee); + + // Post-act balances + uint256 postContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(feeToken); + uint256 postClientFeeBalance = feeCollector.balances(TEST_CLIENT, feeToken); + + // Assertions + assertEq(postContractBalance, preContractBalance + _protocolFee); + assertEq(postTotalClientBalances, preTotalClientBalances + clientFee); + assertEq(postClientFeeBalance, preClientFeeBalance + clientFee); + } + } + + /// @dev + // - The FeeCollector's feeToken balance should increase by _protocolFee + // - The feeToken totalClientBalances should not change + // - The above should be true when _client is sent as address(0) + function testFuzz_CollectFeesNoClient(uint256 _protocolFee) external payable { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address feeToken = positions[i].cToken; + + // Bound fuzzed variables + _protocolFee = bound(_protocolFee, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + + // Fund positionOwner with _protocolFee amount of feeToken + _fund(positionOwner, feeToken, _protocolFee); + + // Approve FeeCollector contract to spend feeToken + IERC20(feeToken).approve(feeCollectorAddr, _protocolFee); + + // Pre-act balances + uint256 preContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(feeToken); + + // Act: collect fees + feeCollector.collectFees(address(0), feeToken, _protocolFee); + + // Post-act balances + uint256 postContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(feeToken); + + // Assertions + assertEq(postContractBalance, preContractBalance + _protocolFee); + assertEq(postTotalClientBalances, preTotalClientBalances); + } + } + + /// @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_CollectFeesWithClientIntegrated(uint256 _cAmt) external payable { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address positionAddr = positions[i].addr; + address cToken = positions[i].cToken; + + // Bound fuzzed variables + _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); + + // Expectations + uint256 protocolFee = (_cAmt * PROTOCOL_FEE) / 1000; + uint256 clientFee = (protocolFee * CLIENT_RATE) / 100; + + // Fund positionOwner with _cAmt of cToken + _fund(positionOwner, cToken, _cAmt); + + // Approve Position contract to spend collateral + IERC20(cToken).approve(positionAddr, _cAmt); + + // Pre-act balances + uint256 preContractBalance = IERC20(cToken).balanceOf(feeCollectorAddr); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(cToken); + uint256 preClientFeeBalance = feeCollector.balances(TEST_CLIENT, cToken); + + // Act: increase short position + IPosition(positionAddr).short(_cAmt, 50, 0, 3000, TEST_CLIENT); + + // Post-act balances + uint256 postContractBalance = IERC20(cToken).balanceOf(feeCollectorAddr); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(cToken); + uint256 postClientFeeBalance = feeCollector.balances(TEST_CLIENT, cToken); + + // Assertions + assertEq(postContractBalance, preContractBalance + protocolFee); + assertEq(postTotalClientBalances, preTotalClientBalances + clientFee); + assertEq(postClientFeeBalance, preClientFeeBalance + clientFee); + } + } + + /// @dev + // - The FeeCollector's cToken balance should increase by protocolFee + // - The cToken totalClientBalances should not change + // - The above should be true when _client is sent as address(0) + function testFuzz_CollectFeesNoClientIntegrated(uint256 _cAmt) external payable { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address positionAddr = positions[i].addr; + address cToken = positions[i].cToken; + + // Bound fuzzed variables + _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); + + // Expectations + uint256 protocolFee = (_cAmt * PROTOCOL_FEE) / 1000; + + // Fund positionOwner with _cAmt of cToken + _fund(positionOwner, cToken, _cAmt); + + // Approve Position contract to spend collateral + IERC20(cToken).approve(positionAddr, _cAmt); + + // Pre-act balances + uint256 preContractBalance = IERC20(cToken).balanceOf(feeCollectorAddr); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(cToken); + + // Act: increase short position + IPosition(positionAddr).short(_cAmt, 50, 0, 3000, address(0)); + + // Post-act balances + uint256 postContractBalance = IERC20(cToken).balanceOf(feeCollectorAddr); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(cToken); + + // Assertions + assertEq(postContractBalance, preContractBalance + protocolFee); + assertEq(postTotalClientBalances, preTotalClientBalances); + } + } + + /// @dev + // - The FeeCollector's feeToken balance should decrease by amount withdrawn + // - The feeToken totalClientBalances should decrease by amount withdrawn + // - The client's feeToken balance on the FeeCollector contract should decrease by amount withdrawn + // - The feeToken balance of client's account should increase by amount withdrawn + function testFuzz_ClientWithdraw(uint256 _amount) external payable { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address feeToken = positions[i].cToken; + + // Bound fuzzed variables + _amount = bound(_amount, assets.minCAmts(feeToken), assets.maxCAmts(feeToken)); + + // Fund positionOwner with _amount of feeToken + _fund(positionOwner, feeToken, _amount); + + // Approve FeeCollector contract to spend feeToken + IERC20(feeToken).approve(feeCollectorAddr, _amount); + + // Collect fees + feeCollector.collectFees(TEST_CLIENT, feeToken, _amount); + + // Pre-act balances + uint256 preContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(feeToken); + uint256 preClientContractBalance = feeCollector.balances(TEST_CLIENT, feeToken); + uint256 preClientAccountBalance = IERC20(feeToken).balanceOf(TEST_CLIENT); + + // Act: client withdraws fees + vm.prank(TEST_CLIENT); + feeCollector.clientWithdraw(feeToken); + + // Post-act balances + uint256 postContractBalance = IERC20(feeToken).balanceOf(feeCollectorAddr); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(feeToken); + uint256 postClientContractBalance = feeCollector.balances(TEST_CLIENT, feeToken); + uint256 postClientAccountBalance = IERC20(feeToken).balanceOf(TEST_CLIENT); + + // Assertions + assertEq(postContractBalance, preContractBalance - preClientContractBalance); + assertEq(postTotalClientBalances, preTotalClientBalances - preClientContractBalance); + assertEq(postClientContractBalance, 0); + assertEq(postClientAccountBalance, preClientAccountBalance + preClientContractBalance); + } + } + + /// @dev + // - The current client rate should be updated to new client rate + function testFuzz_SetClientRate(uint256 _clientRate) external payable { + // Bound fuzzed variables + _clientRate = bound(_clientRate, 30, 100); + + // Pre-act data + uint256 preClientRate = feeCollector.clientRate(); + + // Assertions + assertEq(preClientRate, CLIENT_RATE); + + // Act + vm.prank(CONTRACT_DEPLOYER); + feeCollector.setClientRate(_clientRate); + + // Post-act data + uint256 postClientRate = feeCollector.clientRate(); + + // Assertions + assertEq(postClientRate, _clientRate); + } + + /// @dev + // - The clientRate in FeeCollector contract cannot be < 30 or > 100 + function testFuzz_CannotSetClientRateOutOfRange(uint256 _clientRate) external payable { + // Assumptions + vm.assume(_clientRate < 30 || _clientRate > 100); + + // Act + vm.prank(CONTRACT_DEPLOYER); + vm.expectRevert(FeeCollector.OutOfRange.selector); + feeCollector.setClientRate(_clientRate); + } + + /// @dev + // - It should revert with Unauthorized() error when called by an unauthorized sender. + function testFuzz_CannotSetClientRateUnauthorized(uint256 _clientRate, address _sender) external payable { + // Assumptions + _clientRate = bound(_clientRate, 30, 100); + vm.assume(_sender != CONTRACT_DEPLOYER); + + // Act + vm.prank(_sender); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _sender)); + feeCollector.setClientRate(_clientRate); + } + + /// @dev + // - The FeeCollector's native balance should decrease by the amount transferred. + // - The owner's native balance should increase by the amount transferred. + function testFuzz_ExtractNative(uint256 _amount) public { + // Setup: fund contract with _amount of native token + _amount = bound(_amount, 1, 1e22); + vm.deal(feeCollectorAddr, _amount); + + // Get pre-act balances + uint256 preContractBalance = feeCollectorAddr.balance; + uint256 preOwnerBalance = CONTRACT_DEPLOYER.balance; + + // Assertions + assertEq(preContractBalance, _amount); + + // Act + vm.prank(CONTRACT_DEPLOYER); + feeCollector.extractNative(); + + // Ge post-act balances + uint256 postContractBalance = feeCollectorAddr.balance; + uint256 postOwnerBalance = CONTRACT_DEPLOYER.balance; + + // Assertions + assertEq(postContractBalance, 0); + assertEq(postOwnerBalance, preOwnerBalance + _amount); + } + + /// @dev + // - It should revert with Unauthorized() error when called by an unauthorized sender. + function testFuzz_CannotExtractNative(uint256 _amount, address _sender) public { + // Setup: fund contract with _amount of native token + _amount = bound(_amount, 1, 1e22); + vm.assume(_sender != CONTRACT_DEPLOYER); + vm.deal(feeCollectorAddr, _amount); + + // Act: attempt to extract native + vm.prank(_sender); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _sender)); + feeCollector.extractNative(); + } + + /// @dev + // - For the specified feeToken, the owner's balance should increase by (total balance - totalClientBalances). + // - For the specified feeToken, the FeeCollector's balance should decrease by (total balance - totalClientBalances). + // - The token totalClientBalances should not change + function testFuzz_ExtractERC20(uint256 _amount) public { + for (uint256 i; i < positions.length; i++) { + // Test Variables + address token = positions[i].cToken; + + // Bound fuzzed variables + _amount = bound(_amount, assets.minCAmts(token), assets.maxCAmts(token)); + + // Fund positionOwner with _amount of token + _fund(positionOwner, token, _amount); + + // Approve FeeCollector contract to spend token + IERC20(token).approve(feeCollectorAddr, _amount); + + // Collect fees + feeCollector.collectFees(TEST_CLIENT, token, _amount); + + // Pre-act balances + uint256 preContractTokenBalance = IERC20(token).balanceOf(feeCollectorAddr); + uint256 preOwnerTokenBalance = IERC20(token).balanceOf(CONTRACT_DEPLOYER); + uint256 preTotalClientBalances = feeCollector.totalClientBalances(token); + + // Act: owner withraws fees + vm.prank(CONTRACT_DEPLOYER); + feeCollector.extractERC20(token); + + // Post-act balances + uint256 postContractTokenBalance = IERC20(token).balanceOf(feeCollectorAddr); + uint256 postOwnerTokenBalance = IERC20(token).balanceOf(CONTRACT_DEPLOYER); + uint256 postTotalClientBalances = feeCollector.totalClientBalances(token); + + // Assertions + assertEq(postContractTokenBalance, preTotalClientBalances); + assertEq(postOwnerTokenBalance, preOwnerTokenBalance + (preContractTokenBalance - preTotalClientBalances)); + assertEq(postTotalClientBalances, preTotalClientBalances); + } + } + + /// @dev + // - It should revert with Unauthorized() error when called by an unauthorized sender. + function testFuzz_CannotExtractERC20(uint256 _amount, address _sender) public { + // Assumptions + vm.assume(_sender != CONTRACT_DEPLOYER); + + for (uint256 i; i < positions.length; i++) { + // Test Variables + address token = positions[i].cToken; + + // Bound fuzzed variables + _amount = bound(_amount, assets.minCAmts(token), assets.maxCAmts(token)); + + // Fund positionOwner with _amount of token + _fund(positionOwner, token, _amount); + + // Approve FeeCollector contract to spend token + IERC20(token).approve(feeCollectorAddr, _amount); + + // Collect fees + feeCollector.collectFees(TEST_CLIENT, token, _amount); + + // Act: attempt to extract ERC20 token + vm.prank(_sender); + vm.expectRevert(abi.encodeWithSelector(OwnableUnauthorizedAccount.selector, _sender)); + feeCollector.extractERC20(token); + } + } + + /// @dev + // - The FeeCollector's native balance should increase by the amount transferred. + function testFuzz_Receive(uint256 _amount, address _sender) public { + // Assumptions + _amount = bound(_amount, 1, 1_000 ether); + uint256 gasMoney = 1 ether; + vm.deal(_sender, _amount + gasMoney); + + // Pre-Act Data + uint256 preContractBalance = feeCollectorAddr.balance; + + // Act + vm.prank(_sender); + (bool success,) = payable(feeCollectorAddr).call{ value: _amount }(""); + + // Post-Act Data + uint256 postContractBalance = feeCollectorAddr.balance; + + // Assertions + assertTrue(success); + assertEq(postContractBalance, preContractBalance + _amount); + } + + /// @dev + // - The FeeCollector's native balance should increase by the amount transferred. + function testFuzz_Fallback(uint256 _amount, address _sender) public { + // Assumptions + vm.assume(_amount != 0 && _amount <= 1000 ether); + uint256 gasMoney = 1 ether; + vm.deal(_sender, _amount + gasMoney); + + // Pre-Act Data + uint256 preSenderBalance = _sender.balance; + uint256 preContractBalance = feeCollectorAddr.balance; + + // Act + vm.prank(_sender); + (bool success,) = feeCollectorAddr.call{ value: _amount }(abi.encodeWithSignature("nonExistentFn()")); + + // Post-Act Data + uint256 postSenderBalance = _sender.balance; + uint256 postContractBalance = feeCollectorAddr.balance; + + // Assertions + assertTrue(success); + assertEq(postSenderBalance, preSenderBalance - _amount); + assertEq(postContractBalance, preContractBalance + _amount); + } +} diff --git a/test/Position.t.sol b/test/Position.t.sol index 7748a511..f855ad19 100644 --- a/test/Position.t.sol +++ b/test/Position.t.sol @@ -6,23 +6,19 @@ import { Test } from "forge-std/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; // Local Imports +import { FeeCollector } from "src/FeeCollector.sol"; import { PositionFactory } from "src/PositionFactory.sol"; import { PositionAdmin } from "src/PositionAdmin.sol"; -import { FeeCollector } from "src/FeeCollector.sol"; import { Assets, AAVE_ORACLE, CONTRACT_DEPLOYER, DAI, - FEE_COLLECTOR, PROFIT_PERCENT, - REPAY_BUFFER, REPAY_PERCENT, SWAP_ROUTER, TEST_CLIENT, USDC, - WBTC, - WETH, WITHDRAW_BUFFER } from "test/common/Constants.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; @@ -83,14 +79,14 @@ contract PositionTest is Test, TokenUtils, DebtUtils { assets = new Assets(); address[4] memory supportedAssets = assets.getSupported(); - // Deploy PositionFactory - vm.prank(CONTRACT_DEPLOYER); - positionFactory = new PositionFactory(CONTRACT_DEPLOYER); - // Deploy FeeCollector vm.prank(CONTRACT_DEPLOYER); feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER, address(feeCollector)); + // Deploy and store all possible positions for (uint256 i; i < supportedAssets.length; i++) { address cToken = supportedAssets[i]; @@ -120,10 +116,6 @@ contract PositionTest is Test, TokenUtils, DebtUtils { abi.encode(assets.prices(supportedAssets[i])) ); } - - // Mock FeeCollector - bytes memory code = address(feeCollector).code; - vm.etch(FEE_COLLECTOR, code); } /// @dev diff --git a/test/PositionAdmin.t.sol b/test/PositionAdmin.t.sol index 465e07c8..35f6c817 100644 --- a/test/PositionAdmin.t.sol +++ b/test/PositionAdmin.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.21; import { Test } from "forge-std/Test.sol"; // Local Imports +import { FeeCollector } from "src/FeeCollector.sol"; import { PositionFactory } from "src/PositionFactory.sol"; import { PositionAdmin } from "src/PositionAdmin.sol"; import { Assets, CONTRACT_DEPLOYER } from "test/common/Constants.t.sol"; @@ -16,6 +17,7 @@ contract PositionAdminTest is Test, TokenUtils { /* solhint-disable func-name-mixedcase */ // Test contracts + FeeCollector public feeCollector; PositionFactory public positionFactory; Assets public assets; @@ -33,11 +35,15 @@ contract PositionAdminTest is Test, TokenUtils { assets = new Assets(); address[4] memory supportedAssets = assets.getSupported(); + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + // Deploy PositionFactory vm.prank(CONTRACT_DEPLOYER); - positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER, address(feeCollector)); - // Deploy Position + // Deploy a Position positionAddr = positionFactory.createPosition(supportedAssets[0], supportedAssets[3], supportedAssets[2]); } diff --git a/test/PositionFactory.t.sol b/test/PositionFactory.t.sol index 762392f3..079dd048 100644 --- a/test/PositionFactory.t.sol +++ b/test/PositionFactory.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.21; import { Test } from "forge-std/Test.sol"; // Local Imports +import { FeeCollector } from "src/FeeCollector.sol"; import { PositionFactory } from "src/PositionFactory.sol"; import { Assets, CONTRACT_DEPLOYER } from "test/common/Constants.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; @@ -15,6 +16,7 @@ contract PositionFactoryTest is Test, TokenUtils { /* solhint-disable func-name-mixedcase */ // Test Contracts + FeeCollector public feeCollector; PositionFactory public positionFactory; Assets public assets; @@ -30,8 +32,12 @@ contract PositionFactoryTest is Test, TokenUtils { mainnetFork = vm.createFork(vm.envString("RPC_URL")); vm.selectFork(mainnetFork); + // Deploy FeeCollector vm.prank(CONTRACT_DEPLOYER); - positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + feeCollector = new FeeCollector(CONTRACT_DEPLOYER); + + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER, address(feeCollector)); assets = new Assets(); } diff --git a/test/common/Constants.t.sol b/test/common/Constants.t.sol index 578c8a6e..09bea260 100644 --- a/test/common/Constants.t.sol +++ b/test/common/Constants.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.21; +// Address constants address constant CONTRACT_DEPLOYER = 0x0a5B347509621337cDDf44CBCf6B6E7C9C908CD2; address constant AAVE_ORACLE = 0xb56c2F0B653B2e0b10C9b928C8580Ac5Df02C7C7; address constant AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; address constant SWAP_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; -address constant FEE_COLLECTOR = 0x2cD6D948263F20C3c27f181f14647840fC64b488; address constant TEST_CLIENT = 0xd05E2C879821C31E5a8e3B5Da490e834211Ca443; // Supported Assets @@ -22,6 +22,8 @@ uint256 constant PROFIT_PERCENT = 25; uint256 constant REPAY_PERCENT = 75; uint256 constant WITHDRAW_BUFFER = 100_000; uint256 constant REPAY_BUFFER = 2; +uint256 constant PROTOCOL_FEE = 3; +uint256 constant CLIENT_RATE = 30; contract Assets { address[4] public supported = [USDC, DAI, WETH, WBTC]; diff --git a/test/common/utils/TokenUtils.t.sol b/test/common/utils/TokenUtils.t.sol index 2f42a122..bae23c16 100644 --- a/test/common/utils/TokenUtils.t.sol +++ b/test/common/utils/TokenUtils.t.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.21; // External Imports import { Test } from "forge-std/Test.sol"; -import { Assets, DAI, USDC, USDC_HOLDER } from "test/common/Constants.t.sol"; + +// Local Imports +import { Assets, USDC, USDC_HOLDER } from "test/common/Constants.t.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; contract TokenUtils is Test { diff --git a/test/services/DebtService.t.sol b/test/services/DebtService.t.sol index 81af5f66..9ea31fa7 100644 --- a/test/services/DebtService.t.sol +++ b/test/services/DebtService.t.sol @@ -9,16 +9,7 @@ import { PositionAdmin } from "src/PositionAdmin.sol"; import { DebtServiceHarness } from "test/harness/DebtServiceHarness.t.sol"; import { DebtUtils } from "test/services/utils/DebtUtils.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; -import { - Assets, - AAVE_ORACLE, - AAVE_POOL, - DAI, - REPAY_BUFFER, - USDC, - USDC_HOLDER, - WITHDRAW_BUFFER -} from "test/common/Constants.t.sol"; +import { Assets, AAVE_ORACLE, AAVE_POOL, REPAY_BUFFER, WITHDRAW_BUFFER } from "test/common/Constants.t.sol"; import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IPool } from "src/interfaces/aave/IPool.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol";