diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 0c32b764..c65d317e 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -462,16 +462,17 @@ def _convert_to_shares(assets: uint256, rounding: Rounding) -> uint256: return assets total_supply: uint256 = self._total_supply() + + # if total_supply is 0, price_per_share is 1 + if total_supply == 0: + return assets + total_assets: uint256 = self._total_assets() + # if total_Supply > 0 but total_assets == 0, price_per_share = 0 if total_assets == 0: - # if total_assets and total_supply is 0, price_per_share is 1 - if total_supply == 0: - return assets - else: - # Else if total_supply > 0 price_per_share is 0 - return 0 - + return 0 + numerator: uint256 = assets * total_supply shares: uint256 = numerator / total_assets if rounding == Rounding.ROUND_UP and numerator % total_assets != 0: @@ -504,31 +505,6 @@ def _issue_shares(shares: uint256, recipient: address): log Transfer(empty(address), recipient, shares) -@internal -def _issue_shares_for_amount(amount: uint256, recipient: address) -> uint256: - """ - Issues shares that are worth 'amount' in the underlying token (asset). - WARNING: this takes into account that any new assets have been summed - to total_assets (otherwise pps will go down). - """ - total_supply: uint256 = self._total_supply() - total_assets: uint256 = self._total_assets() - new_shares: uint256 = 0 - - # If no supply PPS = 1. - if total_supply == 0: - new_shares = amount - elif total_assets > amount: - new_shares = amount * total_supply / (total_assets - amount) - - # We don't make the function revert - if new_shares == 0: - return 0 - - self._issue_shares(new_shares, recipient) - - return new_shares - ## ERC4626 ## @view @internal @@ -662,52 +638,16 @@ def _max_withdraw( return max_assets @internal -def _deposit(sender: address, recipient: address, assets: uint256) -> uint256: +def _deposit(recipient: address, assets: uint256, shares: uint256): """ - Used for `deposit` calls to transfer the amount of `asset` to the vault, - issue the corresponding shares to the `recipient` and update all needed + Used for `deposit` and `mint` calls to transfer the amount of `asset` to the vault, + issue the corresponding `shares` to the `recipient` and update all needed vault accounting. """ - assert self.shutdown == False # dev: shutdown - - amount: uint256 = assets - # Deposit all if sent with max uint - if amount == max_value(uint256): - amount = ERC20(self.asset).balanceOf(msg.sender) - - assert amount <= self._max_deposit(recipient), "exceed deposit limit" - - # Transfer the tokens to the vault first. - self._erc20_safe_transfer_from(self.asset, msg.sender, self, amount) - # Record the change in total assets. - self.total_idle += amount - - # Issue the corresponding shares for amount. - shares: uint256 = self._issue_shares_for_amount(amount, recipient) - - assert shares > 0, "cannot mint zero" - - log Deposit(sender, recipient, amount, shares) - - if self.auto_allocate: - self._update_debt(self.default_queue[0], max_value(uint256), 0) - - return shares - -@internal -def _mint(sender: address, recipient: address, shares: uint256) -> uint256: - """ - Used for `mint` calls to issue the corresponding shares to the `recipient`, - transfer the amount of `asset` to the vault, and update all needed vault - accounting. - """ - assert self.shutdown == False # dev: shutdown - # Get corresponding amount of assets. - assets: uint256 = self._convert_to_assets(shares, Rounding.ROUND_UP) - - assert assets > 0, "cannot deposit zero" assert assets <= self._max_deposit(recipient), "exceed deposit limit" - + assert assets > 0, "cannot deposit zero" + assert shares > 0, "cannot mint zero" + # Transfer the tokens to the vault first. self._erc20_safe_transfer_from(self.asset, msg.sender, self, assets) # Record the change in total assets. @@ -716,13 +656,11 @@ def _mint(sender: address, recipient: address, shares: uint256) -> uint256: # Issue the corresponding shares for assets. self._issue_shares(shares, recipient) - log Deposit(sender, recipient, assets, shares) + log Deposit(msg.sender, recipient, assets, shares) if self.auto_allocate: self._update_debt(self.default_queue[0], max_value(uint256), 0) - return assets - @view @internal def _assess_share_of_unrealised_losses(strategy: address, strategy_current_debt: uint256, assets_needed: uint256) -> uint256: @@ -1849,7 +1787,14 @@ def deposit(assets: uint256, receiver: address) -> uint256: @param receiver The address to receive the shares. @return The amount of shares minted. """ - return self._deposit(msg.sender, receiver, assets) + amount: uint256 = assets + # Deposit all if sent with max uint + if amount == max_value(uint256): + amount = ERC20(self.asset).balanceOf(msg.sender) + + shares: uint256 = self._convert_to_shares(amount, Rounding.ROUND_DOWN) + self._deposit(receiver, amount, shares) + return shares @external @nonreentrant("lock") @@ -1860,7 +1805,9 @@ def mint(shares: uint256, receiver: address) -> uint256: @param receiver The address to receive the shares. @return The amount of assets deposited. """ - return self._mint(msg.sender, receiver, shares) + assets: uint256 = self._convert_to_assets(shares, Rounding.ROUND_UP) + self._deposit(receiver, assets, shares) + return assets @external @nonreentrant("lock") diff --git a/foundry.toml b/foundry.toml index 864746bc..fbce3b5a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,8 +13,8 @@ remappings = [ ] fs_permissions = [{ access = "read", path = "./"}] -match_contract = "VaultERC4626StdTest" -#match_path = "./foundry_tests/tests/*" +#match_contract = "VaultERC4626StdTest" +match_path = "./foundry_tests/tests/*" ffi = true [fuzz] diff --git a/foundry_tests/handlers/VaultHandler.sol b/foundry_tests/handlers/VaultHandler.sol new file mode 100644 index 00000000..19e67513 --- /dev/null +++ b/foundry_tests/handlers/VaultHandler.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import "forge-std/console.sol"; +import {ExtendedTest} from "../utils/ExtendedTest.sol"; +import {Setup, IVault, ERC20Mock, MockTokenizedStrategy} from "../utils/Setup.sol"; +import {LibAddressSet, AddressSet} from "../utils/LibAddressSet.sol"; + +contract VaultHandler is ExtendedTest { + using LibAddressSet for AddressSet; + + Setup public setup; + IVault public vault; + ERC20Mock public asset; + MockTokenizedStrategy public strategy; + + address public keeper; + + uint256 public maxFuzzAmount = 1e30; + uint256 public minFuzzAmount = 10_000; + + uint256 public ghost_depositSum; + uint256 public ghost_withdrawSum; + uint256 public ghost_debt; + uint256 public ghost_profitSum; + uint256 public ghost_lossSum; + uint256 public ghost_unreportedLossSum; + + uint256 public ghost_zeroDeposits; + uint256 public ghost_zeroWithdrawals; + uint256 public ghost_zeroTransfers; + uint256 public ghost_zeroTransferFroms; + + bool public unreported; + + mapping(bytes32 => uint256) public calls; + + AddressSet internal _actors; + address internal actor; + + modifier createActor() { + actor = msg.sender; + _actors.add(msg.sender); + _; + } + + modifier useActor(uint256 actorIndexSeed) { + actor = _actors.rand(actorIndexSeed); + _; + } + + modifier countCall(bytes32 key) { + calls[key]++; + _; + } + + constructor() { + setup = Setup(msg.sender); + + asset = setup.asset(); + vault = setup.vault(); + strategy = setup.strategy(); + keeper = setup.keeper(); + skip(10); + } + + function deposit(uint256 _amount) public createActor countCall("deposit") { + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); + + asset.mint(actor, _amount); + vm.prank(actor); + asset.approve(address(vault), _amount); + + vm.prank(actor); + vault.deposit(_amount, actor); + + ghost_depositSum += _amount; + } + + function mint(uint256 _amount) public createActor countCall("mint") { + _amount = bound(_amount, minFuzzAmount, maxFuzzAmount); + + uint256 toMint = vault.previewMint(_amount); + asset.mint(actor, toMint); + + vm.prank(actor); + asset.approve(address(vault), toMint); + + vm.prank(actor); + uint256 assets = vault.mint(_amount, actor); + + ghost_depositSum += assets; + } + + function withdraw( + uint256 actorSeed, + uint256 _amount + ) public useActor(actorSeed) countCall("withdraw") { + if (vault.maxWithdraw(address(actor)) == 0) { + unchecked { + deposit(_amount * 2); + } + } + _amount = bound(_amount, 0, vault.maxWithdraw(address(actor))); + if (_amount == 0) ghost_zeroWithdrawals++; + + uint256 idle = vault.totalIdle(); + + vm.prank(actor); + vault.withdraw(_amount, actor, actor, 0); + + ghost_withdrawSum += _amount; + if (_amount > idle) ghost_debt -= (_amount - idle); + } + + function redeem( + uint256 actorSeed, + uint256 _amount + ) public useActor(actorSeed) countCall("redeem") { + if (vault.balanceOf(address(actor)) == 0) { + unchecked { + mint(_amount * 2); + } + } + _amount = bound(_amount, 0, vault.balanceOf(address(actor))); + if (_amount == 0) ghost_zeroWithdrawals++; + + uint256 idle = vault.totalIdle(); + + vm.prank(actor); + uint256 assets = vault.redeem(_amount, actor, actor, 0); + + ghost_withdrawSum += assets; + if (assets > idle) ghost_debt -= (assets - idle); + } + + function updateDebt(uint256 _amount) public countCall("updateDebt") { + uint256 min = unreported ? ghost_debt : 0; + + _amount = bound(_amount, min, vault.totalAssets()); + + vm.prank(keeper); + uint256 newDebt = vault.update_debt(address(strategy), _amount); + + ghost_debt = newDebt; + } + + function reportProfit(uint256 _amount) public countCall("reportProfit") { + _amount = bound(_amount, 1_000, strategy.totalAssets() / 2); + + // Simulate earning interest + asset.mint(address(strategy), _amount); + + vm.prank(keeper); + strategy.report(); + + vm.prank(keeper); + (uint256 profit, uint256 loss) = vault.process_report( + address(strategy) + ); + + ghost_profitSum += profit; + ghost_lossSum += loss; + ghost_debt += profit; + ghost_debt -= loss; + unreported = false; + } + + function reportLoss(uint256 _amount) public countCall("reportLoss") { + _amount = bound(_amount, 0, strategy.totalAssets() / 2); + + // Simulate losing money + vm.prank(address(strategy)); + asset.transfer(address(69), _amount); + + vm.prank(keeper); + strategy.report(); + + vm.prank(keeper); + (uint256 profit, uint256 loss) = vault.process_report( + address(strategy) + ); + + ghost_profitSum += profit; + ghost_lossSum += loss; + ghost_debt += profit; + ghost_debt -= loss; + unreported = false; + } + + function approve( + uint256 actorSeed, + uint256 spenderSeed, + uint256 amount + ) public useActor(actorSeed) countCall("approve") { + address spender = _actors.rand(spenderSeed); + + vm.prank(actor); + vault.approve(spender, amount); + } + + function transfer( + uint256 actorSeed, + uint256 toSeed, + uint256 amount + ) public useActor(actorSeed) countCall("transfer") { + address to = _actors.rand(toSeed); + + amount = bound(amount, 0, vault.balanceOf(actor)); + if (amount == 0) ghost_zeroTransfers++; + + vm.prank(actor); + vault.transfer(to, amount); + } + + function transferFrom( + uint256 actorSeed, + uint256 fromSeed, + uint256 amount + ) public useActor(actorSeed) countCall("transferFrom") { + address from = _actors.rand(fromSeed); + address to = msg.sender; + _actors.add(msg.sender); + + amount = bound(amount, 0, vault.balanceOf(from)); + uint256 allowance = vault.allowance(actor, from); + if (allowance != 0) { + vm.prank(from); + vault.approve(actor, 0); + } + + vm.prank(from); + vault.approve(actor, amount); + + if (amount == 0) ghost_zeroTransferFroms++; + + vm.prank(actor); + vault.transferFrom(from, to, amount); + } + + function unreportedLoss( + uint256 _amount + ) public countCall("unreportedLoss") { + _amount = bound(_amount, 0, strategy.totalAssets() / 10); + + // Simulate losing money + vm.prank(address(strategy)); + asset.transfer(address(69), _amount); + + vm.prank(keeper); + strategy.report(); + + ghost_unreportedLossSum += _amount; + unreported = true; + } + + function increaseTime() public countCall("skip") { + skip(1 days); + } + + function callSummary() external view { + console.log("Call summary:"); + console.log("-------------------"); + console.log("deposit", calls["deposit"]); + console.log("mint", calls["mint"]); + console.log("withdraw", calls["withdraw"]); + console.log("redeem", calls["redeem"]); + console.log("debt updates", calls["debtUpdate"]); + console.log("report profit", calls["reportProfit"]); + console.log("report loss", calls["reportLoss"]); + console.log("tend", calls["tend"]); + console.log("approve", calls["approve"]); + console.log("transfer", calls["transfer"]); + console.log("transferFrom", calls["transferFrom"]); + console.log("skip", calls["skip"]); + console.log("unreportedLoss", calls["unreportedLoss"]); + console.log("-------------------"); + console.log("Total Deposit sum", ghost_depositSum); + console.log("Total withdraw sum", ghost_withdrawSum); + console.log("Current Debt", ghost_debt); + console.log("Total Profit", ghost_profitSum); + console.log("Total Loss", ghost_lossSum); + console.log("Total unreported Loss", ghost_unreportedLossSum); + console.log("-------------------"); + console.log("Amount of actors", _actors.count()); + console.log("Zero Deposits:", ghost_zeroDeposits); + console.log("Zero withdrawals:", ghost_zeroWithdrawals); + console.log("Zero transferFroms:", ghost_zeroTransferFroms); + console.log("Zero transfers:", ghost_zeroTransfers); + } +} diff --git a/foundry_tests/tests/Invariants.t.sol b/foundry_tests/tests/Invariants.t.sol new file mode 100644 index 00000000..1c4cf968 --- /dev/null +++ b/foundry_tests/tests/Invariants.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.18; + +import "forge-std/console.sol"; +import {BaseInvariant} from "../utils/BaseInvariant.sol"; +import {VaultHandler} from "../handlers/VaultHandler.sol"; + +contract VaultInvariantTest is BaseInvariant { + VaultHandler public vaultHandler; + + function setUp() public override { + super.setUp(); + + vaultHandler = new VaultHandler(); + + excludeSender(address(0)); + excludeSender(address(vault)); + excludeSender(address(asset)); + excludeSender(address(strategy)); + + targetContract(address(vaultHandler)); + + targetSelector( + FuzzSelector({ + addr: address(vaultHandler), + selectors: getTargetSelectors() + }) + ); + } + + function invariant_totalAssets() public { + assert_totalAssets( + vaultHandler.ghost_depositSum(), + vaultHandler.ghost_withdrawSum(), + vaultHandler.ghost_profitSum(), + vaultHandler.ghost_lossSum() + ); + } + + function invariant_maxWithdraw() public { + assert_maxWithdraw(vaultHandler.unreported()); + } + + function invariant_maxRedeem() public { + assert_maxRedeem(vaultHandler.unreported()); + } + + function invariant_maxWithdrawEqualsMaxRedeem() public { + assert_maxRedeemEqualsMaxWithdraw(vaultHandler.unreported()); + } + + function invariant_unlockingTime() public { + assert_unlockingTime(); + } + + function invariant_unlockedShares() public { + assert_unlockedShares(); + } + + function invariant_previewMintAndConvertToAssets() public { + assert_previewMintAndConvertToAssets(); + } + + function invariant_previewWithdrawAndConvertToShares() public { + assert_previewWithdrawAndConvertToShares(); + } + + function invariant_balanceAndTotalAssets() public { + assert_balanceAndTotalAssets(vaultHandler.unreported()); + } + + function invariant_totalDebt() public { + assert_totalDebt(vaultHandler.unreported()); + } + + function invariant_callSummary() public view { + vaultHandler.callSummary(); + } + + function getTargetSelectors() + internal + view + returns (bytes4[] memory selectors) + { + selectors = new bytes4[](12); + selectors[0] = vaultHandler.deposit.selector; + selectors[1] = vaultHandler.withdraw.selector; + selectors[2] = vaultHandler.mint.selector; + selectors[3] = vaultHandler.redeem.selector; + selectors[4] = vaultHandler.reportProfit.selector; + selectors[5] = vaultHandler.reportLoss.selector; + selectors[6] = vaultHandler.unreportedLoss.selector; + selectors[7] = vaultHandler.approve.selector; + selectors[8] = vaultHandler.transfer.selector; + selectors[9] = vaultHandler.transferFrom.selector; + selectors[10] = vaultHandler.increaseTime.selector; + selectors[11] = vaultHandler.updateDebt.selector; + } +} diff --git a/foundry_tests/utils/BaseInvariant.sol b/foundry_tests/utils/BaseInvariant.sol new file mode 100644 index 00000000..4bf3adb2 --- /dev/null +++ b/foundry_tests/utils/BaseInvariant.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.18; + +import "forge-std/console.sol"; +import {Setup} from "./Setup.sol"; + +abstract contract BaseInvariant is Setup { + function setUp() public virtual override { + super.setUp(); + } + + function assert_totalAssets( + uint256 _totalDeposits, + uint256 _totalWithdraw, + uint256 _totalGain, + uint256 _totalLosses + ) public { + assertEq( + vault.totalAssets(), + _totalDeposits + _totalGain - _totalWithdraw - _totalLosses + ); + assertEq(vault.totalAssets(), vault.totalDebt() + vault.totalIdle()); + } + + function assert_maxWithdraw(bool unreportedLoss) public { + if (unreportedLoss) { + // withdraw would revert with unreported loss so maxWithdraw is totalIdle + assertLe(vault.maxWithdraw(msg.sender), vault.totalIdle()); + } else { + assertLe(vault.maxWithdraw(msg.sender), vault.totalAssets()); + } + assertLe(vault.maxWithdraw(msg.sender, 10_000), vault.totalAssets()); + } + + function assert_maxRedeem(bool unreportedLoss) public { + assertLe(vault.maxRedeem(msg.sender), vault.totalSupply()); + assertLe(vault.maxRedeem(msg.sender), vault.balanceOf(msg.sender)); + if (unreportedLoss) { + assertLe(vault.maxRedeem(msg.sender, 0), vault.totalIdle()); + } else { + assertLe(vault.maxRedeem(msg.sender, 0), vault.totalSupply()); + assertLe( + vault.maxRedeem(msg.sender, 0), + vault.balanceOf(msg.sender) + ); + } + } + + function assert_maxRedeemEqualsMaxWithdraw(bool unreportedLoss) public { + if (unreportedLoss) { + assertApproxEq( + vault.maxWithdraw(msg.sender, 10_000), + vault.convertToAssets(vault.maxRedeem(msg.sender)), + 3 + ); + assertApproxEq( + vault.maxRedeem(msg.sender), + vault.convertToShares(vault.maxWithdraw(msg.sender, 10_000)), + 3 + ); + } else { + assertApproxEq( + vault.maxWithdraw(msg.sender), + vault.convertToAssets(vault.maxRedeem(msg.sender)), + 3 + ); + assertApproxEq( + vault.maxRedeem(msg.sender), + vault.convertToShares(vault.maxWithdraw(msg.sender)), + 3 + ); + } + } + + function assert_unlockingTime() public { + uint256 unlockingDate = vault.fullProfitUnlockDate(); + uint256 balance = vault.balanceOf(address(vault)); + uint256 unlockedShares = vault.unlockedShares(); + if (unlockingDate != 0 && vault.profitUnlockingRate() > 0) { + if ( + block.timestamp == + vault.strategies(address(strategy)).last_report + ) { + assertEq(unlockedShares, 0); + assertGt(balance, 0); + } else if (block.timestamp < unlockingDate) { + assertGt(unlockedShares, 0); + assertGt(balance, 0); + } else { + // We should have unlocked full balance + assertEq(balance, 0); + assertGt(unlockedShares, 0); + } + } else { + assertEq(balance, 0); + } + } + + function assert_unlockedShares() public { + uint256 unlockedShares = vault.unlockedShares(); + uint256 fullBalance = vault.balanceOf(address(vault)) + unlockedShares; + uint256 unlockingDate = vault.fullProfitUnlockDate(); + if ( + unlockingDate != 0 && + vault.profitUnlockingRate() > 0 && + block.timestamp < unlockingDate + ) { + assertLt(unlockedShares, fullBalance); + } else { + assertEq(unlockedShares, fullBalance); + assertEq(vault.balanceOf(address(vault)), 0); + } + } + + function assert_previewMintAndConvertToAssets() public { + assertApproxEq(vault.previewMint(WAD), vault.convertToAssets(WAD), 1); + } + + function assert_previewWithdrawAndConvertToShares() public { + assertApproxEq( + vault.previewWithdraw(WAD), + vault.convertToShares(WAD), + 1 + ); + } + + function assert_balanceAndTotalAssets(bool unreported) public { + if (!unreported) { + assertLe( + vault.totalAssets(), + asset.balanceOf(address(strategy)) + + asset.balanceOf(address(vault)) + ); + } + assertEq(vault.totalIdle(), asset.balanceOf(address(vault))); + } + + function assert_totalDebt(bool unreported) public { + uint256 currentDebt = vault.strategies(address(strategy)).current_debt; + assertEq(vault.totalDebt(), currentDebt); + if (!unreported) { + assertGe(asset.balanceOf(address(strategy)), currentDebt); + } + } +} diff --git a/foundry_tests/utils/LibAddressSet.sol b/foundry_tests/utils/LibAddressSet.sol new file mode 100644 index 00000000..b0b17df1 --- /dev/null +++ b/foundry_tests/utils/LibAddressSet.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8.18; + +struct AddressSet { + address[] addrs; + mapping(address => bool) saved; +} + +library LibAddressSet { + function add(AddressSet storage s, address addr) internal { + if (!s.saved[addr]) { + s.addrs.push(addr); + s.saved[addr] = true; + } + } + + function contains( + AddressSet storage s, + address addr + ) internal view returns (bool) { + return s.saved[addr]; + } + + function count(AddressSet storage s) internal view returns (uint256) { + return s.addrs.length; + } + + function rand( + AddressSet storage s, + uint256 seed + ) internal view returns (address) { + if (s.addrs.length > 0) { + return s.addrs[seed % s.addrs.length]; + } else { + return address(0); + } + } + + function addresses( + AddressSet storage s + ) internal view returns (address[] memory _addrs) { + return s.addrs; + } +} diff --git a/foundry_tests/utils/Setup.sol b/foundry_tests/utils/Setup.sol index c45d1c19..e986786e 100644 --- a/foundry_tests/utils/Setup.sol +++ b/foundry_tests/utils/Setup.sol @@ -10,6 +10,8 @@ import {IVault} from "../../contracts/interfaces/IVault.sol"; import {Roles} from "../../contracts/interfaces/Roles.sol"; import {IVaultFactory} from "../../contracts/interfaces/IVaultFactory.sol"; +import {MockTokenizedStrategy} from "../../contracts/test/mocks/ERC4626/MockTokenizedStrategy.sol"; + import {VyperDeployer} from "./VyperDeployer.sol"; contract Setup is ExtendedTest { @@ -18,11 +20,16 @@ contract Setup is ExtendedTest { IVaultFactory public vaultFactory; VyperDeployer public vyperDeployer; + MockTokenizedStrategy public strategy; + address public daddy = address(69); address public vaultManagement = address(2); + address public keeper = address(32); uint256 public maxFuzzAmount = 1e30; + uint256 public WAD = 1e18; + function setUp() public virtual { vyperDeployer = new VyperDeployer(); @@ -32,6 +39,8 @@ contract Setup is ExtendedTest { vault = IVault(setUpVault()); + strategy = MockTokenizedStrategy(setUpStrategy()); + vm.label(address(vault), "Vault"); vm.label(address(asset), "Asset"); vm.label(address(vaultFactory), "Vault Factory"); @@ -67,9 +76,33 @@ contract Setup is ExtendedTest { // Give the vault manager all the roles _vault.set_role(vaultManagement, Roles.ALL); + vm.prank(daddy); + _vault.set_role(keeper, Roles.REPORTING_MANAGER | Roles.DEBT_MANAGER); + vm.prank(vaultManagement); _vault.set_deposit_limit(type(uint256).max); return _vault; } + + function setUpStrategy() public returns (MockTokenizedStrategy _strategy) { + _strategy = new MockTokenizedStrategy( + address(vaultFactory), + address(asset), + "Test Strategy", + vaultManagement, + keeper + ); + + vm.startPrank(vaultManagement); + + vault.add_strategy(address(_strategy)); + + vault.update_max_debt_for_strategy( + address(_strategy), + type(uint256).max + ); + + vm.stopPrank(); + } } diff --git a/tests/unit/vault/test_shares.py b/tests/unit/vault/test_shares.py index 9ddca2f6..ea0a31fe 100644 --- a/tests/unit/vault/test_shares.py +++ b/tests/unit/vault/test_shares.py @@ -18,7 +18,7 @@ def test_deposit__with_zero_funds__reverts(fish, asset, create_vault): vault = create_vault(asset) amount = 0 - with ape.reverts("cannot mint zero"): + with ape.reverts("cannot deposit zero"): vault.deposit(amount, fish.address, sender=fish) @@ -471,7 +471,7 @@ def create_profit( return event[0].total_fees -def test__mint_shares_with_zero_total_supply_positive_assets( +def test__deposit_shares_with_zero_total_supply_positive_assets( asset, fish_amount, fish, initial_set_up, gov ): amount = fish_amount // 10 @@ -498,3 +498,90 @@ def test__mint_shares_with_zero_total_supply_positive_assets( # shares should be minted at 1:1 assert vault.balanceOf(fish) == amount assert vault.pricePerShare() > (10 ** vault.decimals()) + + +def test__mint_shares_with_zero_total_supply_positive_assets( + asset, fish_amount, fish, initial_set_up, gov +): + amount = fish_amount // 10 + first_profit = fish_amount // 10 + + vault, strategy, _ = initial_set_up(asset, gov, amount, fish) + create_profit(asset, strategy, gov, vault, first_profit) + vault.update_debt(strategy, int(0), sender=gov) + assert ( + vault.totalSupply() > amount + ) # there are more shares than deposits (due to profit unlock) + + # User redeems shares + vault.redeem(vault.balanceOf(fish), fish, fish, sender=fish) + + assert vault.totalSupply() > 0 + + ape.chain.mine(timestamp=ape.chain.pending_timestamp + 14 * 24 * 3600) + + assert vault.totalSupply() == 0 + + vault.mint(amount, fish, sender=fish) + + # shares should be minted at 1:1 + assert vault.balanceOf(fish) == amount + assert vault.pricePerShare() > (10 ** vault.decimals()) + + +def test__deposit_with_zero_total_assets_positive_supply( + asset, fish_amount, fish, initial_set_up, gov +): + amount = fish_amount // 10 + + vault, strategy, _ = initial_set_up(asset, gov, amount, fish) + + # Create a loss + asset.transfer(gov, amount, sender=strategy) + strategy.report(sender=gov) + + assert strategy.convertToAssets(amount) == 0 + + vault.process_report(strategy, sender=gov) + + assert vault.totalAssets() == 0 + assert vault.totalSupply() != 0 + + with ape.reverts("cannot mint zero"): + vault.deposit(amount, fish, sender=fish) + + # shares should not be minted + assert vault.balanceOf(fish) == amount + assert vault.pricePerShare() == 0 + assert vault.convertToShares(amount) == 0 + assert vault.convertToAssets(amount) == 0 + # assert vault.maxDeposit(fish) == 0 + + +def test__mint_with_zero_total_assets_positive_supply( + asset, fish_amount, fish, initial_set_up, gov +): + amount = fish_amount // 10 + + vault, strategy, _ = initial_set_up(asset, gov, amount, fish) + + # Create a loss + asset.transfer(gov, amount, sender=strategy) + strategy.report(sender=gov) + + assert strategy.convertToAssets(amount) == 0 + + vault.process_report(strategy, sender=gov) + + assert vault.totalAssets() == 0 + assert vault.totalSupply() != 0 + + with ape.reverts("cannot deposit zero"): + vault.mint(amount, fish, sender=fish) + + # shares should not be minted + assert vault.balanceOf(fish) == amount + assert vault.pricePerShare() == 0 + assert vault.convertToShares(amount) == 0 + assert vault.convertToAssets(amount) == 0 + # assert vault.maxMint(fish) == 0