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; + } +}