From 49f099570f27bd73273bca3af18af22cf96d1750 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Fri, 26 Jul 2024 14:13:46 +0200 Subject: [PATCH 1/8] tests: add invariants actors --- test/invariant/.gitkeep | 0 test/invariant/BasicInvariants.t.sol | 74 +++++++++++++++++++++++++++ test/invariant/actors/BaseActor.t.sol | 42 +++++++++++++++ test/invariant/actors/Keeper.t.sol | 27 ++++++++++ test/invariant/actors/Param.t.sol | 14 +++++ test/invariant/actors/User.t.sol | 36 +++++++++++++ 6 files changed, 193 insertions(+) delete mode 100644 test/invariant/.gitkeep create mode 100644 test/invariant/BasicInvariants.t.sol create mode 100644 test/invariant/actors/BaseActor.t.sol create mode 100644 test/invariant/actors/Keeper.t.sol create mode 100644 test/invariant/actors/Param.t.sol create mode 100644 test/invariant/actors/User.t.sol diff --git a/test/invariant/.gitkeep b/test/invariant/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol new file mode 100644 index 0000000..3ab2c57 --- /dev/null +++ b/test/invariant/BasicInvariants.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.19; + +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 "../ERC4626StrategyTest.t.sol"; + +contract BasicInvariants is ERC4626StrategyTest { + uint256 internal constant _NUM_USER = 10; + uint256 internal constant _NUM_KEEPER = 2; + uint256 internal constant _NUM_PARAM = 5; + + UserActor internal _userHandler; + KeeperActor internal _keeperHandler; + ParamActor internal _paramHandler; + + 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(); + + // Create actors + _userHandler = new UserActor(_NUM_USER, address(strategy)); + _keeperHandler = new KeeperActor(_NUM_KEEPER, address(strategy)); + _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))); + } + for (uint256 i; i < _NUM_KEEPER; i++) { + vm.prank(developer); + strategy.grantRole(strategy.KEEPER_ROLE(), _keeperHandler.actors(i)); + vm.label(_keeperHandler.actors(i), string.concat("Keeper ", vm.toString(i))); + } + for (uint256 i; i < _NUM_PARAM; i++) { + vm.label(_paramHandler.actors(i), string.concat("Param ", vm.toString(i))); + } + + targetContract(address(_userHandler)); + targetContract(address(_keeperHandler)); + targetContract(address(_paramHandler)); + + { + 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 })); + } + } + + function invariant_XXXXX() public {} +} diff --git a/test/invariant/actors/BaseActor.t.sol b/test/invariant/actors/BaseActor.t.sol new file mode 100644 index 0000000..35a3f61 --- /dev/null +++ b/test/invariant/actors/BaseActor.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "../../Constants.t.sol"; +import { ERC4626Strategy } 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; + 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()); + router = address(strategy.swapRouter()); + } +} diff --git a/test/invariant/actors/Keeper.t.sol b/test/invariant/actors/Keeper.t.sol new file mode 100644 index 0000000..4b21b7b --- /dev/null +++ b/test/invariant/actors/Keeper.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./BaseActor.t.sol"; + +contract KeeperActor is BaseActor { + constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "keeper", _strategy) {} + + 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); + deal(address(asset), address(router), tokenOut); + + 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); + } + + function accumulate(uint256 actorIndexSeed) public useActor(actorIndexSeed) { + strategy.accumulate(); + } +} diff --git a/test/invariant/actors/Param.t.sol b/test/invariant/actors/Param.t.sol new file mode 100644 index 0000000..000e9d2 --- /dev/null +++ b/test/invariant/actors/Param.t.sol @@ -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, 1 weeks); + + vm.warp(block.timestamp + timeForward); + } +} diff --git a/test/invariant/actors/User.t.sol b/test/invariant/actors/User.t.sol new file mode 100644 index 0000000..b048cfc --- /dev/null +++ b/test/invariant/actors/User.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.19; + +import "./BaseActor.t.sol"; + +contract UserActor is BaseActor { + constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "user", _strategy) {} + + function deposit(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { + deal(address(asset), _currentActor, amount); + strategy.deposit(amount, _currentActor); + } + + function withdraw(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { + if (amount > strategy.maxWithdraw(_currentActor)) { + return; + } + + strategy.withdraw(amount, _currentActor, _currentActor); + } + + function mint(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { + uint256 assets = strategy.convertToAssets(amount); + deal(address(asset), address(strategy), assets); + + strategy.mint(amount, _currentActor); + } + + function redeem(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { + if (amount > strategy.maxRedeem(_currentActor)) { + return; + } + + strategy.redeem(amount, _currentActor, _currentActor); + } +} From d4b8fbe3f01e1eb7e182931409b9b4243007711f Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 29 Jul 2024 16:11:26 +0200 Subject: [PATCH 2/8] fix: prank developer on invariants setup --- test/invariant/BasicInvariants.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index 3ab2c57..3eac137 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -36,11 +36,12 @@ contract BasicInvariants is ERC4626StrategyTest { for (uint256 i; i < _NUM_USER; i++) { vm.label(_userHandler.actors(i), string.concat("User ", vm.toString(i))); } + vm.startPrank(developer); for (uint256 i; i < _NUM_KEEPER; i++) { - vm.prank(developer); strategy.grantRole(strategy.KEEPER_ROLE(), _keeperHandler.actors(i)); vm.label(_keeperHandler.actors(i), string.concat("Keeper ", vm.toString(i))); } + vm.stopPrank(); for (uint256 i; i < _NUM_PARAM; i++) { vm.label(_paramHandler.actors(i), string.concat("Param ", vm.toString(i))); } From 8b1916ec68c3bc8c6f0ab5c2b48b60c263aadd0e Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 15:17:45 +0200 Subject: [PATCH 3/8] tests: all state invariants tests --- foundry.toml | 7 +- test/invariant/BasicInvariants.t.sol | 78 +++++++- test/invariant/actors/BaseActor.t.sol | 4 +- test/invariant/actors/Keeper.t.sol | 61 ++++++- test/invariant/actors/User.t.sol | 180 ++++++++++++++++++- test/invariant/stores/StateVariableStore.sol | 24 +++ test/invariant/stores/VestingStore.sol | 21 +++ 7 files changed, 358 insertions(+), 17 deletions(-) create mode 100644 test/invariant/stores/StateVariableStore.sol create mode 100644 test/invariant/stores/VestingStore.sol diff --git a/foundry.toml b/foundry.toml index 50bc202..035a4f2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ libs = ["lib"] script = "scripts" cache_path = "cache" gas_reports = ["*"] -via_ir = true +via_ir = false sizes = true optimizer = true optimizer_runs = 1000 @@ -17,8 +17,9 @@ ffi = true runs = 10000 [invariant] -runs = 1000 -depth = 30 +fail_on_revert = true +runs = 10 +depth = 1000 [rpc_endpoints] arbitrum = "${ETH_NODE_URI_ARBITRUM}" diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index 3eac137..ce26e2a 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -2,13 +2,18 @@ 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 "../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; @@ -16,6 +21,12 @@ contract BasicInvariants is ERC4626StrategyTest { UserActor internal _userHandler; KeeperActor internal _keeperHandler; ParamActor internal _paramHandler; + VestingStore internal _vestingStore; + StateVariableStore internal _stateVariableStore; + + // state variables + uint256 internal _previousDeveloperShares; + uint256 internal _previousIntegratorShares; function setUp() public virtual override { super.setUp(); @@ -26,15 +37,31 @@ contract BasicInvariants is ERC4626StrategyTest { strategy.setTokenTransferAddress(address(router)); strategy.setSwapRouter(address(router)); vm.stopPrank(); + deal(asset, address(router), 1e27); + + // Deposit some assets + vm.startPrank(alice); + deal(asset, alice, 1e18); + IERC20(asset).approve(address(strategy), 1e18); + strategy.deposit(1e18, alice); + vm.stopPrank(); + + // Create stores + _vestingStore = new VestingStore(); + _stateVariableStore = new StateVariableStore(); + + _stateVariableStore.addShares(1e18); + _stateVariableStore.addUnderlyingStrategyShares(ERC4626(strategyAsset).convertToShares(1e18)); // Create actors - _userHandler = new UserActor(_NUM_USER, address(strategy)); - _keeperHandler = new KeeperActor(_NUM_KEEPER, 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), 1e27); } vm.startPrank(developer); for (uint256 i; i < _NUM_KEEPER; i++) { @@ -71,5 +98,50 @@ contract BasicInvariants is ERC4626StrategyTest { } } - function invariant_XXXXX() public {} + 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 + ); + } } diff --git a/test/invariant/actors/BaseActor.t.sol b/test/invariant/actors/BaseActor.t.sol index 35a3f61..f9cb24a 100644 --- a/test/invariant/actors/BaseActor.t.sol +++ b/test/invariant/actors/BaseActor.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "../../Constants.t.sol"; -import { ERC4626Strategy } from "../../../contracts/ERC4626Strategy.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"; @@ -13,6 +13,7 @@ contract BaseActor is Test { ERC4626Strategy public strategy; IERC20 public asset; + ERC4626 public strategyAsset; address public router; mapping(address => uint256) public addressToIndex; @@ -37,6 +38,7 @@ contract BaseActor is Test { strategy = ERC4626Strategy(_strategy); asset = IERC20(strategy.asset()); + strategyAsset = ERC4626(strategy.STRATEGY_ASSET()); router = address(strategy.swapRouter()); } } diff --git a/test/invariant/actors/Keeper.t.sol b/test/invariant/actors/Keeper.t.sol index 4b21b7b..ae07afd 100644 --- a/test/invariant/actors/Keeper.t.sol +++ b/test/invariant/actors/Keeper.t.sol @@ -1,17 +1,32 @@ // 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 { - constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "keeper", _strategy) {} + 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); - deal(address(asset), address(router), tokenOut); address[] memory tokens = new address[](1); tokens[0] = USDC; @@ -19,9 +34,49 @@ contract KeeperActor is BaseActor { 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) public useActor(actorIndexSeed) { + function accumulate(uint256 actorIndexSeed, uint256 profit, uint8 negative) public useActor(actorIndexSeed) { + uint256 assetsHeld = strategyAsset.convertToAssets(strategyAsset.balanceOf(address(strategy))); + profit = bound(profit, 1, 1e8); + + 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); } } diff --git a/test/invariant/actors/User.t.sol b/test/invariant/actors/User.t.sol index b048cfc..f934d6a 100644 --- a/test/invariant/actors/User.t.sol +++ b/test/invariant/actors/User.t.sol @@ -1,14 +1,66 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.19; +import { UtilsLib } from "morpho/libraries/UtilsLib.sol"; +import { StateVariableStore } from "../stores/StateVariableStore.sol"; import "./BaseActor.t.sol"; contract UserActor is BaseActor { - constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "user", _strategy) {} + using UtilsLib for uint256; + + StateVariableStore public stateVariableStore; + + constructor( + uint256 _nbrActor, + address _strategy, + StateVariableStore _stateVariableStore + ) BaseActor(_nbrActor, "user", _strategy) { + stateVariableStore = _stateVariableStore; + } function deposit(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { - deal(address(asset), _currentActor, amount); - strategy.deposit(amount, _currentActor); + amount = bound(amount, 1e18, 1e21); + + uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); + uint256 previousIntegratorBalance = strategy.balanceOf(strategy.integratorFeeRecipient()); + uint256 previousBalance = strategy.balanceOf(_currentActor); + uint256 previousStrategyBalance = strategyAsset.balanceOf(address(strategy)); + + uint256 feeShares; + { + uint256 storedTotalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + feeShares = strategy.convertToShares( + ((storedTotalAssets.zeroFloorSub(lastTotalAssets)) * strategy.performanceFee()) / strategy.BPS() + ); + } + + asset.approve(address(strategy), amount); + uint256 previewedDeposit = strategy.previewDeposit(amount); + uint256 deposited = strategy.deposit(amount, _currentActor); + + { + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + assertEq( + strategy.balanceOf(strategy.integratorFeeRecipient()), + previousIntegratorBalance + feeShares - developerFeeShares + ); + assertEq( + strategy.balanceOf(strategy.developerFeeRecipient()), + previousDeveloperBalance + developerFeeShares + ); + } + + assertEq(previewedDeposit, deposited); + assertEq( + strategyAsset.balanceOf(address(strategy)), + previousStrategyBalance + strategyAsset.convertToShares(amount) + ); + assertEq(asset.balanceOf(address(strategy)), 0); + assertEq(strategy.balanceOf(_currentActor), previousBalance + previewedDeposit); + + stateVariableStore.addUnderlyingStrategyShares(strategyAsset.convertToShares(amount)); + stateVariableStore.addShares(deposited + feeShares); } function withdraw(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { @@ -16,14 +68,93 @@ contract UserActor is BaseActor { return; } - strategy.withdraw(amount, _currentActor, _currentActor); + uint256 previousBalance = strategy.balanceOf(_currentActor); + uint256 previousAssetBalance = asset.balanceOf(_currentActor); + uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); + uint256 previousIntegratorBalance = strategy.balanceOf(strategy.integratorFeeRecipient()); + + uint256 feeShares; + { + uint256 storedTotalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + feeShares = strategy.convertToShares( + ((storedTotalAssets.zeroFloorSub(lastTotalAssets)) * strategy.performanceFee()) / strategy.BPS() + ); + } + + uint256 previewedWithdraw = strategy.previewWithdraw(amount); + uint256 withdrawed = strategy.withdraw(amount, _currentActor, _currentActor); + + { + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + assertEq( + strategy.balanceOf(strategy.integratorFeeRecipient()), + previousIntegratorBalance + feeShares - developerFeeShares + ); + assertEq( + strategy.balanceOf(strategy.developerFeeRecipient()), + previousDeveloperBalance + developerFeeShares + ); + } + + assertEq(previewedWithdraw, withdrawed); + assertEq(asset.balanceOf(_currentActor), previousAssetBalance + amount); + assertEq(strategy.balanceOf(_currentActor), previousBalance - previewedWithdraw); + + stateVariableStore.removeUnderlyingStrategyShares(strategyAsset.convertToShares(amount)); + stateVariableStore.removeShares(withdrawed); + stateVariableStore.addShares(feeShares); } function mint(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { + amount = bound(amount, 1e18, 1e21); uint256 assets = strategy.convertToAssets(amount); - deal(address(asset), address(strategy), assets); - strategy.mint(amount, _currentActor); + uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); + uint256 previousIntegratorBalance = strategy.balanceOf(strategy.integratorFeeRecipient()); + uint256 previousBalance = strategy.balanceOf(_currentActor); + uint256 previousStrategyBalance = strategyAsset.balanceOf(address(strategy)); + + uint256 feeShares; + { + uint256 storedTotalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + feeShares = strategy.convertToShares( + ((storedTotalAssets.zeroFloorSub(lastTotalAssets)) * strategy.performanceFee()) / strategy.BPS() + ); + } + + asset.approve(address(strategy), assets); + + uint256 previewedMint = strategy.previewMint(amount); + uint256 assetsMinted = strategy.mint(amount, _currentActor); + + { + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + assertEq( + strategy.balanceOf(strategy.integratorFeeRecipient()), + previousIntegratorBalance + feeShares - developerFeeShares + ); + assertEq( + strategy.balanceOf(strategy.developerFeeRecipient()), + previousDeveloperBalance + developerFeeShares + ); + } + + assertEq(assetsMinted, previewedMint); + assertEq( + strategyAsset.balanceOf(address(strategy)), + previousStrategyBalance + strategyAsset.convertToShares(previewedMint) + ); + assertEq(asset.balanceOf(address(strategy)), 0); + assertEq( + strategy.totalSupply(), + previousBalance + amount + feeShares + previousIntegratorBalance + previousDeveloperBalance + ); + assertEq(strategy.balanceOf(_currentActor), previousBalance + amount); + + stateVariableStore.addUnderlyingStrategyShares(strategyAsset.convertToShares(assets)); + stateVariableStore.addShares(feeShares + amount); } function redeem(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { @@ -31,6 +162,41 @@ contract UserActor is BaseActor { return; } - strategy.redeem(amount, _currentActor, _currentActor); + uint256 previousBalance = strategy.balanceOf(_currentActor); + uint256 previousAssetBalance = asset.balanceOf(_currentActor); + uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); + uint256 previousIntegratorBalance = strategy.balanceOf(strategy.integratorFeeRecipient()); + + uint256 feeShares; + { + uint256 storedTotalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + feeShares = strategy.convertToShares( + ((storedTotalAssets.zeroFloorSub(lastTotalAssets)) * strategy.performanceFee()) / strategy.BPS() + ); + } + + uint256 previewedRedeem = strategy.previewRedeem(amount); + uint256 redeemed = strategy.redeem(amount, _currentActor, _currentActor); + + { + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + assertEq( + strategy.balanceOf(strategy.integratorFeeRecipient()), + previousIntegratorBalance + feeShares - developerFeeShares + ); + assertEq( + strategy.balanceOf(strategy.developerFeeRecipient()), + previousDeveloperBalance + developerFeeShares + ); + } + + assertEq(previewedRedeem, redeemed); + assertEq(asset.balanceOf(_currentActor), previousAssetBalance + previewedRedeem); + assertEq(strategy.balanceOf(_currentActor), previousBalance - amount); + + stateVariableStore.removeUnderlyingStrategyShares(strategyAsset.convertToShares(redeemed)); + stateVariableStore.removeShares(amount); + stateVariableStore.addShares(feeShares); } } diff --git a/test/invariant/stores/StateVariableStore.sol b/test/invariant/stores/StateVariableStore.sol new file mode 100644 index 0000000..ff1a26e --- /dev/null +++ b/test/invariant/stores/StateVariableStore.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.19; + +contract StateVariableStore { + uint256 public shares; + uint256 public underlyingStrategyShares; + + function addShares(uint256 _shares) public { + shares += _shares; + } + + function removeShares(uint256 _shares) public { + shares -= _shares; + } + + function addUnderlyingStrategyShares(uint256 _underlyingStrategyShares) public { + underlyingStrategyShares += _underlyingStrategyShares; + } + + function removeUnderlyingStrategyShares(uint256 _underlyingStrategyShares) public { + underlyingStrategyShares -= _underlyingStrategyShares; + } +} diff --git a/test/invariant/stores/VestingStore.sol b/test/invariant/stores/VestingStore.sol new file mode 100644 index 0000000..6ed7a0d --- /dev/null +++ b/test/invariant/stores/VestingStore.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.19; + +contract VestingStore { + struct Vesting { + uint256 start; + uint256 amount; + uint256 previousLockedProfit; + } + + Vesting[] public vestings; + + function addVesting(uint256 start, uint256 amount, uint256 previousLockedProfit) public { + vestings.push(Vesting(start, amount, previousLockedProfit)); + } + + function getVestings() public view returns (Vesting[] memory) { + return vestings; + } +} From 871c8ac1c47e71fed852b3ab2cb8f73f4df5a3cc Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 16:04:35 +0200 Subject: [PATCH 4/8] chore: add fail on revert for all invariants --- foundry.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/foundry.toml b/foundry.toml index 035a4f2..639dc05 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,7 +6,7 @@ libs = ["lib"] script = "scripts" cache_path = "cache" gas_reports = ["*"] -via_ir = false +via_ir = true sizes = true optimizer = true optimizer_runs = 1000 @@ -17,9 +17,9 @@ ffi = true runs = 10000 [invariant] -fail_on_revert = true runs = 10 depth = 1000 +fail_on_revert = true [rpc_endpoints] arbitrum = "${ETH_NODE_URI_ARBITRUM}" @@ -58,7 +58,7 @@ runs = 2000 [profile.dev.invariant] runs = 10 depth = 1 -fail_on_revert = false +fail_on_revert = true [profile.ci] src = "test" @@ -71,4 +71,4 @@ runs = 100 [profile.ci.invariant] runs = 10 depth = 30 -fail_on_revert = false +fail_on_revert = true From 888df92a58983fba0977a8c0387ba01b0078e7ca Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 17:25:33 +0200 Subject: [PATCH 5/8] tests: add integrator and developer actors to change strategy params --- test/invariant/BasicInvariants.t.sol | 33 ++++++++++++++++++++++++++ test/invariant/actors/Developer.t.sol | 18 ++++++++++++++ test/invariant/actors/Integrator.t.sol | 24 +++++++++++++++++++ test/invariant/actors/Param.t.sol | 2 +- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/invariant/actors/Developer.t.sol create mode 100644 test/invariant/actors/Integrator.t.sol diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index ce26e2a..88b4e45 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -9,6 +9,8 @@ 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 { @@ -17,10 +19,14 @@ contract BasicInvariants is ERC4626StrategyTest { 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; @@ -54,6 +60,8 @@ contract BasicInvariants is ERC4626StrategyTest { _stateVariableStore.addUnderlyingStrategyShares(ERC4626(strategyAsset).convertToShares(1e18)); // 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)); @@ -68,14 +76,26 @@ contract BasicInvariants is ERC4626StrategyTest { 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); @@ -96,6 +116,19 @@ contract BasicInvariants is ERC4626StrategyTest { 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 { diff --git a/test/invariant/actors/Developer.t.sol b/test/invariant/actors/Developer.t.sol new file mode 100644 index 0000000..2d30350 --- /dev/null +++ b/test/invariant/actors/Developer.t.sol @@ -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); + } +} diff --git a/test/invariant/actors/Integrator.t.sol b/test/invariant/actors/Integrator.t.sol new file mode 100644 index 0000000..ada2516 --- /dev/null +++ b/test/invariant/actors/Integrator.t.sol @@ -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); + } +} diff --git a/test/invariant/actors/Param.t.sol b/test/invariant/actors/Param.t.sol index 000e9d2..fff5152 100644 --- a/test/invariant/actors/Param.t.sol +++ b/test/invariant/actors/Param.t.sol @@ -7,7 +7,7 @@ contract ParamActor is BaseActor { constructor(uint256 _nbrActor, address _strategy) BaseActor(_nbrActor, "param", _strategy) {} function warp(uint256 timeForward) public { - timeForward = bound(timeForward, 1, 1 weeks); + timeForward = bound(timeForward, 1, 30 days); vm.warp(block.timestamp + timeForward); } From 1b513552409e837684a41d2d268b5a170c4d37ac Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 17:29:00 +0200 Subject: [PATCH 6/8] tests: increase user actiosn ranges amount --- test/invariant/BasicInvariants.t.sol | 2 +- test/invariant/actors/User.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index 88b4e45..65ebd0c 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -43,7 +43,7 @@ contract BasicInvariants is ERC4626StrategyTest { strategy.setTokenTransferAddress(address(router)); strategy.setSwapRouter(address(router)); vm.stopPrank(); - deal(asset, address(router), 1e27); + deal(asset, address(router), 1e40); // Deposit some assets vm.startPrank(alice); diff --git a/test/invariant/actors/User.t.sol b/test/invariant/actors/User.t.sol index f934d6a..9fc9cca 100644 --- a/test/invariant/actors/User.t.sol +++ b/test/invariant/actors/User.t.sol @@ -19,7 +19,7 @@ contract UserActor is BaseActor { } function deposit(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { - amount = bound(amount, 1e18, 1e21); + amount = bound(amount, 1e15, 1e26); uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); uint256 previousIntegratorBalance = strategy.balanceOf(strategy.integratorFeeRecipient()); @@ -107,7 +107,7 @@ contract UserActor is BaseActor { } function mint(uint256 actorIndexSeed, uint256 amount) public useActor(actorIndexSeed) { - amount = bound(amount, 1e18, 1e21); + amount = bound(amount, 1e15, 1e26); uint256 assets = strategy.convertToAssets(amount); uint256 previousDeveloperBalance = strategy.balanceOf(strategy.developerFeeRecipient()); From 61298c9cc6661db77c2666e64589dc54a2faecd0 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 17:40:25 +0200 Subject: [PATCH 7/8] tests: higher range for accumulate profit or negative profit --- test/invariant/BasicInvariants.t.sol | 10 +++++----- test/invariant/actors/Keeper.t.sol | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index 65ebd0c..f6e23bb 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -47,17 +47,17 @@ contract BasicInvariants is ERC4626StrategyTest { // Deposit some assets vm.startPrank(alice); - deal(asset, alice, 1e18); - IERC20(asset).approve(address(strategy), 1e18); - strategy.deposit(1e18, 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(1e18); - _stateVariableStore.addUnderlyingStrategyShares(ERC4626(strategyAsset).convertToShares(1e18)); + _stateVariableStore.addShares(1e24); + _stateVariableStore.addUnderlyingStrategyShares(ERC4626(strategyAsset).convertToShares(1e24)); // Create actors _developerHandler = new DeveloperActor(_NUM_DEVELOPER, address(strategy)); diff --git a/test/invariant/actors/Keeper.t.sol b/test/invariant/actors/Keeper.t.sol index ae07afd..65015f6 100644 --- a/test/invariant/actors/Keeper.t.sol +++ b/test/invariant/actors/Keeper.t.sol @@ -48,7 +48,7 @@ contract KeeperActor is BaseActor { function accumulate(uint256 actorIndexSeed, uint256 profit, uint8 negative) public useActor(actorIndexSeed) { uint256 assetsHeld = strategyAsset.convertToAssets(strategyAsset.balanceOf(address(strategy))); - profit = bound(profit, 1, 1e8); + profit = bound(profit, 1e16, 1e20); vm.mockCall( address(strategyAsset), From 3a71bd2aed046693109c6c388b2d52705f331ee8 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Mon, 5 Aug 2024 18:35:32 +0200 Subject: [PATCH 8/8] tests: bigger deal for each users --- test/invariant/BasicInvariants.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/invariant/BasicInvariants.t.sol b/test/invariant/BasicInvariants.t.sol index f6e23bb..3e75847 100644 --- a/test/invariant/BasicInvariants.t.sol +++ b/test/invariant/BasicInvariants.t.sol @@ -43,7 +43,7 @@ contract BasicInvariants is ERC4626StrategyTest { strategy.setTokenTransferAddress(address(router)); strategy.setSwapRouter(address(router)); vm.stopPrank(); - deal(asset, address(router), 1e40); + deal(asset, address(router), 1e30); // Deposit some assets vm.startPrank(alice); @@ -69,7 +69,7 @@ contract BasicInvariants is ERC4626StrategyTest { // 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), 1e27); + deal(asset, _userHandler.actors(i), 1e30); } vm.startPrank(developer); for (uint256 i; i < _NUM_KEEPER; i++) {