generated from AngleProtocol/boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
1c8f70c
commit 7c79b1a
Showing
11 changed files
with
614 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.