Skip to content

Commit

Permalink
Feat/tests invariants (#6)
Browse files Browse the repository at this point in the history
* tests: add invariants actors

* fix: prank developer on invariants setup

* tests: all state invariants tests

* chore: add fail on revert for all invariants

* tests: add integrator and developer actors to change strategy params

* tests: increase user actiosn ranges amount

* tests: higher range for accumulate profit or negative profit

* tests: bigger deal for each users
  • Loading branch information
0xtekgrinder authored Aug 9, 2024
1 parent 1c8f70c commit 7c79b1a
Show file tree
Hide file tree
Showing 11 changed files with 614 additions and 4 deletions.
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

0 comments on commit 7c79b1a

Please sign in to comment.