Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/tests invariants #6

Merged
merged 8 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ ffi = true
runs = 10000

[invariant]
runs = 1000
depth = 30
runs = 10
depth = 1000
fail_on_revert = true

[rpc_endpoints]
arbitrum = "${ETH_NODE_URI_ARBITRUM}"
Expand Down Expand Up @@ -57,7 +58,7 @@ runs = 2000
[profile.dev.invariant]
runs = 10
depth = 1
fail_on_revert = false
fail_on_revert = true

[profile.ci]
src = "test"
Expand All @@ -70,4 +71,4 @@ runs = 100
[profile.ci.invariant]
runs = 10
depth = 30
fail_on_revert = false
fail_on_revert = true
Empty file removed test/invariant/.gitkeep
Empty file.
180 changes: 180 additions & 0 deletions test/invariant/BasicInvariants.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.19;

import { UtilsLib } from "morpho/libraries/UtilsLib.sol";
import { UserActor } from "./actors/User.t.sol";
import { KeeperActor } from "./actors/Keeper.t.sol";
import { ParamActor } from "./actors/Param.t.sol";
import { MockRouter } from "../mock/MockRouter.sol";
import { VestingStore } from "./stores/VestingStore.sol";
import { StateVariableStore } from "./stores/StateVariableStore.sol";
import { IntegratorActor } from "./actors/Integrator.t.sol";
import { DeveloperActor } from "./actors/Developer.t.sol";
import "../ERC4626StrategyTest.t.sol";

contract BasicInvariants is ERC4626StrategyTest {
using UtilsLib for uint256;

uint256 internal constant _NUM_USER = 10;
uint256 internal constant _NUM_KEEPER = 2;
uint256 internal constant _NUM_PARAM = 5;
uint256 internal constant _NUM_INTEGRATOR = 2;
uint256 internal constant _NUM_DEVELOPER = 2;

UserActor internal _userHandler;
KeeperActor internal _keeperHandler;
ParamActor internal _paramHandler;
DeveloperActor internal _developerHandler;
IntegratorActor internal _integratorHandler;
VestingStore internal _vestingStore;
StateVariableStore internal _stateVariableStore;

// state variables
uint256 internal _previousDeveloperShares;
uint256 internal _previousIntegratorShares;

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

// Switch to mock router
MockRouter router = new MockRouter();
vm.startPrank(developer);
strategy.setTokenTransferAddress(address(router));
strategy.setSwapRouter(address(router));
vm.stopPrank();
deal(asset, address(router), 1e30);

// Deposit some assets
vm.startPrank(alice);
deal(asset, alice, 1e24);
IERC20(asset).approve(address(strategy), 1e24);
uint256 deposited = strategy.deposit(1e24, alice);
vm.stopPrank();

// Create stores
_vestingStore = new VestingStore();
_stateVariableStore = new StateVariableStore();

_stateVariableStore.addShares(1e24);
_stateVariableStore.addUnderlyingStrategyShares(ERC4626(strategyAsset).convertToShares(1e24));

// Create actors
_developerHandler = new DeveloperActor(_NUM_DEVELOPER, address(strategy));
_integratorHandler = new IntegratorActor(_NUM_INTEGRATOR, address(strategy));
_userHandler = new UserActor(_NUM_USER, address(strategy), _stateVariableStore);
_keeperHandler = new KeeperActor(_NUM_KEEPER, address(strategy), _stateVariableStore, _vestingStore);
_paramHandler = new ParamActor(_NUM_PARAM, address(strategy));

// Label newly created addresses
for (uint256 i; i < _NUM_USER; i++) {
vm.label(_userHandler.actors(i), string.concat("User ", vm.toString(i)));
deal(asset, _userHandler.actors(i), 1e30);
}
vm.startPrank(developer);
for (uint256 i; i < _NUM_KEEPER; i++) {
strategy.grantRole(strategy.KEEPER_ROLE(), _keeperHandler.actors(i));
vm.label(_keeperHandler.actors(i), string.concat("Keeper ", vm.toString(i)));
}
for (uint256 i; i < _NUM_DEVELOPER; i++) {
strategy.grantRole(strategy.DEVELOPER_ROLE(), _developerHandler.actors(i));
vm.label(_developerHandler.actors(i), string.concat("Developer ", vm.toString(i)));
}
vm.stopPrank();
for (uint256 i; i < _NUM_PARAM; i++) {
vm.label(_paramHandler.actors(i), string.concat("Param ", vm.toString(i)));
}
vm.startPrank(integrator);
for (uint256 i; i < _NUM_INTEGRATOR; i++) {
strategy.grantRole(strategy.INTEGRATOR_ROLE(), _integratorHandler.actors(i));
vm.label(_integratorHandler.actors(i), string.concat("Integrator ", vm.toString(i)));
}
vm.stopPrank();

targetContract(address(_userHandler));
targetContract(address(_keeperHandler));
targetContract(address(_paramHandler));
targetContract(address(_integratorHandler));
targetContract(address(_developerHandler));

{
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = KeeperActor.swap.selector;
selectors[1] = KeeperActor.accumulate.selector;
targetSelector(FuzzSelector({ addr: address(_keeperHandler), selectors: selectors }));
}
{
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = ParamActor.warp.selector;
targetSelector(FuzzSelector({ addr: address(_paramHandler), selectors: selectors }));
}
{
bytes4[] memory selectors = new bytes4[](4);
selectors[0] = UserActor.deposit.selector;
selectors[1] = UserActor.withdraw.selector;
selectors[2] = UserActor.redeem.selector;
selectors[3] = UserActor.withdraw.selector;
targetSelector(FuzzSelector({ addr: address(_userHandler), selectors: selectors }));
}
{
bytes4[] memory selectors = new bytes4[](3);
selectors[0] = IntegratorActor.setVestingPeriod.selector;
selectors[1] = IntegratorActor.setPerformanceFee.selector;
selectors[2] = IntegratorActor.setIntegratorFeeRecipient.selector;
targetSelector(FuzzSelector({ addr: address(_integratorHandler), selectors: selectors }));
}
{
bytes4[] memory selectors = new bytes4[](2);
selectors[0] = DeveloperActor.setDeveloperFeeRecipient.selector;
selectors[1] = DeveloperActor.setDeveloperFee.selector;
targetSelector(FuzzSelector({ addr: address(_developerHandler), selectors: selectors }));
}
}

function invariant_CorrectVesting() public {
VestingStore.Vesting[] memory vestings = _vestingStore.getVestings();
uint256 totalAmount;
for (uint256 i; i < vestings.length; i++) {
if (block.timestamp >= vestings[i].start + strategy.vestingPeriod()) {
totalAmount = 0;
} else {
uint256 nextTimestamp = i + 1 < vestings.length ? vestings[i + 1].start : block.timestamp;
uint256 amount = vestings[i].amount + vestings[i].previousLockedProfit;
totalAmount = amount - (amount * (nextTimestamp - vestings[i].start)) / strategy.vestingPeriod();
}
}
uint256 strategyBalance = ERC4626(strategyAsset).balanceOf(address(strategy));
assertApproxEqAbs(strategy.lockedProfit(), totalAmount, 1);
assertApproxEqAbs(
strategy.totalAssets(),
ERC4626(strategyAsset).convertToAssets(strategyBalance).zeroFloorSub(totalAmount),
1
);
}

function invariant_FeeRecipientNoBurn() public {
assertGe(strategy.balanceOf(strategy.integratorFeeRecipient()), _previousIntegratorShares);
assertGe(strategy.balanceOf(strategy.developerFeeRecipient()), _previousDeveloperShares);

_previousIntegratorShares = strategy.balanceOf(strategy.integratorFeeRecipient());
_previousDeveloperShares = strategy.balanceOf(strategy.developerFeeRecipient());
}

function invariant_CorrectTotalSupply() public {
assertEq(strategy.totalSupply(), _stateVariableStore.shares());
}

function invariant_CorrectTotalAssets() public {
assertApproxEqRel(
strategy.totalAssets(),
ERC4626(strategyAsset).convertToAssets(_stateVariableStore.underlyingStrategyShares()) -
strategy.lockedProfit(),
10
);
assertApproxEqRel(
_stateVariableStore.underlyingStrategyShares(),
ERC4626(strategyAsset).balanceOf(address(strategy)),
10
);
}
}
44 changes: 44 additions & 0 deletions test/invariant/actors/BaseActor.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "../../Constants.t.sol";
import { ERC4626Strategy, ERC4626 } from "../../../contracts/ERC4626Strategy.sol";
import { IERC20 } from "forge-std/interfaces/IERC20.sol";
import { MockRouter } from "../../mock/MockRouter.sol";
import { Test, stdMath, StdStorage, stdStorage, console } from "forge-std/Test.sol";

contract BaseActor is Test {
uint256 internal _minWallet = 0; // in base 18
uint256 internal _maxWallet = 10 ** (18 + 12); // in base 18

ERC4626Strategy public strategy;
IERC20 public asset;
ERC4626 public strategyAsset;
address public router;

mapping(address => uint256) public addressToIndex;
address[] public actors;
uint256 public nbrActor;
address internal _currentActor;

modifier useActor(uint256 actorIndexSeed) {
_currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
vm.startPrank(_currentActor, _currentActor);
_;
vm.stopPrank();
}

constructor(uint256 _nbrActor, string memory actorType, address _strategy) {
for (uint256 i; i < _nbrActor; ++i) {
address actor = address(uint160(uint256(keccak256(abi.encodePacked("actor", actorType, i)))));
actors.push(actor);
addressToIndex[actor] = i;
}
nbrActor = _nbrActor;

strategy = ERC4626Strategy(_strategy);
asset = IERC20(strategy.asset());
strategyAsset = ERC4626(strategy.STRATEGY_ASSET());
router = address(strategy.swapRouter());
}
}
18 changes: 18 additions & 0 deletions test/invariant/actors/Developer.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "./BaseActor.t.sol";

contract DeveloperActor is BaseActor {
constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "developer", _strategy) {}

function setDeveloperFeeRecipient(uint256 actorIndexSeed) public useActor(actorIndexSeed) {
strategy.setDeveloperFeeRecipient(_currentActor);
}

function setDeveloperFee(uint256 actorIndexSeed, uint32 fee) public useActor(actorIndexSeed) {
fee = uint32(bound(fee, 0, strategy.MAX_FEE()));

strategy.setDeveloperFee(fee);
}
}
24 changes: 24 additions & 0 deletions test/invariant/actors/Integrator.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "./BaseActor.t.sol";

contract IntegratorActor is BaseActor {
constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "integrator", _strategy) {}

function setVestingPeriod(uint256 actorIndexSeed, uint64 period) public useActor(actorIndexSeed) {
period = uint64(bound(period, 1 weeks, 30 days));

strategy.setVestingPeriod(period);
}

function setPerformanceFee(uint256 actorIndexSeed, uint32 fee) public useActor(actorIndexSeed) {
fee = uint32(bound(fee, 0, strategy.BPS()));

strategy.setPerformanceFee(fee);
}

function setIntegratorFeeRecipient(uint256 actorIndexSeed) public useActor(actorIndexSeed) {
strategy.setIntegratorFeeRecipient(_currentActor);
}
}
82 changes: 82 additions & 0 deletions test/invariant/actors/Keeper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import { UtilsLib } from "morpho/libraries/UtilsLib.sol";
import { VestingStore } from "../stores/VestingStore.sol";
import { StateVariableStore } from "../stores/StateVariableStore.sol";
import "./BaseActor.t.sol";

contract KeeperActor is BaseActor {
using UtilsLib for uint256;

VestingStore public vestingStore;
StateVariableStore public stateVariableStore;

constructor(
uint256 _nbrActor,
address _strategy,
StateVariableStore _stateVariableStore,
VestingStore _vestingStore
) BaseActor(_nbrActor, "keeper", _strategy) {
stateVariableStore = _stateVariableStore;
vestingStore = _vestingStore;
}

function swap(uint256 actorIndexSeed, uint256 tokenIn, uint256 tokenOut) public useActor(actorIndexSeed) {
tokenIn = bound(tokenIn, 1e18, 1e21);
tokenOut = bound(tokenOut, 1e18, 1e21);

deal(USDC, address(strategy), tokenIn);

address[] memory tokens = new address[](1);
tokens[0] = USDC;
uint256[] memory amounts = new uint256[](1);
amounts[0] = tokenIn;
bytes[] memory data = new bytes[](1);
data[0] = abi.encodeWithSelector(MockRouter.swap.selector, tokenIn, USDC, tokenOut, asset);

uint256 previousLockedProfit = strategy.lockedProfit();
strategy.swap(tokens, data, amounts);

assertEq(strategy.lockedProfit(), previousLockedProfit + tokenOut);
assertEq(strategy.vestingProfit(), previousLockedProfit + tokenOut);
assertEq(strategy.lastUpdate(), block.timestamp);

vestingStore.addVesting(block.timestamp, tokenOut, previousLockedProfit);
stateVariableStore.addUnderlyingStrategyShares(strategyAsset.convertToShares(tokenOut));
}

function accumulate(uint256 actorIndexSeed, uint256 profit, uint8 negative) public useActor(actorIndexSeed) {
uint256 assetsHeld = strategyAsset.convertToAssets(strategyAsset.balanceOf(address(strategy)));
profit = bound(profit, 1e16, 1e20);

vm.mockCall(
address(strategyAsset),
abi.encodeWithSelector(ERC4626.convertToAssets.selector),
abi.encode(negative % 2 == 0 ? assetsHeld - profit : assetsHeld + profit)
);

uint256 totalAssets = strategy.totalAssets();
uint256 lastTotalAssets = strategy.lastTotalAssets();
uint256 previousDeveloperShares = strategy.balanceOf(strategy.developerFeeRecipient());
uint256 previousIntegratorShares = strategy.balanceOf(strategy.integratorFeeRecipient());

strategy.accumulate();

uint256 feeShare = strategy.convertToShares(
((totalAssets.zeroFloorSub(lastTotalAssets)) * strategy.performanceFee()) / strategy.BPS()
);
uint256 developerFeeShare = (feeShare * strategy.developerFee()) / strategy.BPS();

assertEq(strategy.lastTotalAssets(), totalAssets);
assertEq(
strategy.balanceOf(strategy.integratorFeeRecipient()),
previousIntegratorShares + feeShare - developerFeeShare
);
assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), previousDeveloperShares + developerFeeShare);

vm.clearMockedCalls();

stateVariableStore.addShares(feeShare);
}
}
14 changes: 14 additions & 0 deletions test/invariant/actors/Param.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import "./BaseActor.t.sol";

contract ParamActor is BaseActor {
constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "param", _strategy) {}

function warp(uint256 timeForward) public {
timeForward = bound(timeForward, 1, 30 days);

vm.warp(block.timestamp + timeForward);
}
}
Loading
Loading