diff --git a/README.md b/README.md index f65a6fad..ee58e956 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ Logic: Tests: -- [ ] Separate integration tests from unit tests (separate PR) +- [x] Separate integration tests from unit tests (separate PR) +- [x] Move all utils to test/common/ Considerations: diff --git a/test/FeeCollector.t.sol b/test/FeeCollector.t.sol index 8af61613..cc59f9da 100644 --- a/test/FeeCollector.t.sol +++ b/test/FeeCollector.t.sol @@ -7,22 +7,8 @@ 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, - FEE_COLLECTOR, - TEST_CLIENT, - PROTOCOL_FEE, - CLIENT_RATE, - USDC, - WETH, - WBTC -} from "test/common/Constants.t.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 { 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 FeeCollectorTest is Test, TokenUtils { @@ -49,9 +35,6 @@ contract FeeCollectorTest is Test, TokenUtils { address public positionOwner = address(this); address public feeCollectorAddr; - // Events - event Short(uint256 cAmt, uint256 dAmt, uint256 bAmt); - function setUp() public { // Setup: use mainnet fork mainnetFork = vm.createFork(vm.envString("RPC_URL")); @@ -88,15 +71,6 @@ contract FeeCollectorTest is Test, TokenUtils { 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 @@ -434,163 +408,3 @@ contract FeeCollectorTest is Test, TokenUtils { assertEq(postContractBalance, preContractBalance + _amount); } } - -contract FeeCollectorIntegrationTest 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; - Assets public assets; - TestPosition[] public positions; - - // Test Storage - uint256 public mainnetFork; - 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); - deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); - - // Deploy PositionFactory - vm.prank(CONTRACT_DEPLOYER); - positionFactory = new PositionFactory(CONTRACT_DEPLOYER); - - // Set client rate - vm.prank(CONTRACT_DEPLOYER); - IFeeCollector(FEE_COLLECTOR).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 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(FEE_COLLECTOR); - uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); - uint256 preClientFeeBalance = IFeeCollector(FEE_COLLECTOR).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(FEE_COLLECTOR); - uint256 postTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); - uint256 postClientFeeBalance = IFeeCollector(FEE_COLLECTOR).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(FEE_COLLECTOR); - uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); - - // 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); - - // Assertions - assertEq(postContractBalance, preContractBalance + protocolFee); - assertEq(postTotalClientBalances, preTotalClientBalances); - } - } -} diff --git a/test/Position.t.sol b/test/Position.t.sol index 69779bf9..c178c9f0 100644 --- a/test/Position.t.sol +++ b/test/Position.t.sol @@ -10,21 +10,15 @@ import { PositionFactory } from "src/PositionFactory.sol"; import { PositionAdmin } from "src/PositionAdmin.sol"; import { Assets, - AAVE_ORACLE, CONTRACT_DEPLOYER, DAI, FEE_COLLECTOR, - PROFIT_PERCENT, - REPAY_PERCENT, - SWAP_ROUTER, TEST_CLIENT, USDC, WITHDRAW_BUFFER } from "test/common/Constants.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; -import { DebtUtils } from "test/services/utils/DebtUtils.t.sol"; -import { MockUniswapGains, MockUniswapLosses } from "test/mocks/MockUniswap.t.sol"; -import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; import { IPosition } from "src/interfaces/IPosition.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; @@ -38,42 +32,26 @@ contract PositionTest is Test, TokenUtils, DebtUtils { address bToken; } - struct ContractBalances { - uint256 preBToken; - uint256 postBToken; - uint256 preVDToken; - uint256 postVDToken; - uint256 preAToken; - uint256 postAToken; - uint256 preDToken; - uint256 postDToken; - } - - struct OwnerBalances { - uint256 preBToken; - uint256 postBToken; - uint256 preCToken; - uint256 postCToken; - } - // Test contracts PositionFactory public positionFactory; Assets public assets; TestPosition[] public positions; // Test Storage + VmSafe.Wallet public wallet; address public positionAddr; uint256 public mainnetFork; - address public owner = address(this); - - // Events - event Short(uint256 cAmt, uint256 dAmt, uint256 bAmt); + address public owner; function setUp() public { // Setup: use mainnet fork mainnetFork = vm.createFork(vm.envString("RPC_URL")); vm.selectFork(mainnetFork); + // Set contract owner + wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(1))))); + owner = wallet.addr; + // Deploy assets assets = new Assets(); address[4] memory supportedAssets = assets.getSupported(); @@ -97,6 +75,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils { // Exclude positions with no pool bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); if (k != j && poolExists) { + vm.prank(owner); positionAddr = positionFactory.createPosition(cToken, dToken, bToken); TestPosition memory newPosition = TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); @@ -106,15 +85,6 @@ contract PositionTest is Test, TokenUtils, DebtUtils { } } } - - // 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 @@ -123,61 +93,6 @@ contract PositionTest is Test, TokenUtils, DebtUtils { assertEq(vm.activeFork(), mainnetFork, "vm.activeFork() != mainnetFork"); } - /// @dev - // - Owner's cToken balance should decrease by collateral amount supplied. - // - Position's bToken balance should increase by amount receieved from swap. - // - The above should be true for a wide range of LTVs. - // - The above should be true for a wide range of collateral amounts. - // - The above should be true for all supported tokens. - function testFuzz_Short(uint256 _ltv, uint256 _cAmt) public { - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address addr = positions[i].addr; - address cToken = positions[i].cToken; - address bToken = positions[i].bToken; - - // Bound fuzzed variables - _ltv = bound(_ltv, 1, 60); - _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); - - // Fund owner with collateral - _fund(owner, cToken, _cAmt); - - // Approve position to spend collateral - IERC20(cToken).approve(addr, _cAmt); - - // Pre-act balances - uint256 cTokenPreBal = IERC20(cToken).balanceOf(owner); - uint256 bTokenPreBal = IERC20(bToken).balanceOf(addr); - - // Act - vm.recordLogs(); - IPosition(addr).short(_cAmt, _ltv, 0, 3000, TEST_CLIENT); - VmSafe.Log[] memory entries = vm.getRecordedLogs(); - - // Post-act balances - uint256 cTokenPostBal = IERC20(cToken).balanceOf(owner); - uint256 bTokenPostBal = IERC20(bToken).balanceOf(addr); - bytes memory shortEvent = entries[entries.length - 1].data; - uint256 bAmt; - - assembly { - let startPos := sub(mload(shortEvent), 32) - bAmt := mload(add(shortEvent, add(0x20, startPos))) - } - - // Assertions - assertEq(cTokenPostBal, cTokenPreBal - _cAmt); - assertEq(bTokenPostBal, bTokenPreBal + bAmt); - - // Revert to snapshot - vm.revertTo(id); - } - } - /// @dev // - It should revert with Unauthorized() error when called by an unauthorized sender. function testFuzz_CannotShort(address _sender) public { @@ -213,283 +128,31 @@ contract PositionTest is Test, TokenUtils, DebtUtils { } /// @dev - // - Position contract's bToken balance should go to 0. - // - Position contract's debt on Aave should go to 0. - // - Owner's cToken balance should increase by the amount of collateral withdrawn. - // - Owner's bToken balance should increase by the position's gains amount. - // - The above should be true for all supported tokens. - function test_CloseWithGainsExactOutput() public { - // Setup - ContractBalances memory contractBalances; - OwnerBalances memory ownerBalances; - - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address addr = positions[i].addr; - - // Setup: open short position - uint256 cAmt = assets.maxCAmts(positions[i].cToken); - uint256 ltv = 50; - _fund(owner, positions[i].cToken, cAmt); - IERC20(positions[i].cToken).approve(addr, cAmt); - IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); - - // Get pre-act balances - contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); - ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); - - // Assertions - assertEq(ownerBalances.preBToken, 0); - assertNotEq(contractBalances.preBToken, 0); - assertNotEq(contractBalances.preVDToken, 0); - - // Mock Uniswap to ensure position gains - _fund(SWAP_ROUTER, positions[i].dToken, contractBalances.preVDToken); - bytes memory code = address(new MockUniswapGains()).code; - vm.etch(SWAP_ROUTER, code); - - // Act - /// @dev start event recorder - vm.recordLogs(); - IPosition(addr).close(3000, true, 0, WITHDRAW_BUFFER); - VmSafe.Log[] memory entries = vm.getRecordedLogs(); - - // Get post-act balances - contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); - ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); - - bytes memory closeEvent = entries[entries.length - 1].data; - uint256 gains; - - assembly { - gains := mload(add(closeEvent, 0x20)) - } - - // Assertions: - assertEq(contractBalances.postBToken, 0); - assertEq(contractBalances.postVDToken, 0); - assertEq(contractBalances.postAToken, 0); - assertApproxEqAbs(gains, contractBalances.preBToken * PROFIT_PERCENT / 100, 1); - - if (positions[i].bToken == positions[i].cToken) { - /// @dev In this case, bToken and cToken balances will increase by the same amount (gains + collateral withdrawn) - assertEq(ownerBalances.postBToken, ownerBalances.preBToken + gains + contractBalances.preAToken); - assertEq(ownerBalances.postCToken, ownerBalances.postBToken); - } else { - assertEq(ownerBalances.postBToken, ownerBalances.preBToken + gains); - assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preAToken); - } - - // Revert to snapshot - vm.revertTo(id); - } - } - - /// @dev - // - Position contract's bToken balance should go to 0. - // - Position contract's debt on Aave should go to 0. - // - Position contract's dToken balance should be the amount received from swap minus the amount repaid to Aave. - // - Owner's cToken balance should increase by the amount of collateral withdrawn. - // - Owner's bToken balance should stay the same, as there are no gains. - // - The above should be true for all supported tokens. - function testFuzz_CloseWithGainsExactInput(uint256 _dAmtRemainder) public { - // Setup - ContractBalances memory contractBalances; - OwnerBalances memory ownerBalances; - - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address addr = positions[i].addr; - - // Setup: open short position - uint256 cAmt = assets.maxCAmts(positions[i].cToken); - uint256 ltv = 50; - _fund(owner, positions[i].cToken, cAmt); - IERC20(positions[i].cToken).approve(addr, cAmt); - IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); - - // Get pre-act balances - contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); - contractBalances.preDToken = IERC20(positions[i].dToken).balanceOf(addr); - ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); - - // Assertions - assertEq(ownerBalances.preBToken, 0); - assertEq(contractBalances.preDToken, 0); - assertNotEq(contractBalances.preBToken, 0); - assertNotEq(contractBalances.preVDToken, 0); - - // Bound: upper bound is 150% of preVDToken - uint256 upperBound = contractBalances.preVDToken + (contractBalances.preVDToken * 50) / 100; - _dAmtRemainder = bound(_dAmtRemainder, 2, upperBound); - - // Mock Uniswap to ensure position gains - uint256 amountOut = contractBalances.preVDToken + _dAmtRemainder; - _fund(SWAP_ROUTER, positions[i].dToken, contractBalances.preVDToken + _dAmtRemainder); - bytes memory code = address(new MockUniswapGains()).code; - vm.etch(SWAP_ROUTER, code); - - // Act - /// @dev start event recorder - vm.recordLogs(); - IPosition(addr).close(3000, false, 0, WITHDRAW_BUFFER); - VmSafe.Log[] memory entries = vm.getRecordedLogs(); - - // Get post-act balances - contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); - contractBalances.postDToken = IERC20(positions[i].dToken).balanceOf(addr); - ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); - - bytes memory closeEvent = entries[entries.length - 1].data; - uint256 gains; - - assembly { - gains := mload(add(closeEvent, 0x20)) - } - - // Assertions: - assertApproxEqAbs(contractBalances.postDToken, amountOut - contractBalances.preVDToken, 1); - assertEq(contractBalances.postBToken, 0); - assertEq(contractBalances.postVDToken, 0); - assertEq(contractBalances.postAToken, 0); - assertEq(gains, 0); - - if (positions[i].bToken == positions[i].cToken) { - /// @dev In this case, bToken and cToken balances will increase by the same amount (collateral withdrawn) - assertEq(ownerBalances.postBToken, ownerBalances.preBToken + contractBalances.preAToken); - assertEq(ownerBalances.postCToken, ownerBalances.postBToken); - } else { - assertEq(ownerBalances.postBToken, ownerBalances.preBToken); - assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preAToken); - } - - // Revert to snapshot - vm.revertTo(id); - } - } - - /// @dev - // - Position contract's bToken balance should go to 0. - // - Position contract's debt on Aave should decrease by the amount received from the swap. - // - Owner's cToken balance should increase by the amount of collateral withdrawn. - // - If bToken != cToken, the owner's bToken balance should not increase. - // - The above should be true for all supported tokens. - function test_CloseNoGainsExactInput() public { - // Setup - ContractBalances memory contractBalances; - OwnerBalances memory ownerBalances; - - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address addr = positions[i].addr; - - // Setup: open short position - uint256 cAmt = assets.maxCAmts(positions[i].cToken); - uint256 ltv = 50; - _fund(owner, positions[i].cToken, cAmt); - IERC20(positions[i].cToken).approve(addr, cAmt); - IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); - - // Get pre-act balances - contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); - ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); - - // Assertions - assertEq(ownerBalances.preBToken, 0); - assertNotEq(contractBalances.preBToken, 0); - assertNotEq(contractBalances.preVDToken, 0); - - // Mock Uniswap to ensure position gains - uint256 repayAmt = contractBalances.preVDToken * REPAY_PERCENT / 100; - _fund(SWAP_ROUTER, positions[i].dToken, repayAmt); - bytes memory code = address(new MockUniswapLosses()).code; - vm.etch(SWAP_ROUTER, code); - - // Act - IPosition(addr).close(3000, false, 0, WITHDRAW_BUFFER); - - // Get post-act balances - contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); - contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); - contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); - ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); - ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); - - // Assertions - assertApproxEqAbs( - contractBalances.postVDToken, contractBalances.preVDToken * (100 - REPAY_PERCENT) / 100, 1 - ); - assertApproxEqAbs(contractBalances.postVDToken, contractBalances.preVDToken - repayAmt, 1); - assertEq(contractBalances.postBToken, 0); - uint256 withdrawAmt = contractBalances.preAToken - contractBalances.postAToken; - assertApproxEqAbs(ownerBalances.postCToken, ownerBalances.preCToken + withdrawAmt, 1); - if (positions[i].bToken == positions[i].cToken) { - /// @dev In this case, bToken and cToken balances will increase by the same amount (the collateral amount withdrawn) - assertEq(ownerBalances.postBToken, ownerBalances.postCToken); - } else { - assertEq(ownerBalances.postBToken, ownerBalances.preBToken); - } - - // Revert to snapshot - vm.revertTo(id); - } - } - - /// @dev - // - It should revert because the position contract doesn't have enough bTokens to facilitate the exact output swap. - function testFail_CloseNoGainsExactOutput() public { - // Setup - ContractBalances memory contractBalances; - + // - It should revert with Unauthorized() error when called by an unauthorized sender. + function testFuzz_CannotShortWithPermit(address _sender) public { // Take snapshot uint256 id = vm.snapshot(); for (uint256 i; i < positions.length; i++) { // Test variables - address addr = positions[i].addr; + address cToken = positions[i].cToken; + uint256 cAmt = assets.maxCAmts(cToken); + uint256 ltv = 60; - // Setup: open short position - uint256 cAmt = assets.maxCAmts(positions[i].cToken); - uint256 ltv = 50; - _fund(owner, positions[i].cToken, cAmt); - IERC20(positions[i].cToken).approve(addr, cAmt); - IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + // Assumptions + vm.assume(_sender != owner); - // Get pre-act balances - contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + // Fund owner with collateral + _fund(owner, cToken, cAmt); - // Mock Uniswap to ensure position gains - uint256 repayAmt = contractBalances.preVDToken * REPAY_PERCENT / 100; - _fund(SWAP_ROUTER, positions[i].dToken, repayAmt); - bytes memory code = address(new MockUniswapLosses()).code; - vm.etch(SWAP_ROUTER, code); + // Get permit + uint256 permitTimestamp = block.timestamp + 1000; + (uint8 v, bytes32 r, bytes32 s) = _getPermit(cToken, wallet, positions[i].addr, cAmt, permitTimestamp); // Act - IPosition(addr).close(3000, true, 0, WITHDRAW_BUFFER); + vm.prank(_sender); + vm.expectRevert(PositionAdmin.Unauthorized.selector); + IPosition(positions[i].addr).shortWithPermit(cAmt, ltv, 0, 3000, TEST_CLIENT, permitTimestamp, v, r, s); // Revert to snapshot vm.revertTo(id); @@ -515,8 +178,10 @@ contract PositionTest is Test, TokenUtils, DebtUtils { // Setup: open short position uint256 cAmt = assets.maxCAmts(positions[i].cToken); _fund(owner, positions[i].cToken, cAmt); + vm.startPrank(owner); IERC20(positions[i].cToken).approve(addr, cAmt); IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + vm.stopPrank(); // Act vm.prank(_sender); @@ -528,191 +193,3 @@ contract PositionTest is Test, TokenUtils, DebtUtils { } } } - -contract PositionPermitTest is Test, TokenUtils, DebtUtils { - /* solhint-disable func-name-mixedcase */ - - struct TestPosition { - address addr; - address cToken; - address dToken; - address bToken; - } - - struct ContractBalances { - uint256 preBToken; - uint256 postBToken; - uint256 preVDToken; - uint256 postVDToken; - uint256 preAToken; - uint256 postAToken; - uint256 preDToken; - uint256 postDToken; - } - - struct OwnerBalances { - uint256 preBToken; - uint256 postBToken; - uint256 preCToken; - uint256 postCToken; - } - - // Test contracts - PositionFactory public positionFactory; - Assets public assets; - TestPosition[] public positions; - - // Test Storage - VmSafe.Wallet public wallet; - address public positionAddr; - uint256 public mainnetFork; - address public owner; - - // 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); - deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); - - // Deploy PositionFactory - vm.prank(CONTRACT_DEPLOYER); - positionFactory = new PositionFactory(CONTRACT_DEPLOYER); - - // Set contract owner - wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(1))))); - owner = wallet.addr; - - // Deploy and store all possible positions - for (uint256 i; i < supportedAssets.length; i++) { - address cToken = supportedAssets[i]; - for (uint256 j; j < supportedAssets.length; j++) { - if (j != i) { - address dToken = supportedAssets[j]; - for (uint256 k; k < supportedAssets.length; k++) { - address bToken = supportedAssets[k]; - // Exclude positions with no pool - bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); - if (k != j && poolExists) { - vm.prank(owner); - positionAddr = positionFactory.createPosition(cToken, dToken, bToken); - TestPosition memory newPosition = - TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); - 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 - // - Owner's cToken balance should decrease by collateral amount supplied. - // - Position's bToken balance should increase by amount receieved from swap. - // - The above should be true for a wide range of LTVs. - // - The above should be true for a wide range of collateral amounts. - // - The above should be true for all supported tokens. - // - The act should be accomplished without a separate approve tx. - function testFuzz_ShortWithPermit(uint256 _ltv, uint256 _cAmt) public { - ContractBalances memory contractBalances; - OwnerBalances memory ownerBalances; - - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address cToken = positions[i].cToken; - address bToken = positions[i].bToken; - - // Bound fuzzed variables - _ltv = bound(_ltv, 1, 60); - _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); - - // Fund owner with collateral - _fund(owner, cToken, _cAmt); - - // Get permit - uint256 permitTimestamp = block.timestamp + 1000; - (uint8 v, bytes32 r, bytes32 s) = _getPermit(cToken, wallet, positions[i].addr, _cAmt, permitTimestamp); - - // Pre-act balances - contractBalances.preBToken = IERC20(bToken).balanceOf(positions[i].addr); - ownerBalances.preCToken = IERC20(cToken).balanceOf(owner); - - // Act - vm.recordLogs(); - vm.prank(owner); - IPosition(positions[i].addr).shortWithPermit(_cAmt, _ltv, 0, 3000, TEST_CLIENT, permitTimestamp, v, r, s); - VmSafe.Log[] memory entries = vm.getRecordedLogs(); - - // Post-act balances - contractBalances.postBToken = IERC20(bToken).balanceOf(positions[i].addr); - ownerBalances.postCToken = IERC20(cToken).balanceOf(owner); - bytes memory shortEvent = entries[entries.length - 1].data; - uint256 bAmt; - - assembly { - let startPos := sub(mload(shortEvent), 32) - bAmt := mload(add(shortEvent, add(0x20, startPos))) - } - - // Assertions - assertEq(ownerBalances.postCToken, ownerBalances.preCToken - _cAmt); - assertEq(contractBalances.postBToken, contractBalances.preBToken + bAmt); - - // Revert to snapshot - vm.revertTo(id); - } - } - - /// @dev - // - It should revert with Unauthorized() error when called by an unauthorized sender. - function testFuzz_CannotShortWithPermit(address _sender) public { - // Take snapshot - uint256 id = vm.snapshot(); - - for (uint256 i; i < positions.length; i++) { - // Test variables - address cToken = positions[i].cToken; - uint256 cAmt = assets.maxCAmts(cToken); - uint256 ltv = 60; - - // Assumptions - vm.assume(_sender != owner); - - // Fund owner with collateral - _fund(owner, cToken, cAmt); - - // Get permit - uint256 permitTimestamp = block.timestamp + 1000; - (uint8 v, bytes32 r, bytes32 s) = _getPermit(cToken, wallet, positions[i].addr, cAmt, permitTimestamp); - - // Act - vm.prank(_sender); - vm.expectRevert(PositionAdmin.Unauthorized.selector); - IPosition(positions[i].addr).shortWithPermit(cAmt, ltv, 0, 3000, TEST_CLIENT, permitTimestamp, v, r, s); - - // Revert to snapshot - vm.revertTo(id); - } - } -} diff --git a/test/services/utils/DebtUtils.t.sol b/test/common/utils/DebtUtils.t.sol similarity index 100% rename from test/services/utils/DebtUtils.t.sol rename to test/common/utils/DebtUtils.t.sol diff --git a/test/integration/FeeCollector.short.t.sol b/test/integration/FeeCollector.short.t.sol new file mode 100644 index 00000000..6321e063 --- /dev/null +++ b/test/integration/FeeCollector.short.t.sol @@ -0,0 +1,180 @@ +// 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, + FEE_COLLECTOR, + 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 { 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 { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + uint256 public mainnetFork; + address public positionOwner = address(this); + + 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); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Set client rate + vm.prank(CONTRACT_DEPLOYER); + IFeeCollector(FEE_COLLECTOR).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 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 { + 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(FEE_COLLECTOR); + uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 preClientFeeBalance = IFeeCollector(FEE_COLLECTOR).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(FEE_COLLECTOR); + uint256 postTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + uint256 postClientFeeBalance = IFeeCollector(FEE_COLLECTOR).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_ShortCollectFeesNoClient(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(FEE_COLLECTOR); + uint256 preTotalClientBalances = IFeeCollector(FEE_COLLECTOR).totalClientBalances(cToken); + + // 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); + + // Assertions + assertEq(postContractBalance, preContractBalance + protocolFee); + assertEq(postTotalClientBalances, preTotalClientBalances); + } + } +} diff --git a/test/integration/Position.close.t.sol b/test/integration/Position.close.t.sol new file mode 100644 index 00000000..501753fa --- /dev/null +++ b/test/integration/Position.close.t.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, + AAVE_ORACLE, + CONTRACT_DEPLOYER, + DAI, + FEE_COLLECTOR, + PROFIT_PERCENT, + REPAY_PERCENT, + SWAP_ROUTER, + TEST_CLIENT, + USDC, + WITHDRAW_BUFFER +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; +import { MockUniswapGains, MockUniswapLosses } from "test/mocks/MockUniswap.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 PositionCloseTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + struct ContractBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preVDToken; + uint256 postVDToken; + uint256 preAToken; + uint256 postAToken; + uint256 preDToken; + uint256 postDToken; + } + + struct OwnerBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preCToken; + uint256 postCToken; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + address public positionAddr; + uint256 public mainnetFork; + address public owner = address(this); + + 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); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + 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 + // - Position contract's bToken balance should go to 0. + // - Position contract's debt on Aave should go to 0. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should increase by the position's gains amount. + // - The above should be true for all supported tokens. + function test_CloseWithGainsExactOutput() public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address addr = positions[i].addr; + + // Setup: open short position + uint256 cAmt = assets.maxCAmts(positions[i].cToken); + uint256 ltv = 50; + _fund(owner, positions[i].cToken, cAmt); + IERC20(positions[i].cToken).approve(addr, cAmt); + IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); + ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertNotEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + + // Mock Uniswap to ensure position gains + _fund(SWAP_ROUTER, positions[i].dToken, contractBalances.preVDToken); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(addr).close(3000, true, 0, WITHDRAW_BUFFER); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); + ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); + + bytes memory closeEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(closeEvent, 0x20)) + } + + // Assertions: + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postAToken, 0); + assertApproxEqAbs(gains, contractBalances.preBToken * PROFIT_PERCENT / 100, 1); + + if (positions[i].bToken == positions[i].cToken) { + /// @dev In this case, bToken and cToken balances will increase by the same amount (gains + collateral withdrawn) + assertEq(ownerBalances.postBToken, ownerBalances.preBToken + gains + contractBalances.preAToken); + assertEq(ownerBalances.postCToken, ownerBalances.postBToken); + } else { + assertEq(ownerBalances.postBToken, ownerBalances.preBToken + gains); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preAToken); + } + + // Revert to snapshot + vm.revertTo(id); + } + } + + /// @dev + // - Position contract's bToken balance should go to 0. + // - Position contract's debt on Aave should go to 0. + // - Position contract's dToken balance should be the amount received from swap minus the amount repaid to Aave. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should stay the same, as there are no gains. + // - The above should be true for all supported tokens. + function testFuzz_CloseWithGainsExactInput(uint256 _dAmtRemainder) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address addr = positions[i].addr; + + // Setup: open short position + uint256 cAmt = assets.maxCAmts(positions[i].cToken); + uint256 ltv = 50; + _fund(owner, positions[i].cToken, cAmt); + IERC20(positions[i].cToken).approve(addr, cAmt); + IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); + contractBalances.preDToken = IERC20(positions[i].dToken).balanceOf(addr); + ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertNotEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + + // Bound: upper bound is 150% of preVDToken + uint256 upperBound = contractBalances.preVDToken + (contractBalances.preVDToken * 50) / 100; + _dAmtRemainder = bound(_dAmtRemainder, 2, upperBound); + + // Mock Uniswap to ensure position gains + uint256 amountOut = contractBalances.preVDToken + _dAmtRemainder; + _fund(SWAP_ROUTER, positions[i].dToken, contractBalances.preVDToken + _dAmtRemainder); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(addr).close(3000, false, 0, WITHDRAW_BUFFER); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); + contractBalances.postDToken = IERC20(positions[i].dToken).balanceOf(addr); + ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); + + bytes memory closeEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(closeEvent, 0x20)) + } + + // Assertions: + assertApproxEqAbs(contractBalances.postDToken, amountOut - contractBalances.preVDToken, 1); + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postAToken, 0); + assertEq(gains, 0); + + if (positions[i].bToken == positions[i].cToken) { + /// @dev In this case, bToken and cToken balances will increase by the same amount (collateral withdrawn) + assertEq(ownerBalances.postBToken, ownerBalances.preBToken + contractBalances.preAToken); + assertEq(ownerBalances.postCToken, ownerBalances.postBToken); + } else { + assertEq(ownerBalances.postBToken, ownerBalances.preBToken); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preAToken); + } + + // Revert to snapshot + vm.revertTo(id); + } + } + + /// @dev + // - Position contract's bToken balance should go to 0. + // - Position contract's debt on Aave should decrease by the amount received from the swap. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - If bToken != cToken, the owner's bToken balance should not increase. + // - The above should be true for all supported tokens. + function test_CloseNoGainsExactInput() public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address addr = positions[i].addr; + + // Setup: open short position + uint256 cAmt = assets.maxCAmts(positions[i].cToken); + uint256 ltv = 50; + _fund(owner, positions[i].cToken, cAmt); + IERC20(positions[i].cToken).approve(addr, cAmt); + IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.preAToken = _getATokenBalance(addr, positions[i].cToken); + ownerBalances.preBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(positions[i].cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertNotEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + + // Mock Uniswap to ensure position gains + uint256 repayAmt = contractBalances.preVDToken * REPAY_PERCENT / 100; + _fund(SWAP_ROUTER, positions[i].dToken, repayAmt); + bytes memory code = address(new MockUniswapLosses()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + IPosition(addr).close(3000, false, 0, WITHDRAW_BUFFER); + + // Get post-act balances + contractBalances.postBToken = IERC20(positions[i].bToken).balanceOf(addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + contractBalances.postAToken = _getATokenBalance(addr, positions[i].cToken); + ownerBalances.postBToken = IERC20(positions[i].bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(positions[i].cToken).balanceOf(owner); + + // Assertions + assertApproxEqAbs( + contractBalances.postVDToken, contractBalances.preVDToken * (100 - REPAY_PERCENT) / 100, 1 + ); + assertApproxEqAbs(contractBalances.postVDToken, contractBalances.preVDToken - repayAmt, 1); + assertEq(contractBalances.postBToken, 0); + uint256 withdrawAmt = contractBalances.preAToken - contractBalances.postAToken; + assertApproxEqAbs(ownerBalances.postCToken, ownerBalances.preCToken + withdrawAmt, 1); + if (positions[i].bToken == positions[i].cToken) { + /// @dev In this case, bToken and cToken balances will increase by the same amount (the collateral amount withdrawn) + assertEq(ownerBalances.postBToken, ownerBalances.postCToken); + } else { + assertEq(ownerBalances.postBToken, ownerBalances.preBToken); + } + + // Revert to snapshot + vm.revertTo(id); + } + } + + /// @dev + // - It should revert because the position contract doesn't have enough bTokens to facilitate the exact output swap. + function testFail_CloseNoGainsExactOutput() public { + // Setup + ContractBalances memory contractBalances; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address addr = positions[i].addr; + + // Setup: open short position + uint256 cAmt = assets.maxCAmts(positions[i].cToken); + uint256 ltv = 50; + _fund(owner, positions[i].cToken, cAmt); + IERC20(positions[i].cToken).approve(addr, cAmt); + IPosition(addr).short(cAmt, ltv, 0, 3000, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preVDToken = _getVariableDebtTokenBalance(addr, positions[i].dToken); + + // Mock Uniswap to ensure position gains + uint256 repayAmt = contractBalances.preVDToken * REPAY_PERCENT / 100; + _fund(SWAP_ROUTER, positions[i].dToken, repayAmt); + bytes memory code = address(new MockUniswapLosses()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + IPosition(addr).close(3000, true, 0, WITHDRAW_BUFFER); + + // Revert to snapshot + vm.revertTo(id); + } + } +} diff --git a/test/integration/Position.short.t.sol b/test/integration/Position.short.t.sol new file mode 100644 index 00000000..363d43da --- /dev/null +++ b/test/integration/Position.short.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, AAVE_ORACLE, CONTRACT_DEPLOYER, DAI, FEE_COLLECTOR, TEST_CLIENT, USDC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.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 PositionShortTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + address public positionAddr; + uint256 public mainnetFork; + address public owner = 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); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + 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 + // - Owner's cToken balance should decrease by collateral amount supplied. + // - Position's bToken balance should increase by amount receieved from swap. + // - The above should be true for a wide range of LTVs. + // - The above should be true for a wide range of collateral amounts. + // - The above should be true for all supported tokens. + function testFuzz_Short(uint256 _ltv, uint256 _cAmt) public { + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address addr = positions[i].addr; + address cToken = positions[i].cToken; + address bToken = positions[i].bToken; + + // Bound fuzzed variables + _ltv = bound(_ltv, 1, 60); + _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); + + // Fund owner with collateral + _fund(owner, cToken, _cAmt); + + // Approve position to spend collateral + IERC20(cToken).approve(addr, _cAmt); + + // Pre-act balances + uint256 cTokenPreBal = IERC20(cToken).balanceOf(owner); + uint256 bTokenPreBal = IERC20(bToken).balanceOf(addr); + + // Act + vm.recordLogs(); + IPosition(addr).short(_cAmt, _ltv, 0, 3000, TEST_CLIENT); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Post-act balances + uint256 cTokenPostBal = IERC20(cToken).balanceOf(owner); + uint256 bTokenPostBal = IERC20(bToken).balanceOf(addr); + bytes memory shortEvent = entries[entries.length - 1].data; + uint256 bAmt; + + assembly { + let startPos := sub(mload(shortEvent), 32) + bAmt := mload(add(shortEvent, add(0x20, startPos))) + } + + // Assertions + assertEq(cTokenPostBal, cTokenPreBal - _cAmt); + assertEq(bTokenPostBal, bTokenPreBal + bAmt); + + // Revert to snapshot + vm.revertTo(id); + } + } +} diff --git a/test/integration/Position.shortWithPermit.t.sol b/test/integration/Position.shortWithPermit.t.sol new file mode 100644 index 00000000..f22a5a4d --- /dev/null +++ b/test/integration/Position.shortWithPermit.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, AAVE_ORACLE, CONTRACT_DEPLOYER, DAI, FEE_COLLECTOR, TEST_CLIENT, USDC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.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 PositionShortPermitTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + struct ContractBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preVDToken; + uint256 postVDToken; + uint256 preAToken; + uint256 postAToken; + uint256 preDToken; + uint256 postDToken; + } + + struct OwnerBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preCToken; + uint256 postCToken; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + VmSafe.Wallet public wallet; + address public positionAddr; + uint256 public mainnetFork; + address public owner; + + // 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); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Set contract owner + wallet = vm.createWallet(uint256(keccak256(abi.encodePacked(uint256(1))))); + owner = wallet.addr; + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + vm.prank(owner); + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + 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 + // - Owner's cToken balance should decrease by collateral amount supplied. + // - Position's bToken balance should increase by amount receieved from swap. + // - The above should be true for a wide range of LTVs. + // - The above should be true for a wide range of collateral amounts. + // - The above should be true for all supported tokens. + // - The act should be accomplished without a separate approve tx. + function testFuzz_ShortWithPermit(uint256 _ltv, uint256 _cAmt) public { + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + address cToken = positions[i].cToken; + address bToken = positions[i].bToken; + + // Bound fuzzed variables + _ltv = bound(_ltv, 1, 60); + _cAmt = bound(_cAmt, assets.minCAmts(cToken), assets.maxCAmts(cToken)); + + // Fund owner with collateral + _fund(owner, cToken, _cAmt); + + // Get permit + uint256 permitTimestamp = block.timestamp + 1000; + (uint8 v, bytes32 r, bytes32 s) = _getPermit(cToken, wallet, positions[i].addr, _cAmt, permitTimestamp); + + // Pre-act balances + contractBalances.preBToken = IERC20(bToken).balanceOf(positions[i].addr); + ownerBalances.preCToken = IERC20(cToken).balanceOf(owner); + + // Act + vm.recordLogs(); + vm.prank(owner); + IPosition(positions[i].addr).shortWithPermit(_cAmt, _ltv, 0, 3000, TEST_CLIENT, permitTimestamp, v, r, s); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Post-act balances + contractBalances.postBToken = IERC20(bToken).balanceOf(positions[i].addr); + ownerBalances.postCToken = IERC20(cToken).balanceOf(owner); + bytes memory shortEvent = entries[entries.length - 1].data; + uint256 bAmt; + + assembly { + let startPos := sub(mload(shortEvent), 32) + bAmt := mload(add(shortEvent, add(0x20, startPos))) + } + + // Assertions + assertEq(ownerBalances.postCToken, ownerBalances.preCToken - _cAmt); + assertEq(contractBalances.postBToken, contractBalances.preBToken + bAmt); + + // Revert to snapshot + vm.revertTo(id); + } + } +} diff --git a/test/services/DebtService.t.sol b/test/services/DebtService.t.sol index 3156c470..4f1f487b 100644 --- a/test/services/DebtService.t.sol +++ b/test/services/DebtService.t.sol @@ -8,7 +8,7 @@ import { VmSafe } from "forge-std/Vm.sol"; // Local Imports import { PositionAdmin } from "src/PositionAdmin.sol"; import { DebtServiceHarness } from "test/harness/DebtServiceHarness.t.sol"; -import { DebtUtils } from "test/services/utils/DebtUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; import { TokenUtils } from "test/common/utils/TokenUtils.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";