diff --git a/Makefile b/Makefile index 2c2cdb255..cb44c3d33 100644 --- a/Makefile +++ b/Makefile @@ -8,41 +8,49 @@ NETWORK?=eth-mainnet FOUNDRY_SRC=contracts/${PROTOCOL}/ FOUNDRY_TEST=test-foundry/${PROTOCOL}/ FOUNDRY_REMAPPINGS=@config/=config/${NETWORK}/${PROTOCOL}/ -FOUNDRY_ETH_RPC_URL?=https://${NETWORK}.g.alchemy.com/v2/${ALCHEMY_KEY} + FOUNDRY_PRIVATE_KEY?=${DEPLOYER_PRIVATE_KEY} -ifeq (${NETWORK}, eth-mainnet) - FOUNDRY_CHAIN_ID=1 - FOUNDRY_FORK_BLOCK_NUMBER=14292587 -endif +ifdef FOUNDRY_ETH_RPC_URL + FOUNDRY_TEST=test-foundry/prod/${PROTOCOL}/ + FOUNDRY_FUZZ_RUNS=4096 + FOUNDRY_FUZZ_MAX_LOCAL_REJECTS=16384 + FOUNDRY_FUZZ_MAX_GLOBAL_REJECTS=1048576 +else + FOUNDRY_ETH_RPC_URL=https://${NETWORK}.g.alchemy.com/v2/${ALCHEMY_KEY} -ifeq (${NETWORK}, eth-ropsten) - FOUNDRY_CHAIN_ID=3 -endif + ifeq (${NETWORK}, eth-mainnet) + FOUNDRY_CHAIN_ID=1 + FOUNDRY_FORK_BLOCK_NUMBER?=14292587 + endif -ifeq (${NETWORK}, eth-goerli) - FOUNDRY_CHAIN_ID=5 -endif + ifeq (${NETWORK}, eth-ropsten) + FOUNDRY_CHAIN_ID=3 + endif -ifeq (${NETWORK}, polygon-mainnet) - FOUNDRY_CHAIN_ID=137 - FOUNDRY_FORK_BLOCK_NUMBER=22116728 + ifeq (${NETWORK}, eth-goerli) + FOUNDRY_CHAIN_ID=5 + endif - ifeq (${PROTOCOL}, aave-v3) - FOUNDRY_FORK_BLOCK_NUMBER=29116728 - FOUNDRY_CONTRACT_PATTERN_INVERSE=(Fees|IncentivesVault|Rewards) + ifeq (${NETWORK}, polygon-mainnet) + ifeq (${PROTOCOL}, aave-v3) + FOUNDRY_FORK_BLOCK_NUMBER?=29116728 + FOUNDRY_CONTRACT_PATTERN_INVERSE=(Fees|IncentivesVault|Rewards) + endif + + FOUNDRY_CHAIN_ID=137 + FOUNDRY_FORK_BLOCK_NUMBER?=22116728 endif -endif -ifeq (${NETWORK}, avalanche-mainnet) - FOUNDRY_CHAIN_ID=43114 - FOUNDRY_ETH_RPC_URL=https://api.avax.network/ext/bc/C/rpc - FOUNDRY_FORK_BLOCK_NUMBER=12675271 + ifeq (${NETWORK}, avalanche-mainnet) + ifeq (${PROTOCOL}, aave-v3) + FOUNDRY_FORK_BLOCK_NUMBER?=15675271 + endif - ifeq (${PROTOCOL}, aave-v3) - FOUNDRY_FORK_BLOCK_NUMBER=15675271 + FOUNDRY_CHAIN_ID=43114 + FOUNDRY_ETH_RPC_URL=https://api.avax.network/ext/bc/C/rpc + FOUNDRY_FORK_BLOCK_NUMBER?=12675271 endif -else endif ifeq (${SMODE}, local) @@ -70,32 +78,32 @@ create-market: ./scripts/${PROTOCOL}/create-market.sh anvil: - @echo Starting fork of ${NETWORK} - @anvil --fork-url ${FOUNDRY_ETH_RPC_URL} + @echo Starting fork of ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} + @anvil --fork-url ${FOUNDRY_ETH_RPC_URL} --fork-block-number ${FOUNDRY_FORK_BLOCK_NUMBER} script-%: @echo Running script $* of ${PROTOCOL} on ${NETWORK} with script mode: ${SMODE} @forge script scripts/${PROTOCOL}/$*.s.sol:$* --broadcast -vvvv test: - @echo Running all ${PROTOCOL} tests on ${NETWORK} + @echo Running all ${PROTOCOL} tests on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge test -vv | tee trace.ansi coverage: - @echo Create coverage report for ${PROTOCOL} tests on ${NETWORK} + @echo Create coverage report for ${PROTOCOL} tests on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge coverage coverage-lcov: - @echo Create coverage lcov for ${PROTOCOL} tests on ${NETWORK} + @echo Create coverage lcov for ${PROTOCOL} tests on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge coverage --report lcov fuzz: $(eval FOUNDRY_TEST=test-foundry/fuzzing/${PROTOCOL}/) - @echo Running all ${PROTOCOL} fuzzing tests on ${NETWORK} + @echo Running all ${PROTOCOL} fuzzing tests on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge test -vv gas-report: - @echo Creating gas report for ${PROTOCOL} on ${NETWORK} + @echo Creating gas report for ${PROTOCOL} on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge test --gas-report test-common: @@ -103,11 +111,11 @@ test-common: @FOUNDRY_TEST=test-foundry/common forge test -vvv contract-% c-%: - @echo Running tests for contract $* of ${PROTOCOL} on ${NETWORK} + @echo Running tests for contract $* of ${PROTOCOL} on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge test -vvv --match-contract $* | tee trace.ansi single-% s-%: - @echo Running single test $* of ${PROTOCOL} on ${NETWORK} + @echo Running single test $* of ${PROTOCOL} on ${NETWORK} at block ${FOUNDRY_FORK_BLOCK_NUMBER} @forge test -vvv --match-test $* | tee trace.ansi storage-layout-generate: diff --git a/config/eth-mainnet/compound/Config.sol b/config/eth-mainnet/compound/Config.sol index 859a6b7b8..a61a8e338 100644 --- a/config/eth-mainnet/compound/Config.sol +++ b/config/eth-mainnet/compound/Config.sol @@ -1,6 +1,22 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity 0.8.13; +import "@contracts/compound/interfaces/compound/ICompound.sol"; +import {IIncentivesVault} from "@contracts/compound/interfaces/IIncentivesVault.sol"; +import {IPositionsManager} from "@contracts/compound/interfaces/IPositionsManager.sol"; +import {IInterestRatesManager} from "@contracts/compound/interfaces/IInterestRatesManager.sol"; + +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {ERC20} from "@rari-capital/solmate/src/tokens/ERC20.sol"; + +import {RewardsManager} from "@contracts/compound/RewardsManager.sol"; +import {PositionsManager} from "@contracts/compound/PositionsManager.sol"; +import {InterestRatesManager} from "@contracts/compound/InterestRatesManager.sol"; +import {IncentivesVault} from "@contracts/compound/IncentivesVault.sol"; +import {Lens} from "@contracts/compound/lens/Lens.sol"; +import {Morpho} from "@contracts/compound/Morpho.sol"; + contract Config { address constant aave = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; address constant dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F; @@ -26,10 +42,10 @@ contract Config { address constant cUsdt = 0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9; address constant cWbtc2 = 0xccF4429DB6322D5C611ee964527D42E5d685DD6a; address constant cEth = 0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5; + address constant cComp = 0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4; address constant cBat = 0x6C8c6b02E7b2BE14d4fA6022Dfd6d75921D90E4E; address constant cTusd = 0x12392F67bdf24faE0AF363c24aC620a2f67DAd86; address constant cUni = 0x35A18000230DA775CAc24873d00Ff85BccdeD550; - address constant cComp = 0x70e36f6BF80a52b3B46b3aF8e106CC0ed743E8e4; address constant cZrx = 0xB3319f5D18Bc0D84dD1b4825Dcde5d5f7266d407; address constant cLink = 0xFAce851a4921ce59e912d19329929CE6da6EB0c7; address constant cMkr = 0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b; @@ -38,5 +54,25 @@ contract Config { address constant cUsdp = 0x041171993284df560249B57358F931D9eB7b925D; address constant cSushi = 0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7; - address constant comptrollerAddress = 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B; + address public morphoDao = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; + IComptroller public comptroller = IComptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); + + ProxyAdmin public proxyAdmin = ProxyAdmin(0x99917ca0426fbC677e84f873Fb0b726Bb4799cD8); + + TransparentUpgradeableProxy public lensProxy = + TransparentUpgradeableProxy(payable(0x930f1b46e1D081Ec1524efD95752bE3eCe51EF67)); + TransparentUpgradeableProxy public morphoProxy = + TransparentUpgradeableProxy(payable(0x8888882f8f843896699869179fB6E4f7e3B58888)); + TransparentUpgradeableProxy public rewardsManagerProxy; + + Lens public lensImplV1; + Morpho public morphoImplV1; + RewardsManager public rewardsManagerImplV1; + + Lens public lens; + Morpho public morpho; + RewardsManager public rewardsManager; + IIncentivesVault public incentivesVault; + IPositionsManager public positionsManager; + IInterestRatesManager public interestRatesManager; } diff --git a/contracts/compound/interfaces/IIncentivesVault.sol b/contracts/compound/interfaces/IIncentivesVault.sol index 284f3f002..faa7ced89 100644 --- a/contracts/compound/interfaces/IIncentivesVault.sol +++ b/contracts/compound/interfaces/IIncentivesVault.sol @@ -4,6 +4,16 @@ pragma solidity ^0.8.0; import "./IOracle.sol"; interface IIncentivesVault { + function isPaused() external view returns (bool); + + function bonus() external view returns (uint256); + + function MAX_BASIS_POINTS() external view returns (uint256); + + function incentivesTreasuryVault() external view returns (address); + + function oracle() external view returns (IOracle); + function setOracle(IOracle _newOracle) external; function setIncentivesTreasuryVault(address _newIncentivesTreasuryVault) external; diff --git a/lib/forge-std b/lib/forge-std index 0d0485bde..8d93b5273 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 0d0485bdea9f9455bd684acb0ba88548a104d99b +Subproject commit 8d93b5273ca94b1c50b055ffc0e1b8b0a3c03d78 diff --git a/test-foundry/compound/TestGovernance.t.sol b/test-foundry/compound/TestGovernance.t.sol index 827390188..d67393b2a 100644 --- a/test-foundry/compound/TestGovernance.t.sol +++ b/test-foundry/compound/TestGovernance.t.sol @@ -172,7 +172,7 @@ contract TestGovernance is TestSetup { function testOnlyOwnerShouldSetIncentivesVault() public { IIncentivesVault incentivesVaultV2 = new IncentivesVault( - IComptroller(comptrollerAddress), + comptroller, IMorpho(address(morpho)), morphoToken, address(1), diff --git a/test-foundry/compound/TestIncentivesVault.t.sol b/test-foundry/compound/TestIncentivesVault.t.sol index 980c863d0..52871578c 100644 --- a/test-foundry/compound/TestIncentivesVault.t.sol +++ b/test-foundry/compound/TestIncentivesVault.t.sol @@ -1,51 +1,11 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity 0.8.13; -import "@contracts/compound/interfaces/compound/ICompound.sol"; -import "@contracts/compound/interfaces/IOracle.sol"; -import "@contracts/compound/IncentivesVault.sol"; +import "./setup/TestSetup.sol"; -import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; - -import "../common/helpers/MorphoToken.sol"; -import "./helpers/DumbOracle.sol"; -import "@forge-std/Test.sol"; -import "@config/Config.sol"; - -contract TestIncentivesVault is Test, Config { +contract TestIncentivesVault is TestSetup { using SafeTransferLib for ERC20; - Vm public hevm = Vm(HEVM_ADDRESS); - address public constant COMP = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - address public incentivesTreasuryVault = address(1); - address public morpho = address(3); - IncentivesVault public incentivesVault; - MorphoToken public morphoToken; - DumbOracle public dumbOracle; - - function setUp() public { - morphoToken = new MorphoToken(address(this)); - dumbOracle = new DumbOracle(); - - incentivesVault = new IncentivesVault( - IComptroller(comptrollerAddress), - IMorpho(address(morpho)), - morphoToken, - incentivesTreasuryVault, - dumbOracle - ); - ERC20(morphoToken).transfer( - address(incentivesVault), - ERC20(morphoToken).balanceOf(address(this)) - ); - - hevm.label(address(morphoToken), "MORPHO"); - hevm.label(address(dumbOracle), "DumbOracle"); - hevm.label(address(incentivesVault), "IncentivesVault"); - hevm.label(COMP, "COMP"); - hevm.label(morpho, "morpho"); - } - function testShouldNotSetBonusAboveMaxBasisPoints() public { uint256 moreThanMaxBasisPoints = incentivesVault.MAX_BASIS_POINTS() + 1; hevm.expectRevert(abi.encodeWithSelector(IncentivesVault.ExceedsMaxBasisPoints.selector)); @@ -64,6 +24,8 @@ contract TestIncentivesVault is Test, Config { } function testOnlyOwnerShouldSetIncentivesTreasuryVault() public { + address incentivesTreasuryVault = address(1); + hevm.prank(address(0)); hevm.expectRevert("Ownable: caller is not the owner"); incentivesVault.setIncentivesTreasuryVault(incentivesTreasuryVault); @@ -101,43 +63,43 @@ contract TestIncentivesVault is Test, Config { incentivesVault.transferTokensToDao(address(morphoToken), 1); incentivesVault.transferTokensToDao(address(morphoToken), 1); - assertEq(ERC20(morphoToken).balanceOf(incentivesTreasuryVault), 1); + assertEq(ERC20(morphoToken).balanceOf(address(treasuryVault)), 1); } function testFailWhenContractNotActive() public { incentivesVault.setPauseStatus(true); - hevm.prank(morpho); + hevm.prank(address(morpho)); incentivesVault.tradeCompForMorphoTokens(address(1), 0); } function testOnlyMorphoShouldTriggerCompConvertFunction() public { incentivesVault.setIncentivesTreasuryVault(address(1)); uint256 amount = 100; - deal(COMP, address(morpho), amount); + deal(comp, address(morpho), amount); - hevm.prank(morpho); - ERC20(COMP).safeApprove(address(incentivesVault), amount); + hevm.prank(address(morpho)); + ERC20(comp).safeApprove(address(incentivesVault), amount); hevm.expectRevert(abi.encodeWithSignature("OnlyMorpho()")); incentivesVault.tradeCompForMorphoTokens(address(2), amount); - hevm.prank(morpho); + hevm.prank(address(morpho)); incentivesVault.tradeCompForMorphoTokens(address(2), amount); } function testShouldGiveTheRightAmountOfRewards() public { incentivesVault.setIncentivesTreasuryVault(address(1)); uint256 toApprove = 1_000 ether; - deal(COMP, address(morpho), toApprove); + deal(comp, address(morpho), toApprove); - hevm.prank(morpho); - ERC20(COMP).safeApprove(address(incentivesVault), toApprove); + hevm.prank(address(morpho)); + ERC20(comp).safeApprove(address(incentivesVault), toApprove); uint256 amount = 100; // O% bonus. uint256 balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(morpho); + hevm.prank(address(morpho)); incentivesVault.tradeCompForMorphoTokens(address(2), amount); uint256 balanceAfter = ERC20(morphoToken).balanceOf(address(2)); assertEq(balanceAfter - balanceBefore, 100); @@ -145,7 +107,7 @@ contract TestIncentivesVault is Test, Config { // 10% bonus. incentivesVault.setBonus(1_000); balanceBefore = ERC20(morphoToken).balanceOf(address(2)); - hevm.prank(morpho); + hevm.prank(address(morpho)); incentivesVault.tradeCompForMorphoTokens(address(2), amount); balanceAfter = ERC20(morphoToken).balanceOf(address(2)); assertEq(balanceAfter - balanceBefore, 110); diff --git a/test-foundry/compound/helpers/User.sol b/test-foundry/compound/helpers/User.sol index 193990502..f73f569e6 100644 --- a/test-foundry/compound/helpers/User.sol +++ b/test-foundry/compound/helpers/User.sol @@ -77,20 +77,33 @@ contract User { morpho.setReserveFactor(_poolToken, _reserveFactor); } - function supply(address _poolToken, uint256 _amount) external { - morpho.supply(_poolToken, address(this), _amount); + function supply( + address _poolToken, + address _onBehalf, + uint256 _amount + ) public { + morpho.supply(_poolToken, _onBehalf, _amount); } function supply( address _poolToken, + address _onBehalf, uint256 _amount, uint256 _maxGasForMatching - ) external { - morpho.supply(_poolToken, address(this), _amount, _maxGasForMatching); + ) public { + morpho.supply(_poolToken, _onBehalf, _amount, _maxGasForMatching); } - function withdraw(address _poolToken, uint256 _amount) external { - morpho.withdraw(_poolToken, _amount); + function supply(address _poolToken, uint256 _amount) external { + supply(_poolToken, address(this), _amount); + } + + function supply( + address _poolToken, + uint256 _amount, + uint256 _maxGasForMatching + ) external { + supply(_poolToken, address(this), _amount, _maxGasForMatching); } function borrow(address _poolToken, uint256 _amount) external { @@ -105,8 +118,20 @@ contract User { morpho.borrow(_poolToken, _amount, _maxGasForMatching); } + function withdraw(address _poolToken, uint256 _amount) external { + morpho.withdraw(_poolToken, _amount); + } + + function repay( + address _poolToken, + address _onBehalf, + uint256 _amount + ) public { + morpho.repay(_poolToken, _onBehalf, _amount); + } + function repay(address _poolToken, uint256 _amount) external { - morpho.repay(_poolToken, address(this), _amount); + repay(_poolToken, address(this), _amount); } function liquidate( diff --git a/test-foundry/compound/setup/TestSetup.sol b/test-foundry/compound/setup/TestSetup.sol index 88d60b8d5..1f28ecd96 100644 --- a/test-foundry/compound/setup/TestSetup.sol +++ b/test-foundry/compound/setup/TestSetup.sol @@ -1,20 +1,11 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity 0.8.13; -import "@contracts/compound/interfaces/compound/ICompound.sol"; -import "@contracts/compound/interfaces/IRewardsManager.sol"; +import "@contracts/compound/interfaces/IMorpho.sol"; -import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; - -import "@contracts/compound/IncentivesVault.sol"; -import "@contracts/compound/RewardsManager.sol"; -import "@contracts/compound/PositionsManager.sol"; -import "@contracts/compound/MatchingEngine.sol"; -import "@contracts/compound/InterestRatesManager.sol"; -import "@contracts/compound/Morpho.sol"; -import "@contracts/compound/lens/Lens.sol"; +import "@contracts/compound/libraries/CompoundMath.sol"; +import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; import "../../common/helpers/MorphoToken.sol"; import "../../common/helpers/Chains.sol"; @@ -32,25 +23,8 @@ contract TestSetup is Config, Utils { uint256 public constant MAX_BASIS_POINTS = 10_000; uint256 public constant INITIAL_BALANCE = 1_000_000; - ProxyAdmin public proxyAdmin; - TransparentUpgradeableProxy public lensProxy; - TransparentUpgradeableProxy public morphoProxy; - TransparentUpgradeableProxy public rewardsManagerProxy; - - Lens public lensImplV1; - Morpho public morphoImplV1; - IRewardsManager public rewardsManagerImplV1; - - Lens public lens; - Morpho public morpho; - IPositionsManager public positionsManager; - InterestRatesManager public interestRatesManager; - IncentivesVault public incentivesVault; - IRewardsManager public rewardsManager; - DumbOracle public dumbOracle; MorphoToken public morphoToken; - IComptroller public comptroller; ICompoundOracle public oracle; User public treasuryVault; @@ -78,7 +52,6 @@ contract TestSetup is Config, Utils { function onSetUp() public virtual {} function initContracts() internal { - comptroller = IComptroller(comptrollerAddress); interestRatesManager = new InterestRatesManager(); positionsManager = new PositionsManager(); @@ -123,10 +96,10 @@ contract TestSetup is Config, Utils { morphoToken = new MorphoToken(address(this)); dumbOracle = new DumbOracle(); incentivesVault = new IncentivesVault( - IComptroller(comptrollerAddress), + comptroller, IMorpho(address(morpho)), morphoToken, - address(1), + address(treasuryVault), dumbOracle ); morphoToken.transfer(address(incentivesVault), 1_000_000 ether); diff --git a/test-foundry/fuzzing/compound/setup/TestSetupFuzzing.sol b/test-foundry/fuzzing/compound/setup/TestSetupFuzzing.sol index c3fd5da1b..a107b1537 100644 --- a/test-foundry/fuzzing/compound/setup/TestSetupFuzzing.sol +++ b/test-foundry/fuzzing/compound/setup/TestSetupFuzzing.sol @@ -106,7 +106,6 @@ contract TestSetupFuzzing is Config, Utils, stdCheats { repay: 3e6 }); - comptroller = IComptroller(comptrollerAddress); interestRatesManager = new InterestRatesManager(); positionsManager = new PositionsManager(); diff --git a/test-foundry/prod/compound/TestBorrow.t.sol b/test-foundry/prod/compound/TestBorrow.t.sol new file mode 100644 index 000000000..5f3f84546 --- /dev/null +++ b/test-foundry/prod/compound/TestBorrow.t.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.13; + +import "./setup/TestSetup.sol"; + +contract TestBorrow is TestSetup { + using CompoundMath for uint256; + + struct BorrowTest { + ERC20 collateral; + ICToken collateralPoolToken; + uint256 collateralDecimals; + ERC20 borrowed; + ICToken borrowedPoolToken; + uint256 borrowedDecimals; + uint256 collateralFactor; + uint256 collateralPrice; + uint256 borrowedPrice; + uint256 borrowedAmount; + uint256 collateralAmount; + uint256 borrowedBalanceBefore; + uint256 borrowedBalanceAfter; + uint256 morphoBorrowedOnPoolBefore; + uint256 morphoBorrowedBalanceOnPoolBefore; + uint256 morphoUnderlyingBalanceBefore; + bool p2pDisabled; + uint256 p2pSupplyDelta; + uint256 p2pBorrowIndex; + uint256 poolSupplyIndex; + uint256 poolBorrowIndex; + uint256 borrowRatePerBlock; + uint256 p2pBorrowRatePerBlock; + uint256 poolBorrowRatePerBlock; + uint256 balanceInP2P; + uint256 balanceOnPool; + uint256 unclaimedRewardsBefore; + uint256 unclaimedRewardsAfter; + uint256 borrowedOnPoolBefore; + uint256 borrowedInP2PBefore; + uint256 totalBorrowedBefore; + uint256 borrowedOnPoolAfter; + uint256 borrowedInP2PAfter; + uint256 totalBorrowedAfter; + } + + function _setUpBorrowTest( + address _borrowedPoolToken, + address _collateralPoolToken, + uint96 _amount + ) internal returns (BorrowTest memory test) { + test.borrowedPoolToken = ICToken(_borrowedPoolToken); + test.collateralPoolToken = ICToken(_collateralPoolToken); + + (, test.collateralFactor, ) = morpho.comptroller().markets( + address(test.collateralPoolToken) + ); + + (test.collateral, test.collateralDecimals) = _getUnderlying(_collateralPoolToken); + (test.borrowed, test.borrowedDecimals) = _getUnderlying(_borrowedPoolToken); + + ICompoundOracle oracle = ICompoundOracle(morpho.comptroller().oracle()); + test.collateralPrice = oracle.getUnderlyingPrice(address(test.collateralPoolToken)); + test.borrowedPrice = oracle.getUnderlyingPrice(address(test.borrowedPoolToken)); + + (test.p2pSupplyDelta, , , ) = morpho.deltas(address(test.borrowedPoolToken)); + test.p2pDisabled = morpho.p2pDisabled(address(test.borrowedPoolToken)); + test.borrowedBalanceBefore = test.borrowed.balanceOf(address(borrower1)); + test.morphoBorrowedOnPoolBefore = test.borrowedPoolToken.borrowBalanceCurrent( + address(morpho) + ); + test.morphoBorrowedBalanceOnPoolBefore = test.borrowedPoolToken.balanceOfUnderlying( + address(morpho) + ); + test.morphoUnderlyingBalanceBefore = test.borrowed.balanceOf(address(morpho)); + + test.borrowedAmount = _boundBorrowedAmount( + _amount, + _borrowedPoolToken, + address(test.borrowed), + test.borrowedDecimals + ); + } + + function _testShouldBorrowMarketP2PAndFromPool( + address _borrowedPoolToken, + address _collateralPoolToken, + uint96 _amount + ) internal { + BorrowTest memory test = _setUpBorrowTest( + _borrowedPoolToken, + _collateralPoolToken, + _amount + ); + + test.collateralAmount = + _getMinimumCollateralAmount( + test.borrowedAmount, + test.borrowedPrice, + test.collateralPrice, + test.collateralFactor + ) + + 10**(test.collateralDecimals - 5); // Inflate collateral amount to compensate for compound rounding errors. + _tip(address(test.collateral), address(borrower1), test.collateralAmount); + + borrower1.approve(address(test.collateral), test.collateralAmount); + borrower1.supply(address(test.collateralPoolToken), test.collateralAmount); + borrower1.borrow(address(test.borrowedPoolToken), test.borrowedAmount); + + test.borrowedBalanceAfter = test.borrowed.balanceOf(address(borrower1)); + test.p2pBorrowIndex = morpho.p2pBorrowIndex(address(test.borrowedPoolToken)); + test.poolSupplyIndex = test.borrowedPoolToken.exchangeRateCurrent(); + test.poolBorrowIndex = test.borrowedPoolToken.borrowIndex(); + test.borrowRatePerBlock = lens.getCurrentUserBorrowRatePerBlock( + address(test.borrowedPoolToken), + address(borrower1) + ); + (, test.p2pBorrowRatePerBlock, , test.poolBorrowRatePerBlock) = lens.getRatesPerBlock( + address(test.borrowedPoolToken) + ); + + (test.balanceInP2P, test.balanceOnPool) = morpho.borrowBalanceInOf( + address(test.borrowedPoolToken), + address(borrower1) + ); + + address[] memory borrowedPoolTokens = new address[](1); + borrowedPoolTokens[0] = address(test.borrowedPoolToken); + test.unclaimedRewardsBefore = lens.getUserUnclaimedRewards( + borrowedPoolTokens, + address(borrower1) + ); + + test.borrowedInP2PBefore = test.balanceInP2P.mul(test.p2pBorrowIndex); + test.borrowedOnPoolBefore = test.balanceOnPool.mul(test.poolBorrowIndex); + test.totalBorrowedBefore = test.borrowedOnPoolBefore + test.borrowedInP2PBefore; + + assertEq( + test.collateral.balanceOf(address(borrower1)), + address(test.collateral) == address(test.borrowed) ? test.borrowedAmount : 0, + "unexpected collateral balance after" + ); + assertEq( + test.borrowedBalanceAfter, + test.borrowedBalanceBefore + test.borrowedAmount, + "unexpected borrowed balance change" + ); + assertLe( + test.borrowedOnPoolBefore + test.borrowedInP2PBefore, + test.borrowedAmount, + "greater borrowed amount than expected" + ); + assertGe( + test.borrowedOnPoolBefore + test.borrowedInP2PBefore + 10**(test.borrowedDecimals / 2), + test.borrowedAmount, + "unexpected borrowed amount" + ); + if (morpho.p2pDisabled(address(test.borrowedPoolToken))) + assertEq(test.balanceInP2P, 0, "unexpected p2p balance"); + assertEq(test.unclaimedRewardsBefore, 0, "unclaimed rewards not zero"); + + if (test.p2pSupplyDelta <= test.borrowedAmount.div(test.poolSupplyIndex)) + assertGe( + test.borrowedInP2PBefore, + test.p2pSupplyDelta.mul(test.poolSupplyIndex), + "expected p2p supply delta minimum match" + ); + else + assertApproxEqAbs( + test.borrowedInP2PBefore, + test.borrowedAmount, + 1, + "expected full match" + ); + + assertEq( + test.borrowed.balanceOf(address(morpho)), + test.morphoUnderlyingBalanceBefore, + "unexpected morpho underlying balance" + ); + assertApproxEqAbs( + test.borrowedPoolToken.borrowBalanceCurrent(address(morpho)), + test.morphoBorrowedOnPoolBefore + test.balanceOnPool.mul(test.poolBorrowIndex), + 10, + "unexpected morpho borrowed balance on pool" + ); + + vm.roll(block.number + 500); + + morpho.updateP2PIndexes(address(test.borrowedPoolToken)); + + vm.roll(block.number + 500); + + test.unclaimedRewardsAfter = lens.getUserUnclaimedRewards( + borrowedPoolTokens, + address(borrower1) + ); + (test.borrowedOnPoolAfter, test.borrowedInP2PAfter, test.totalBorrowedAfter) = lens + .getCurrentBorrowBalanceInOf(address(test.borrowedPoolToken), address(borrower1)); + + uint256 expectedBorrowedOnPoolAfter = test.borrowedOnPoolBefore.mul( + 1e18 + test.poolBorrowRatePerBlock * 1_000 + ); + uint256 expectedBorrowedInP2PAfter = test.borrowedInP2PBefore.mul( + 1e18 + test.p2pBorrowRatePerBlock * 1_000 + ); + uint256 expectedTotalBorrowedAfter = test.totalBorrowedBefore.mul( + 1e18 + test.borrowRatePerBlock * 1_000 + ); + + assertApproxEqAbs( + test.borrowedOnPoolAfter, + expectedBorrowedOnPoolAfter, + test.borrowedOnPoolAfter / 1e6 + 1, + "unexpected pool borrowed amount" + ); + assertApproxEqAbs( + test.borrowedInP2PAfter, + expectedBorrowedInP2PAfter, + test.borrowedInP2PAfter / 1e6 + 1, + "unexpected p2p borrowed amount" + ); + assertApproxEqAbs( + test.totalBorrowedAfter, + expectedTotalBorrowedAfter, + test.totalBorrowedAfter / 1e6 + 1, + "unexpected total borrowed amount from avg borrow rate" + ); + assertApproxEqAbs( + test.totalBorrowedAfter, + expectedBorrowedOnPoolAfter + expectedBorrowedInP2PAfter, + test.totalBorrowedAfter / 1e6 + 1, + "unexpected total borrowed amount" + ); + if ( + test.borrowedOnPoolAfter > 0 && + morpho.comptroller().compBorrowSpeeds(address(test.borrowedPoolToken)) > 0 + ) + assertGt( + test.unclaimedRewardsAfter, + test.unclaimedRewardsBefore, + "lower unclaimed rewards" + ); + } + + function testShouldBorrowAmountP2PAndFromPool( + uint8 _borrowMarketIndex, + uint8 _collateralMarketIndex, + uint96 _amount + ) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + address[] memory activeCollateralMarkets = getAllFullyActiveCollateralMarkets(); + + _borrowMarketIndex = uint8(_borrowMarketIndex % activeMarkets.length); + _collateralMarketIndex = uint8(_collateralMarketIndex % activeCollateralMarkets.length); + + _testShouldBorrowMarketP2PAndFromPool( + activeMarkets[_borrowMarketIndex], + activeCollateralMarkets[_collateralMarketIndex], + _amount + ); + } + + function testShouldNotBorrowZeroAmount() public { + address[] memory markets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < markets.length; ++marketIndex) { + BorrowTest memory test; + test.borrowedPoolToken = ICToken(markets[marketIndex]); + + vm.expectRevert(PositionsManager.AmountIsZero.selector); + borrower1.borrow(address(test.borrowedPoolToken), 0); + } + } + + function testShouldNotBorrowWithoutEnoughCollateral( + uint8 _borrowMarketIndex, + uint8 _collateralMarketIndex, + uint96 _amount + ) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + address[] memory activeCollateralMarkets = getAllFullyActiveMarkets(); + + _borrowMarketIndex = uint8(_borrowMarketIndex % activeMarkets.length); + _collateralMarketIndex = uint8(_collateralMarketIndex % activeCollateralMarkets.length); + + BorrowTest memory test = _setUpBorrowTest( + activeMarkets[_borrowMarketIndex], + activeCollateralMarkets[_collateralMarketIndex], + _amount + ); + + if (test.collateralFactor > 0) { + test.collateralAmount = _getMinimumCollateralAmount( + test.borrowedAmount, + test.borrowedPrice, + test.collateralPrice, + test.collateralFactor + ); // Not enough collateral because of compound rounding errors. + _tip(address(test.collateral), address(borrower1), test.collateralAmount); + + if (test.collateralAmount > 0) { + borrower1.approve(address(test.collateral), test.collateralAmount); + borrower1.supply(address(test.collateralPoolToken), test.collateralAmount); + } + } + + vm.expectRevert(PositionsManager.UnauthorisedBorrow.selector); + borrower1.borrow(address(test.borrowedPoolToken), test.borrowedAmount); + } +} diff --git a/test-foundry/prod/compound/TestRepay.t.sol b/test-foundry/prod/compound/TestRepay.t.sol new file mode 100644 index 000000000..fb64436e1 --- /dev/null +++ b/test-foundry/prod/compound/TestRepay.t.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.13; + +import "./setup/TestSetup.sol"; + +contract TestRepay is TestSetup { + using CompoundMath for uint256; + + struct RepayTest { + ERC20 collateral; + ICToken collateralPoolToken; + uint256 collateralDecimals; + ERC20 borrowed; + ICToken borrowedPoolToken; + uint256 borrowedDecimals; + uint256 borrowCap; + uint256 collateralFactor; + uint256 collateralPrice; + uint256 borrowedPrice; + uint256 borrowedAmount; + uint256 collateralAmount; + uint256 borrowedBalanceBefore; + uint256 borrowedBalanceAfter; + uint256 morphoBalanceOnPoolBefore; + uint256 morphoUnderlyingBalanceBefore; + uint256 p2pBorrowIndex; + uint256 poolBorrowIndex; + uint256 borrowRatePerBlock; + uint256 p2pBorrowRatePerBlock; + uint256 poolBorrowRatePerBlock; + uint256 balanceInP2P; + uint256 balanceOnPool; + uint256 unclaimedRewardsBefore; + uint256 unclaimedRewardsAfter; + uint256 borrowedOnPoolBefore; + uint256 borrowedInP2PBefore; + uint256 totalBorrowedBefore; + uint256 borrowedOnPoolAfter; + uint256 borrowedInP2PAfter; + uint256 totalBorrowedAfter; + } + + function _setUpRepayTest( + address _borrowedPoolToken, + address _collateralPoolToken, + uint96 _amount + ) internal returns (RepayTest memory test) { + test.borrowedPoolToken = ICToken(_borrowedPoolToken); + test.collateralPoolToken = ICToken(_collateralPoolToken); + + (, test.collateralFactor, ) = morpho.comptroller().markets( + address(test.collateralPoolToken) + ); + test.borrowCap = morpho.comptroller().borrowCaps(address(test.borrowedPoolToken)); + + (test.collateral, test.collateralDecimals) = _getUnderlying(_collateralPoolToken); + (test.borrowed, test.borrowedDecimals) = _getUnderlying(_borrowedPoolToken); + + ICompoundOracle oracle = ICompoundOracle(morpho.comptroller().oracle()); + test.collateralPrice = oracle.getUnderlyingPrice(address(test.collateralPoolToken)); + test.borrowedPrice = oracle.getUnderlyingPrice(address(test.borrowedPoolToken)); + + test.borrowedBalanceBefore = test.borrowed.balanceOf(address(borrower1)); + test.morphoBalanceOnPoolBefore = test.borrowedPoolToken.balanceOf(address(morpho)); + test.morphoUnderlyingBalanceBefore = test.borrowed.balanceOf(address(morpho)); + + test.borrowedAmount = _boundBorrowedAmount( + _amount, + _borrowedPoolToken, + address(test.borrowed), + test.borrowedDecimals + ); + } + + function _testShouldRepayMarketP2PAndFromPool( + address _borrowedPoolToken, + address _collateralPoolToken, + uint96 _amount + ) internal { + RepayTest memory test = _setUpRepayTest(_borrowedPoolToken, _collateralPoolToken, _amount); + + test.collateralAmount = + _getMinimumCollateralAmount( + test.borrowedAmount, + test.borrowedPrice, + test.collateralPrice, + test.collateralFactor + ) + + 10**(test.collateralDecimals - 5); // Inflate collateral amount to compensate for compound rounding errors. + _tip(address(test.collateral), address(borrower1), test.collateralAmount); + + borrower1.approve(address(test.collateral), test.collateralAmount); + borrower1.supply(address(test.collateralPoolToken), test.collateralAmount); + borrower1.borrow(address(test.borrowedPoolToken), test.borrowedAmount); + + test.borrowedBalanceAfter = test.borrowed.balanceOf(address(borrower1)); + test.p2pBorrowIndex = morpho.p2pBorrowIndex(address(test.borrowedPoolToken)); + test.poolBorrowIndex = test.borrowedPoolToken.borrowIndex(); + test.borrowRatePerBlock = lens.getCurrentUserBorrowRatePerBlock( + address(test.borrowedPoolToken), + address(borrower1) + ); + (, test.p2pBorrowRatePerBlock, , test.poolBorrowRatePerBlock) = lens.getRatesPerBlock( + address(test.borrowedPoolToken) + ); + + (test.balanceInP2P, test.balanceOnPool) = morpho.borrowBalanceInOf( + address(test.borrowedPoolToken), + address(borrower1) + ); + + address[] memory borrowedPoolTokens = new address[](1); + borrowedPoolTokens[0] = address(test.borrowedPoolToken); + test.unclaimedRewardsBefore = lens.getUserUnclaimedRewards( + borrowedPoolTokens, + address(borrower1) + ); + + test.borrowedInP2PBefore = test.balanceInP2P.mul(test.p2pBorrowIndex); + test.borrowedOnPoolBefore = test.balanceOnPool.mul(test.poolBorrowIndex); + test.totalBorrowedBefore = test.borrowedOnPoolBefore + test.borrowedInP2PBefore; + + vm.roll(block.number + 5_000); + + morpho.updateP2PIndexes(address(test.borrowedPoolToken)); + + vm.roll(block.number + 5_000); + + assertEq( + test.borrowed.balanceOf(address(borrower1)), + test.borrowedAmount, + "unexpected borrowed balance before repay" + ); + + (test.borrowedOnPoolAfter, test.borrowedInP2PAfter, test.totalBorrowedAfter) = lens + .getCurrentBorrowBalanceInOf(address(test.borrowedPoolToken), address(borrower1)); + + assertGe( + test.totalBorrowedAfter, + test.totalBorrowedBefore, + "unexpected borrowed amount before repay" + ); + + _tip( + address(test.borrowed), + address(borrower1), + test.totalBorrowedAfter - test.totalBorrowedBefore + ); + borrower1.approve(address(test.borrowed), type(uint256).max); + borrower1.repay(address(test.borrowedPoolToken), type(uint256).max); + + assertApproxEqAbs( + test.borrowed.balanceOf(address(borrower1)), + 0, + 10**(test.borrowedDecimals / 2), + "unexpected borrowed balance after repay" + ); + + (test.borrowedOnPoolAfter, test.borrowedInP2PAfter, test.totalBorrowedAfter) = lens + .getCurrentBorrowBalanceInOf(address(test.borrowedPoolToken), address(borrower1)); + + assertEq(test.borrowedOnPoolAfter, 0, "unexpected pool borrowed amount after repay"); + assertEq(test.borrowedInP2PAfter, 0, "unexpected p2p borrowed amount after repay"); + assertEq(test.totalBorrowedAfter, 0, "unexpected total borrowed after repay"); + } + + function testShouldRepayAmountP2PAndFromPool( + uint8 _borrowMarketIndex, + uint8 _collateralMarketIndex, + uint96 _amount + ) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + address[] memory activeCollateralMarkets = getAllFullyActiveCollateralMarkets(); + + _borrowMarketIndex = uint8(_borrowMarketIndex % activeMarkets.length); + _collateralMarketIndex = uint8(_collateralMarketIndex % activeCollateralMarkets.length); + + _testShouldRepayMarketP2PAndFromPool( + activeMarkets[_borrowMarketIndex], + activeCollateralMarkets[_collateralMarketIndex], + _amount + ); + } + + function testShouldNotRepayZeroAmount() public { + address[] memory markets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < markets.length; ++marketIndex) { + RepayTest memory test; + test.borrowedPoolToken = ICToken(markets[marketIndex]); + + vm.expectRevert(PositionsManager.AmountIsZero.selector); + borrower1.repay(address(test.borrowedPoolToken), 0); + } + } +} diff --git a/test-foundry/prod/compound/TestSupply.t.sol b/test-foundry/prod/compound/TestSupply.t.sol new file mode 100644 index 000000000..53523e25c --- /dev/null +++ b/test-foundry/prod/compound/TestSupply.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.13; + +import "./setup/TestSetup.sol"; + +contract TestSupply is TestSetup { + using CompoundMath for uint256; + + struct SupplyTest { + ERC20 underlying; + ICToken poolToken; + uint256 decimals; + uint256 morphoBalanceOnPoolBefore; + uint256 morphoBorrowOnPoolBefore; + uint256 morphoUnderlyingBalanceBefore; + uint256 p2pSupplyIndex; + uint256 poolSupplyIndex; + uint256 poolBorrowIndex; + bool p2pDisabled; + uint256 p2pBorrowDelta; + uint256 supplyRatePerBlock; + uint256 p2pSupplyRatePerBlock; + uint256 poolSupplyRatePerBlock; + uint256 balanceInP2P; + uint256 balanceOnPool; + uint256 unclaimedRewardsBefore; + uint256 unclaimedRewardsAfter; + uint256 underlyingOnPoolBefore; + uint256 underlyingInP2PBefore; + uint256 totalUnderlyingBefore; + uint256 underlyingOnPoolAfter; + uint256 underlyingInP2PAfter; + uint256 totalUnderlyingAfter; + } + + function _testShouldSupplyMarketP2PAndOnPool(address _poolToken, uint96 _amount) internal { + SupplyTest memory test; + test.poolToken = ICToken(_poolToken); + (test.underlying, test.decimals) = _getUnderlying(_poolToken); + + (, test.p2pBorrowDelta, , ) = morpho.deltas(address(test.poolToken)); + test.p2pDisabled = morpho.p2pDisabled(address(test.poolToken)); + test.morphoBalanceOnPoolBefore = test.poolToken.balanceOf(address(morpho)); + test.morphoBorrowOnPoolBefore = test.poolToken.borrowBalanceCurrent(address(morpho)); + test.morphoUnderlyingBalanceBefore = test.underlying.balanceOf(address(morpho)); + + uint256 amount = bound(_amount, 10**(test.decimals - 6), type(uint96).max); + + _tip(address(test.underlying), address(supplier1), amount); + + supplier1.approve(address(test.underlying), amount); + supplier1.supply(address(test.poolToken), amount); + + test.p2pSupplyIndex = morpho.p2pSupplyIndex(address(test.poolToken)); + test.poolSupplyIndex = test.poolToken.exchangeRateCurrent(); + test.poolBorrowIndex = test.poolToken.borrowIndex(); + test.supplyRatePerBlock = lens.getCurrentUserSupplyRatePerBlock( + address(test.poolToken), + address(supplier1) + ); + (test.p2pSupplyRatePerBlock, , test.poolSupplyRatePerBlock, ) = lens.getRatesPerBlock( + address(test.poolToken) + ); + + (test.balanceInP2P, test.balanceOnPool) = morpho.supplyBalanceInOf( + address(test.poolToken), + address(supplier1) + ); + + address[] memory poolTokens = new address[](1); + poolTokens[0] = address(test.poolToken); + test.unclaimedRewardsBefore = lens.getUserUnclaimedRewards(poolTokens, address(supplier1)); + + test.underlyingInP2PBefore = test.balanceInP2P.mul(test.p2pSupplyIndex); + test.underlyingOnPoolBefore = test.balanceOnPool.mul(test.poolSupplyIndex); + test.totalUnderlyingBefore = test.underlyingOnPoolBefore + test.underlyingInP2PBefore; + + assertEq( + test.underlying.balanceOf(address(supplier1)), + 0, + "unexpected underlying balance after" + ); + assertLe( + test.underlyingOnPoolBefore + test.underlyingInP2PBefore, + amount, + "greater supplied amount than expected" + ); + assertGe( + test.underlyingOnPoolBefore + test.underlyingInP2PBefore + 10**(test.decimals / 2), + amount, + "unexpected supplied amount" + ); + if (test.p2pDisabled) assertEq(test.balanceInP2P, 0, "expected no match"); + assertEq(test.unclaimedRewardsBefore, 0, "unclaimed rewards not zero"); + + assertApproxEqAbs( + test.poolToken.borrowBalanceCurrent(address(morpho)) + test.underlyingInP2PBefore, + test.morphoBorrowOnPoolBefore, + 10**(test.decimals / 2), + "unexpected morpho borrow balance" + ); + + if (test.p2pBorrowDelta <= amount.div(test.poolBorrowIndex)) + assertGe( + test.underlyingInP2PBefore, + test.p2pBorrowDelta.mul(test.poolBorrowIndex), + "expected p2p borrow delta minimum match" + ); + else + assertApproxEqAbs( + test.underlyingInP2PBefore, + amount, + 10**(test.decimals / 2), + "expected full match" + ); + + assertEq( + test.underlying.balanceOf(address(morpho)), + test.morphoUnderlyingBalanceBefore, + "unexpected morpho underlying balance" + ); + assertEq( + test.poolToken.balanceOf(address(morpho)), + test.morphoBalanceOnPoolBefore + test.balanceOnPool, + "unexpected morpho underlying balance on pool" + ); + + vm.roll(block.number + 500); + + morpho.updateP2PIndexes(address(test.poolToken)); + + vm.roll(block.number + 500); + + test.unclaimedRewardsAfter = lens.getUserUnclaimedRewards(poolTokens, address(supplier1)); + (test.underlyingOnPoolAfter, test.underlyingInP2PAfter, test.totalUnderlyingAfter) = lens + .getCurrentSupplyBalanceInOf(address(test.poolToken), address(supplier1)); + + uint256 expectedUnderlyingOnPoolAfter = test.underlyingOnPoolBefore.mul( + 1e18 + test.poolSupplyRatePerBlock * 1_000 + ); + uint256 expectedUnderlyingInP2PAfter = test.underlyingInP2PBefore.mul( + 1e18 + test.p2pSupplyRatePerBlock * 1_000 + ); + uint256 expectedTotalUnderlyingAfter = test.totalUnderlyingBefore.mul( + 1e18 + test.supplyRatePerBlock * 1_000 + ); + + assertApproxEqAbs( + test.underlyingOnPoolAfter, + expectedUnderlyingOnPoolAfter, + test.underlyingOnPoolAfter / 1e9 + 1e4, + "unexpected pool underlying amount" + ); + assertApproxEqAbs( + test.underlyingInP2PAfter, + expectedUnderlyingInP2PAfter, + test.underlyingInP2PAfter / 1e9 + 1e4, + "unexpected p2p underlying amount" + ); + assertApproxEqAbs( + test.totalUnderlyingAfter, + expectedTotalUnderlyingAfter, + test.totalUnderlyingAfter / 1e9 + 1e4, + "unexpected total underlying amount from avg supply rate" + ); + assertApproxEqAbs( + test.totalUnderlyingAfter, + expectedUnderlyingOnPoolAfter + expectedUnderlyingInP2PAfter, + test.totalUnderlyingBefore / 1e9 + 1e4, + "unexpected total underlying amount" + ); + if ( + test.underlyingOnPoolAfter > 0 && + morpho.comptroller().compSupplySpeeds(address(test.poolToken)) > 0 + ) + assertGt( + test.unclaimedRewardsAfter, + test.unclaimedRewardsBefore, + "lower unclaimed rewards" + ); + } + + function testShouldSupplyAllMarketsP2PAndOnPool(uint8 _marketIndex, uint96 _amount) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + _marketIndex = uint8(_marketIndex % activeMarkets.length); + + _testShouldSupplyMarketP2PAndOnPool(activeMarkets[_marketIndex], _amount); + } + + function testShouldNotSupplyZeroAmount() public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < activeMarkets.length; ++marketIndex) { + SupplyTest memory test; + test.poolToken = ICToken(activeMarkets[marketIndex]); + + vm.expectRevert(PositionsManager.AmountIsZero.selector); + supplier1.supply(address(test.poolToken), 0); + } + } + + function testShouldNotSupplyOnBehalfAddressZero(uint96 _amount) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < activeMarkets.length; ++marketIndex) { + SupplyTest memory test; + test.poolToken = ICToken(activeMarkets[marketIndex]); + (test.underlying, test.decimals) = _getUnderlying(activeMarkets[marketIndex]); + + uint256 amount = bound(_amount, 10**(test.decimals - 6), type(uint96).max); + + vm.expectRevert(PositionsManager.AddressIsZero.selector); + supplier1.supply(address(test.poolToken), address(0), amount); + } + } +} diff --git a/test-foundry/prod/compound/TestWithdraw.t.sol b/test-foundry/prod/compound/TestWithdraw.t.sol new file mode 100644 index 000000000..d1aab59a4 --- /dev/null +++ b/test-foundry/prod/compound/TestWithdraw.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.13; + +import "./setup/TestSetup.sol"; + +contract TestWithdraw is TestSetup { + using CompoundMath for uint256; + + struct WithdrawTest { + ERC20 underlying; + ICToken poolToken; + uint256 decimals; + uint256 morphoBalanceOnPoolBefore; + uint256 morphoUnderlyingBalanceBefore; + uint256 p2pSupplyIndex; + uint256 poolSupplyIndex; + uint256 balanceInP2P; + uint256 balanceOnPool; + uint256 underlyingOnPoolBefore; + uint256 underlyingInP2PBefore; + uint256 totalUnderlyingBefore; + uint256 underlyingOnPoolAfter; + uint256 underlyingInP2PAfter; + uint256 totalUnderlyingAfter; + } + + function _testShouldWithdrawMarketP2PAndOnPool(address _poolToken, uint96 _amount) internal { + WithdrawTest memory test; + test.poolToken = ICToken(_poolToken); + (test.underlying, test.decimals) = _getUnderlying(_poolToken); + + test.morphoBalanceOnPoolBefore = test.poolToken.balanceOf(address(morpho)); + test.morphoUnderlyingBalanceBefore = test.underlying.balanceOf(address(morpho)); + + uint256 amount = bound(_amount, 10**(test.decimals - 4), type(uint96).max); + + _tip(address(test.underlying), address(supplier1), amount); + + supplier1.approve(address(test.underlying), amount); + supplier1.supply(address(test.poolToken), amount); + + test.p2pSupplyIndex = morpho.p2pSupplyIndex(address(test.poolToken)); + test.poolSupplyIndex = test.poolToken.exchangeRateCurrent(); + + (test.balanceInP2P, test.balanceOnPool) = morpho.supplyBalanceInOf( + address(test.poolToken), + address(supplier1) + ); + + test.underlyingInP2PBefore = test.balanceInP2P.mul(test.p2pSupplyIndex); + test.underlyingOnPoolBefore = test.balanceOnPool.mul(test.poolSupplyIndex); + test.totalUnderlyingBefore = test.underlyingOnPoolBefore + test.underlyingInP2PBefore; + + vm.roll(block.number + 10_000); + + morpho.updateP2PIndexes(address(test.poolToken)); + + vm.roll(block.number + 10_000); + + assertEq( + test.underlying.balanceOf(address(supplier1)), + 0, + "unexpected underlying balance before withdraw" + ); + + supplier1.withdraw(address(test.poolToken), test.totalUnderlyingBefore); + + (test.underlyingOnPoolAfter, test.underlyingInP2PAfter, test.totalUnderlyingAfter) = lens + .getCurrentSupplyBalanceInOf(address(test.poolToken), address(supplier1)); + + assertEq( + test.underlying.balanceOf(address(supplier1)), + test.totalUnderlyingBefore, + "unexpected underlying balance after withdraw" + ); + + if (test.totalUnderlyingAfter > 0) { + supplier1.withdraw(address(test.poolToken), test.totalUnderlyingBefore); // Withdraw accrued interests. + + ( + test.underlyingOnPoolAfter, + test.underlyingInP2PAfter, + test.totalUnderlyingAfter + ) = lens.getCurrentSupplyBalanceInOf(address(test.poolToken), address(supplier1)); + } + + assertEq(test.underlyingOnPoolAfter, 0, "unexpected pool underlying balance"); + assertEq(test.underlyingInP2PAfter, 0, "unexpected p2p underlying balance"); + assertEq(test.totalUnderlyingAfter, 0, "unexpected total underlying supplied"); + } + + function testShouldWithdrawAllMarketsP2PAndOnPool(uint8 _marketIndex, uint96 _amount) public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + _marketIndex = uint8(_marketIndex % activeMarkets.length); + + _testShouldWithdrawMarketP2PAndOnPool(activeMarkets[_marketIndex], _amount); + } + + function testShouldNotWithdrawZeroAmount() public { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < activeMarkets.length; ++marketIndex) { + WithdrawTest memory test; + test.poolToken = ICToken(activeMarkets[marketIndex]); + + vm.expectRevert(PositionsManager.AmountIsZero.selector); + supplier1.withdraw(address(test.poolToken), 0); + } + } + + function testShouldNotWithdrawFromUnenteredMarket(uint96 _amount) public { + vm.assume(_amount > 0); + + address[] memory activeMarkets = getAllFullyActiveMarkets(); + + for (uint256 marketIndex; marketIndex < activeMarkets.length; ++marketIndex) { + WithdrawTest memory test; + test.poolToken = ICToken(activeMarkets[marketIndex]); + + vm.expectRevert(PositionsManager.UserNotMemberOfMarket.selector); + supplier1.withdraw(address(test.poolToken), _amount); + } + } +} diff --git a/test-foundry/prod/compound/setup/TestSetup.sol b/test-foundry/prod/compound/setup/TestSetup.sol new file mode 100644 index 000000000..8fee8459d --- /dev/null +++ b/test-foundry/prod/compound/setup/TestSetup.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GNU AGPLv3 +pragma solidity 0.8.13; + +import "@contracts/compound/interfaces/IRewardsManager.sol"; +import "@contracts/compound/interfaces/IMorpho.sol"; + +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@contracts/compound/libraries/CompoundMath.sol"; +import "@rari-capital/solmate/src/utils/SafeTransferLib.sol"; +import "@morpho-dao/morpho-utils/math/Math.sol"; + +import {User} from "../../../compound/helpers/User.sol"; +import "@config/Config.sol"; +import "@forge-std/console.sol"; +import "@forge-std/Test.sol"; +import "@forge-std/Vm.sol"; + +contract TestSetup is Config, Test { + using CompoundMath for uint256; + using SafeTransferLib for ERC20; + + // MorphoToken public morphoToken; + + User public supplier1; + User public supplier2; + User public supplier3; + User[] public suppliers; + + User public borrower1; + User public borrower2; + User public borrower3; + User[] public borrowers; + + function setUp() public { + initContracts(); + setContractsLabels(); + initUsers(); + + onSetUp(); + } + + function onSetUp() public virtual {} + + function initContracts() internal { + // vm.prank(address(proxyAdmin)); + // lensImplV1 = Lens(lensProxy.implementation()); + // morphoImplV1 = Morpho(payable(morphoProxy.implementation())); + // rewardsManagerImplV1 = RewardsManager(rewardsManagerProxy.implementation()); + + lens = Lens(address(lensProxy)); + morpho = Morpho(payable(morphoProxy)); + rewardsManager = RewardsManager(address(morpho.rewardsManager())); + incentivesVault = morpho.incentivesVault(); + positionsManager = morpho.positionsManager(); + interestRatesManager = morpho.interestRatesManager(); + + rewardsManagerProxy = TransparentUpgradeableProxy(payable(address(rewardsManager))); + + // morphoToken = new MorphoToken(address(this)); + // morphoToken.transfer(address(incentivesVault), 1_000_000 ether); + } + + function initUsers() internal { + for (uint256 i = 0; i < 3; i++) { + suppliers.push(new User(morpho)); + vm.label( + address(suppliers[i]), + string(abi.encodePacked("Supplier", Strings.toString(i + 1))) + ); + } + supplier1 = suppliers[0]; + supplier2 = suppliers[1]; + supplier3 = suppliers[2]; + + for (uint256 i = 0; i < 3; i++) { + borrowers.push(new User(morpho)); + vm.label( + address(borrowers[i]), + string(abi.encodePacked("Borrower", Strings.toString(i + 1))) + ); + } + + borrower1 = borrowers[0]; + borrower2 = borrowers[1]; + borrower3 = borrowers[2]; + + deal(aave, address(this), type(uint256).max); + deal(dai, address(this), type(uint256).max); + deal(usdc, address(this), type(uint256).max); + deal(usdt, address(this), type(uint256).max); + deal(wbtc, address(this), type(uint256).max); + deal(wEth, address(this), type(uint256).max); + deal(comp, address(this), type(uint256).max); + deal(bat, address(this), type(uint256).max); + deal(tusd, address(this), type(uint256).max); + deal(uni, address(this), type(uint256).max); + deal(zrx, address(this), type(uint256).max); + deal(link, address(this), type(uint256).max); + deal(mkr, address(this), type(uint256).max); + deal(fei, address(this), type(uint256).max); + deal(yfi, address(this), type(uint256).max); + deal(usdp, address(this), type(uint256).max); + deal(sushi, address(this), type(uint256).max); + } + + function setContractsLabels() internal { + vm.label(address(proxyAdmin), "ProxyAdmin"); + vm.label(address(morphoImplV1), "MorphoImplV1"); + vm.label(address(morpho), "Morpho"); + vm.label(address(interestRatesManager), "InterestRatesManager"); + vm.label(address(rewardsManager), "RewardsManager"); + // vm.label(address(morphoToken), "MorphoToken"); + vm.label(address(comptroller), "Comptroller"); + vm.label(comptroller.oracle(), "CompoundOracle"); + vm.label(address(incentivesVault), "IncentivesVault"); + vm.label(address(lens), "Lens"); + + vm.label(address(aave), "AAVE"); + vm.label(address(dai), "DAI"); + vm.label(address(usdc), "USDC"); + vm.label(address(usdt), "USDT"); + vm.label(address(wbtc), "WBTC"); + vm.label(address(wEth), "WETH"); + vm.label(address(comp), "COMP"); + vm.label(address(bat), "BAT"); + vm.label(address(tusd), "TUSD"); + vm.label(address(uni), "UNI"); + vm.label(address(zrx), "ZRX"); + vm.label(address(link), "LINK"); + vm.label(address(mkr), "MKR"); + vm.label(address(fei), "FEI"); + vm.label(address(yfi), "YFI"); + vm.label(address(usdp), "USDP"); + vm.label(address(sushi), "SUSHI"); + + vm.label(address(cAave), "cAAVE"); + vm.label(address(cDai), "cDAI"); + vm.label(address(cUsdc), "cUSDC"); + vm.label(address(cUsdt), "cUSDT"); + vm.label(address(cWbtc2), "cWBTC"); + vm.label(address(cEth), "cWETH"); + vm.label(address(cComp), "cCOMP"); + vm.label(address(cBat), "cBAT"); + vm.label(address(cTusd), "cTUSD"); + vm.label(address(cUni), "cUNI"); + vm.label(address(cZrx), "cZRX"); + vm.label(address(cLink), "cLINK"); + vm.label(address(cMkr), "cMKR"); + vm.label(address(cFei), "cFEI"); + vm.label(address(cYfi), "cYFI"); + vm.label(address(cUsdp), "cUSDP"); + vm.label(address(cSushi), "cSUSHI"); + } + + function getAllFullyActiveMarkets() public view returns (address[] memory activeMarkets) { + address[] memory createdMarkets = morpho.getAllMarkets(); + uint256 nbCreatedMarkets = createdMarkets.length; + + uint256 nbActiveMarkets; + activeMarkets = new address[](nbCreatedMarkets); + + for (uint256 i; i < nbCreatedMarkets; ) { + address poolToken = createdMarkets[i]; + + (, bool isPaused, bool isPartiallyPaused) = morpho.marketStatus(poolToken); + if (!isPaused && !isPartiallyPaused) { + activeMarkets[nbActiveMarkets] = poolToken; + ++nbActiveMarkets; + } else console.log("Skipping paused (or partially paused) market:", poolToken); + + unchecked { + ++i; + } + } + + // Resize the array for return + assembly { + mstore(activeMarkets, nbActiveMarkets) + } + } + + function getAllUnpausedMarkets() public view returns (address[] memory unpausedMarkets) { + address[] memory createdMarkets = morpho.getAllMarkets(); + uint256 nbCreatedMarkets = createdMarkets.length; + + uint256 nbActiveMarkets; + unpausedMarkets = new address[](nbCreatedMarkets); + + for (uint256 i; i < nbCreatedMarkets; ) { + address poolToken = createdMarkets[i]; + + (, bool isPaused, ) = morpho.marketStatus(poolToken); + if (!isPaused) { + unpausedMarkets[nbActiveMarkets] = poolToken; + ++nbActiveMarkets; + } else console.log("Skipping paused market:", poolToken); + + unchecked { + ++i; + } + } + + // Resize the array for return + assembly { + mstore(unpausedMarkets, nbActiveMarkets) + } + } + + function getAllFullyActiveCollateralMarkets() + public + view + returns (address[] memory activeCollateralMarkets) + { + address[] memory activeMarkets = getAllFullyActiveMarkets(); + uint256 nbActiveMarkets = activeMarkets.length; + + uint256 nbActiveCollateralMarkets; + activeCollateralMarkets = new address[](nbActiveMarkets); + + for (uint256 i; i < nbActiveMarkets; ) { + address poolToken = activeMarkets[i]; + + (, uint256 collateralFactor, ) = morpho.comptroller().markets(poolToken); + (, bool isPaused, bool isPartiallyPaused) = morpho.marketStatus(poolToken); + if (collateralFactor > 0 && !isPaused && !isPartiallyPaused) { + activeCollateralMarkets[nbActiveCollateralMarkets] = poolToken; + ++nbActiveCollateralMarkets; + } else console.log("Skipping paused (or partially paused) market:", poolToken); + + unchecked { + ++i; + } + } + + // Resize the array for return + assembly { + mstore(activeCollateralMarkets, nbActiveCollateralMarkets) + } + } + + function _boundBorrowedAmount( + uint96 _amount, + address _poolToken, + address _underlying, + uint256 _decimals + ) internal returns (uint256) { + uint256 borrowCap = morpho.comptroller().borrowCaps(_poolToken); + + return + bound( + _amount, + 10**(_decimals - 6), + Math.min( + (borrowCap > 0 ? borrowCap - 1 : type(uint256).max) - + ICToken(_poolToken).totalBorrows(), + _underlying == wEth + ? _poolToken.balance + : ERC20(_underlying).balanceOf(_poolToken) + ) + ); + } + + function _getUnderlying(address _poolToken) + internal + view + returns (ERC20 underlying, uint256 decimals) + { + underlying = ERC20(_poolToken == cEth ? wEth : ICToken(_poolToken).underlying()); + decimals = underlying.decimals(); + } + + function _getMinimumCollateralAmount( + uint256 _borrowedAmount, + uint256 _borrowedPrice, + uint256 _collateralPrice, + uint256 _collateralFactor + ) internal pure returns (uint256) { + return _borrowedAmount.mul(_borrowedPrice).div(_collateralFactor).div(_collateralPrice); + } + + /// @dev Allows to add ERC20 tokens to the current balance of a given user (instead of resetting it via `deal`). + /// @dev Also avoids to mess with snapshots of snapshotted ERC20 (e.g. AAVE). + function _tip( + address _underlying, + address _user, + uint256 _amount + ) internal { + if (_amount == 0) return; + + if (_underlying == wEth) deal(wEth, wEth.balance + _amount); // Refill wrapped Ether. + + ERC20(_underlying).safeTransfer(_user, _amount); + } +}