Skip to content

Commit

Permalink
Merge pull request #231 from morpho-labs/feat/invariant-tests
Browse files Browse the repository at this point in the history
Feat: invariant tests
  • Loading branch information
MathisGD authored Aug 22, 2023
2 parents e157795 + dd89809 commit 590678d
Show file tree
Hide file tree
Showing 9 changed files with 943 additions and 13 deletions.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ optimizer_runs = 4294967295
[fmt]
wrap_comments = true

# See more config options https://github.com/foundry-rs/foundry/tree/master/crates/config
# See more config options https://github.com/foundry-rs/foundry/tree/master/crates/config
8 changes: 4 additions & 4 deletions test/forge/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ contract BaseTest is Test {
MarketParams internal marketParams;
Id internal id;

function setUp() public {
function setUp() public virtual {
vm.label(OWNER, "Owner");
vm.label(SUPPLIER, "Supplier");
vm.label(BORROWER, "Borrower");
Expand Down Expand Up @@ -183,10 +183,10 @@ contract BaseTest is Test {
UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv)));
}

function _accrueInterest() internal {
function _accrueInterest(MarketParams memory market) internal {
collateralToken.setBalance(address(this), 1);
morpho.supplyCollateral(marketParams, 1, address(this), hex"");
morpho.withdrawCollateral(marketParams, 1, address(this), address(10));
morpho.supplyCollateral(market, 1, address(this), hex"");
morpho.withdrawCollateral(market, 1, address(this), address(10));
}

function neq(MarketParams memory a, MarketParams memory b) internal pure returns (bool) {
Expand Down
214 changes: 214 additions & 0 deletions test/forge/InvariantBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "test/forge/BaseTest.sol";

contract InvariantBaseTest is BaseTest {
using MathLib for uint256;
using MorphoLib for Morpho;
using SharesMathLib for uint256;

uint256 blockNumber;
uint256 timestamp;

bytes4[] internal selectors;

address[] internal addressArray;

function setUp() public virtual override {
super.setUp();

targetContract(address(this));
}

function _targetDefaultSenders() internal {
targetSender(_addrFromHashedString("Morpho address1"));
targetSender(_addrFromHashedString("Morpho address2"));
targetSender(_addrFromHashedString("Morpho address3"));
targetSender(_addrFromHashedString("Morpho address4"));
targetSender(_addrFromHashedString("Morpho address5"));
targetSender(_addrFromHashedString("Morpho address6"));
targetSender(_addrFromHashedString("Morpho address7"));
targetSender(_addrFromHashedString("Morpho address8"));
}

function _weightSelector(bytes4 selector, uint256 weight) internal {
for (uint256 i; i < weight; ++i) {
selectors.push(selector);
}
}

function _approveSendersTransfers(address[] memory senders) internal {
for (uint256 i; i < senders.length; ++i) {
vm.startPrank(senders[i]);
borrowableToken.approve(address(morpho), type(uint256).max);
collateralToken.approve(address(morpho), type(uint256).max);
vm.stopPrank();
}
}

function _supplyHighAmountOfCollateralForAllSenders(address[] memory senders, MarketParams memory marketParams)
internal
{
for (uint256 i; i < senders.length; ++i) {
collateralToken.setBalance(senders[i], 1e30);
vm.prank(senders[i]);
morpho.supplyCollateral(marketParams, 1e30, senders[i], hex"");
}
}

/// @dev Apparently permanently setting block number and timestamp with cheatcodes in this function doesn't work,
/// they get reset to the ones defined in the set up function after each function call.
/// The solution we choose is to store these in storage, and set them with roll and warp cheatcodes with the
/// setCorrectBlock function at the the begenning of each function.
/// The purpose of this function is to increment these variables to simulate a new block.
function newBlock(uint256 elapsed) public {
elapsed = bound(elapsed, 10, 1 days);

blockNumber += 1;
timestamp += elapsed;
}

modifier setCorrectBlock() {
vm.roll(blockNumber);
vm.warp(timestamp);
_;
}

function _randomSenderToWithdrawOnBehalf(address[] memory addresses, address seed, address sender)
internal
returns (address randomSenderToWithdrawOnBehalf)
{
for (uint256 i; i < addresses.length; ++i) {
if (morpho.supplyShares(id, addresses[i]) != 0) {
addressArray.push(addresses[i]);
}
}
if (addressArray.length == 0) return address(0);

randomSenderToWithdrawOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length];

vm.prank(randomSenderToWithdrawOnBehalf);
morpho.setAuthorization(sender, true);

delete addressArray;
}

function _randomSenderToBorrowOnBehalf(address[] memory addresses, address seed, address sender)
internal
returns (address randomSenderToBorrowOnBehalf)
{
for (uint256 i; i < addresses.length; ++i) {
if (morpho.collateral(id, addresses[i]) != 0 && isHealthy(id, addresses[i])) {
addressArray.push(addresses[i]);
}
}
if (addressArray.length == 0) return address(0);

randomSenderToBorrowOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length];

vm.prank(randomSenderToBorrowOnBehalf);
morpho.setAuthorization(sender, true);

delete addressArray;
}

function _randomSenderToRepayOnBehalf(address[] memory addresses, address seed)
internal
returns (address randomSenderToRepayOnBehalf)
{
for (uint256 i; i < addresses.length; ++i) {
if (morpho.borrowShares(id, addresses[i]) != 0) {
addressArray.push(addresses[i]);
}
}
if (addressArray.length == 0) return address(0);

randomSenderToRepayOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length];

delete addressArray;
}

function _randomSenderToWithdrawCollateralOnBehalf(address[] memory addresses, address seed, address sender)
internal
returns (address randomSenderToWithdrawCollateralOnBehalf)
{
for (uint256 i; i < addresses.length; ++i) {
if (morpho.collateral(id, addresses[i]) != 0 && isHealthy(id, addresses[i])) {
addressArray.push(addresses[i]);
}
}
if (addressArray.length == 0) return address(0);

randomSenderToWithdrawCollateralOnBehalf = addressArray[uint256(uint160(seed)) % addressArray.length];

vm.prank(randomSenderToWithdrawCollateralOnBehalf);
morpho.setAuthorization(sender, true);

delete addressArray;
}

function _randomSenderToLiquidate(address[] memory addresses, address seed)
internal
returns (address randomSenderToLiquidate)
{
for (uint256 i; i < addresses.length; ++i) {
if (morpho.borrowShares(id, addresses[i]) != 0 && !isHealthy(id, addresses[i])) {
addressArray.push(addresses[i]);
}
}
if (addressArray.length == 0) return address(0);

randomSenderToLiquidate = addressArray[uint256(uint160(seed)) % addressArray.length];

delete addressArray;
}

function sumUsersSupplyShares(address[] memory addresses) internal view returns (uint256 sum) {
for (uint256 i; i < addresses.length; ++i) {
sum += morpho.supplyShares(id, addresses[i]);
}
sum += morpho.supplyShares(id, morpho.feeRecipient());
}

function sumUsersBorrowShares(address[] memory addresses) internal view returns (uint256 sum) {
for (uint256 i; i < addresses.length; ++i) {
sum += morpho.borrowShares(id, addresses[i]);
}
}

function sumUsersSuppliedAmounts(address[] memory addresses) internal view returns (uint256 sum) {
for (uint256 i; i < addresses.length; ++i) {
sum += morpho.supplyShares(id, addresses[i]).toAssetsDown(
morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)
);
}
sum += morpho.supplyShares(id, morpho.feeRecipient()).toAssetsDown(
morpho.totalSupplyAssets(id), morpho.totalSupplyShares(id)
);
}

function sumUsersBorrowedAmounts(address[] memory addresses) internal view returns (uint256 sum) {
for (uint256 i; i < addresses.length; ++i) {
sum += morpho.borrowShares(id, addresses[i]).toAssetsUp(
morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id)
);
}
}

function isHealthy(Id id, address user) public view returns (bool) {
uint256 collateralPrice = IOracle(marketParams.oracle).price();

uint256 borrowed =
morpho.borrowShares(id, user).toAssetsUp(morpho.totalBorrowAssets(id), morpho.totalBorrowShares(id));
uint256 maxBorrow =
morpho.collateral(id, user).mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv);

return maxBorrow >= borrowed;
}

function _liquidationIncentiveFactor(uint256 lltv) internal pure returns (uint256) {
return
UtilsLib.min(MAX_LIQUIDATION_INCENTIVE_FACTOR, WAD.wDivDown(WAD - LIQUIDATION_CURSOR.wMulDown(WAD - lltv)));
}
}
4 changes: 2 additions & 2 deletions test/forge/integration/TestIntegrationAccrueInterest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ contract IntegrationAccrueInterestTest is BaseTest {
uint256 totalSupplyBeforeAccrued = morpho.totalSupplyAssets(id);
uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id);

_accrueInterest();
_accrueInterest(marketParams);

assertEq(morpho.totalBorrowAssets(id), totalBorrowBeforeAccrued, "total borrow");
assertEq(morpho.totalSupplyAssets(id), totalSupplyBeforeAccrued, "total supply");
Expand All @@ -59,7 +59,7 @@ contract IntegrationAccrueInterestTest is BaseTest {
uint256 totalSupplyBeforeAccrued = morpho.totalSupplyAssets(id);
uint256 totalSupplySharesBeforeAccrued = morpho.totalSupplyShares(id);

_accrueInterest();
_accrueInterest(marketParams);

assertEq(morpho.totalBorrowAssets(id), totalBorrowBeforeAccrued, "total borrow");
assertEq(morpho.totalSupplyAssets(id), totalSupplyBeforeAccrued, "total supply");
Expand Down
Loading

0 comments on commit 590678d

Please sign in to comment.