From 4096342e21cfbe1f65ec0b84d663824db741977f Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 2 Aug 2024 12:21:03 -0600 Subject: [PATCH 01/20] chore: updgrade ape --- .github/workflows/test.yaml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6818f86a..af44be63 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v1 - - uses: ApeWorX/github-action@v2.0 + - uses: ApeWorX/github-action with: python-version: '3.10' @@ -34,7 +34,7 @@ jobs: - name: Setup node.js uses: actions/setup-node@v1 with: - node-version: '16.x' + node-version: '18.x' - name: Install hardhat run: npm install hardhat diff --git a/requirements.txt b/requirements.txt index f0bc9548..e8482fbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ black==22.3.0 -eth-ape>=0.7.0 +eth-ape>=0.8.0 vyper==0.3.7 \ No newline at end of file From 830ea11419550763af833a2bce99beeb7504cd25 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:39:39 -0600 Subject: [PATCH 02/20] build: allow max uint (#204) * build: allow max uint * fix: lint version * forge install: openzeppelin-contracts v4.9.5 * chore: oz sub module * forge install: tokenized-strategy v3.0.2 * test: fix foundry tests --- .github/workflows/foundry.yaml | 29 +--------------------------- .github/workflows/lint.yaml | 4 ++-- .gitmodules | 7 +++++++ contracts/VaultV3.vy | 18 +++++++++++------ foundry.toml | 4 ++-- foundry_tests/tests/ERC4626Std.t.sol | 18 +++++++++++++++++ lib/openzeppelin-contracts | 1 + lib/tokenized-strategy | 1 + tests/unit/vault/test_erc4626.py | 23 ++++++++++++++++++++++ 9 files changed, 67 insertions(+), 38 deletions(-) create mode 160000 lib/openzeppelin-contracts create mode 160000 lib/tokenized-strategy diff --git a/.github/workflows/foundry.yaml b/.github/workflows/foundry.yaml index 607d7a75..ebc1ede3 100644 --- a/.github/workflows/foundry.yaml +++ b/.github/workflows/foundry.yaml @@ -19,43 +19,16 @@ jobs: - ubuntu-latest architecture: - "x64" - python-version: - - "3.10" - node_version: - - 16 steps: - name: Checkout uses: actions/checkout@v3 with: submodules: recursive - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} - - - name: Install Ape - uses: ApeWorX/github-action@v2.0 - with: - python-version: '3.10' - - - name: install vyper - run: pip install git+https://github.com/vyperlang/vyper - - - name: Compile contracts - # Compile Ape contracts to get dependencies - run: ape compile --force --size - + - name: Install Vyper run: pip install vyper==0.3.7 - - name: Use Node.js ${{ matrix.node_version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node_version }} - - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d513b6ab..9f2c89d4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -28,10 +28,10 @@ jobs: with: fetch-depth: 1 - - name: Set up python 3.8 + - name: Set up python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Set pip cache directory path id: pip-cache-dir-path diff --git a/.gitmodules b/.gitmodules index 665e0dd7..9e1e5be8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,10 @@ [submodule "lib/erc4626-tests"] path = lib/erc4626-tests url = https://github.com/a16z/erc4626-tests +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts + release = v4.9.5 +[submodule "lib/tokenized-strategy"] + path = lib/tokenized-strategy + url = https://github.com/yearn/tokenized-strategy diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 8b51b81e..d8f9e631 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -663,19 +663,25 @@ def _deposit(sender: address, recipient: address, assets: uint256) -> uint256: vault accounting. """ assert self.shutdown == False # dev: shutdown - assert assets <= self._max_deposit(recipient), "exceed deposit limit" + + 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, assets) + self._erc20_safe_transfer_from(self.asset, msg.sender, self, amount) # Record the change in total assets. - self.total_idle += assets + self.total_idle += amount - # Issue the corresponding shares for assets. - shares: uint256 = self._issue_shares_for_amount(assets, recipient) + # 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, assets, shares) + log Deposit(sender, recipient, amount, shares) return shares @internal diff --git a/foundry.toml b/foundry.toml index 047eaac0..864746bc 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,8 +8,8 @@ libs = ['lib'] remappings = [ 'forge-std/=lib/forge-std/src/', 'erc4626-tests/=lib/erc4626-tests/', - "@tokenized-strategy=contracts/.cache/tokenized-strategy/v3.0.2", - '@openzeppelin/contracts=contracts/.cache/openzeppelin/v4.9.5/', + "@tokenized-strategy=lib/tokenized-strategy/src", + '@openzeppelin/=lib/openzeppelin-contracts/', ] fs_permissions = [{ access = "read", path = "./"}] diff --git a/foundry_tests/tests/ERC4626Std.t.sol b/foundry_tests/tests/ERC4626Std.t.sol index 7d7162ef..c3de553a 100644 --- a/foundry_tests/tests/ERC4626Std.t.sol +++ b/foundry_tests/tests/ERC4626Std.t.sol @@ -29,6 +29,24 @@ contract VaultERC4626StdTest is ERC4626Test, Setup { super.test_maxRedeem(init); } + //Avoid special case for deposits of uint256 max + function test_previewDeposit( + Init memory init, + uint assets + ) public override { + if (assets == type(uint256).max) assets -= 1; + super.test_previewDeposit(init, assets); + } + + function test_deposit( + Init memory init, + uint assets, + uint allowance + ) public override { + if (assets == type(uint256).max) assets -= 1; + super.test_deposit(init, assets, allowance); + } + function clamp( Init memory init, uint max diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 00000000..bd325d56 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit bd325d56b4c62c9c5c1aff048c37c6bb18ac0290 diff --git a/lib/tokenized-strategy b/lib/tokenized-strategy new file mode 160000 index 00000000..0d90dee1 --- /dev/null +++ b/lib/tokenized-strategy @@ -0,0 +1 @@ +Subproject commit 0d90dee170d53a0e04af3ff41d2f7a4f3ac395bd diff --git a/tests/unit/vault/test_erc4626.py b/tests/unit/vault/test_erc4626.py index 4d4f6d0f..9504fedd 100644 --- a/tests/unit/vault/test_erc4626.py +++ b/tests/unit/vault/test_erc4626.py @@ -785,6 +785,29 @@ def test_max_redeem__with_withdraw_limit_module( assert vault.maxRedeem(bunny.address) == 0 +def test_deposit__with_max_uint( + asset, fish, fish_amount, gov, create_vault, deploy_limit_module, user_deposit +): + vault = create_vault(asset) + assets = fish_amount + + assert asset.balanceOf(fish) == fish_amount + + asset.approve(vault.address, assets, sender=fish) + + # Should go through now + tx = vault.deposit(MAX_INT, fish.address, sender=fish) + + event = list(tx.decode_logs(vault.Deposit))[0] + + assert event.assets == assets + assert event.shares == assets + assert event.owner == fish + assert event.sender == fish + assert vault.balanceOf(fish.address) == assets + assert asset.balanceOf(vault.address) == assets + + def test_deposit__with_deposit_limit_module( asset, fish, fish_amount, gov, create_vault, deploy_limit_module, user_deposit ): From 2add8bdc3d14a3347e820b2821d8b1aed14760a5 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 6 Sep 2024 12:20:59 -0600 Subject: [PATCH 03/20] chore: bump version --- contracts/VaultFactory.vy | 2 +- contracts/VaultV3.vy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/VaultFactory.vy b/contracts/VaultFactory.vy index 12c88762..41530749 100644 --- a/contracts/VaultFactory.vy +++ b/contracts/VaultFactory.vy @@ -73,7 +73,7 @@ struct PFConfig: fee_recipient: address # Identifier for this version of the vault. -API_VERSION: constant(String[28]) = "3.0.2" +API_VERSION: constant(String[28]) = "3.0.3" # The max amount the protocol fee can be set to. MAX_FEE_BPS: constant(uint16) = 5_000 # 50% diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index d8f9e631..2aa72454 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -170,7 +170,7 @@ MAX_BPS: constant(uint256) = 10_000 # Extended for profit locking calculations. MAX_BPS_EXTENDED: constant(uint256) = 1_000_000_000_000 # The version of this vault. -API_VERSION: constant(String[28]) = "3.0.2" +API_VERSION: constant(String[28]) = "3.0.3" # ENUMS # # Each permissioned function has its own Role. From 4ab9f22365d8c08ce107688d50a7e28501cb3cb9 Mon Sep 17 00:00:00 2001 From: FP <83050944+fp-crypto@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:28:15 -0700 Subject: [PATCH 04/20] fix: workflow (#207) --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index af44be63..3fc843a4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,4 +1,4 @@ -name: Test +name: Ape tests on: push: @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v1 - - uses: ApeWorX/github-action + - uses: ApeWorX/github-action@main with: python-version: '3.10' From ffc658355812ba9fc489fd32c383415a3e00aebb Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 9 Sep 2024 20:18:40 -0600 Subject: [PATCH 05/20] chore: update wf version --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3fc843a4..110482c4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v1 - - uses: ApeWorX/github-action@main + - uses: ApeWorX/github-action@v3.0 with: python-version: '3.10' From 48a85702a440d2e213ef365fa3af79f9e76d1d5d Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 11 Sep 2024 08:12:58 -0600 Subject: [PATCH 06/20] chore: add statemind report --- audits/Yearn V3 report Statemind.pdf | Bin 0 -> 168742 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 audits/Yearn V3 report Statemind.pdf diff --git a/audits/Yearn V3 report Statemind.pdf b/audits/Yearn V3 report Statemind.pdf new file mode 100644 index 0000000000000000000000000000000000000000..785253af8c03a2f14b6157533f4ea15b867e509b GIT binary patch literal 168742 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%0D<57(Zg)f670;-n7_vW)~lw0@+-90R;eg)_8x@ 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 Date: Wed, 11 Sep 2024 10:58:27 -0600 Subject: [PATCH 07/20] feat: update name and symbol (#206) * feat: set name and symbol * chore: spech --- TECH_SPEC.md | 2 ++ contracts/VaultV3.vy | 21 ++++++++++++ tests/unit/vault/test_role_base_access.py | 40 +++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/TECH_SPEC.md b/TECH_SPEC.md index a6d8b008..418c5560 100644 --- a/TECH_SPEC.md +++ b/TECH_SPEC.md @@ -45,6 +45,8 @@ When deploying a new vault, it requires the following parameters: - role_manager: account that can assign and revoke Roles - profit_max_unlock_time: max amount of time profit will be locked before being distributed +All deployment variables besides the `asset` can be updated post deployment. + ## Normal Operation ### Deposits / Mints diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 2aa72454..6e04b345 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -1344,6 +1344,27 @@ def _process_report(strategy: address) -> (uint256, uint256): return (gain, loss) # SETTERS # + +@external +def setName(name: String[64]): + """ + @notice Change the vault name. + @dev Can only be called by the Role Manager. + @param name The new name for the vault. + """ + assert msg.sender == self.role_manager, "not allowed" + self.name = name + +@external +def setSymbol(symbol: String[32]): + """ + @notice Change the vault name. + @dev Can only be called by the Role Manager. + @param symbol The new name for the vault. + """ + assert msg.sender == self.role_manager, "not allowed" + self.symbol = symbol + @external def set_accountant(new_accountant: address): """ diff --git a/tests/unit/vault/test_role_base_access.py b/tests/unit/vault/test_role_base_access.py index e04da9a3..ac9000f7 100644 --- a/tests/unit/vault/test_role_base_access.py +++ b/tests/unit/vault/test_role_base_access.py @@ -684,3 +684,43 @@ def test__remove_role__wont_add(gov, vault, bunny, strategy): with ape.reverts("not allowed"): vault.add_strategy(strategy, sender=bunny) + + +def test__set_name(gov, vault, bunny): + name = vault.name() + new_name = "New Vault Name" + + with ape.reverts("not allowed"): + vault.setName(new_name, sender=bunny) + + vault.set_role(bunny, ROLES.ALL, sender=gov) + + with ape.reverts("not allowed"): + vault.setName(new_name, sender=bunny) + + assert vault.name() != new_name + + vault.setName(new_name, sender=gov) + + assert vault.name() == new_name + assert vault.name() != name + + +def test__set_symbol(gov, vault, bunny): + symbol = vault.name() + new_symbol = "New Vault symbol" + + with ape.reverts("not allowed"): + vault.setSymbol(new_symbol, sender=bunny) + + vault.set_role(bunny, ROLES.ALL, sender=gov) + + with ape.reverts("not allowed"): + vault.setSymbol(new_symbol, sender=bunny) + + assert vault.symbol() != new_symbol + + vault.setSymbol(new_symbol, sender=gov) + + assert vault.symbol() == new_symbol + assert vault.symbol() != symbol From efcca261a958923bda402e7b2e494f81d5f47375 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Wed, 11 Sep 2024 10:43:04 -0600 Subject: [PATCH 08/20] test: fix emergency --- tests/unit/vault/test_emergency_shutdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/vault/test_emergency_shutdown.py b/tests/unit/vault/test_emergency_shutdown.py index 2296438e..593cfe58 100644 --- a/tests/unit/vault/test_emergency_shutdown.py +++ b/tests/unit/vault/test_emergency_shutdown.py @@ -26,7 +26,7 @@ def test_shutdown_gives_debt_manager_role(gov, panda, vault): vault.set_role(panda.address, ROLES.EMERGENCY_MANAGER, sender=gov) assert ROLES.DEBT_MANAGER not in ROLES(vault.roles(panda)) vault.shutdown_vault(sender=panda) - assert ROLES.DEBT_MANAGER in ROLES(vault.roles(panda)) + assert ROLES.DEBT_MANAGER | ROLES.EMERGENCY_MANAGER == vault.roles(panda) def test_shutdown__increase_deposit_limit__reverts( From 5bd103442ef4fc1f1147b5f41195ac2a71123181 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:05:46 -0600 Subject: [PATCH 09/20] fix: test workflow (#208) * fix: config override * chore: manual setup * fix: requirements * fix: ape version * chore: rebase --- .github/workflows/test.yaml | 9 ++++++--- ape-config.yaml | 6 ++---- requirements.txt | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 110482c4..5fb93b6c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,12 +20,15 @@ jobs: steps: - uses: actions/checkout@v1 - - uses: ApeWorX/github-action@v3.0 with: python-version: '3.10' - - name: install vyper - run: pip install git+https://github.com/vyperlang/vyper + - name: install requirements + run: python3 -m pip install -r requirements.txt + + - name: install plugins + run: ape plugins install . + - name: Compile contracts # TODO: Force recompiles until ape compile caching is fixed run: ape compile --force --size diff --git a/ape-config.yaml b/ape-config.yaml index 3be01a80..f2fad8ad 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -13,13 +13,11 @@ dependencies: - name: tokenized-strategy github: yearn/tokenized-strategy ref: v3.0.2 - contracts_folder: src + config_override: + contracts_folder: src solidity: version: 0.8.18 - import_remapping: - - "@openzeppelin/contracts=openzeppelin/v4.9.5" - - "@tokenized-strategy=tokenized-strategy/v3.0.2" ethereum: local: diff --git a/requirements.txt b/requirements.txt index e8482fbe..5edc3060 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ black==22.3.0 -eth-ape>=0.8.0 -vyper==0.3.7 \ No newline at end of file +eth-ape==0.8.10 +vyper==0.3.7 +eth-typing==3.5.2 \ No newline at end of file From cbc66dcf7e07dcd3ba4836a7c4dd4c268163794f Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:16:49 -0600 Subject: [PATCH 10/20] chore: dont double pull from storage (#212) --- contracts/VaultV3.vy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 6e04b345..4bf5ef85 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -602,16 +602,17 @@ def _max_withdraw( # Can't use an invalid strategy. assert self.strategies[strategy].activation != 0, "inactive strategy" + current_debt: uint256 = self.strategies[strategy].current_debt # Get the maximum amount the vault would withdraw from the strategy. to_withdraw: uint256 = min( # What we still need for the full withdraw. max_assets - have, # The current debt the strategy has. - self.strategies[strategy].current_debt + current_debt ) # Get any unrealised loss for the strategy. - unrealised_loss: uint256 = self._assess_share_of_unrealised_losses(strategy, to_withdraw) + unrealised_loss: uint256 = self._assess_share_of_unrealised_losses(strategy, current_debt, to_withdraw) # See if any limit is enforced by the strategy. strategy_limit: uint256 = IStrategy(strategy).convertToAssets( @@ -711,7 +712,7 @@ def _mint(sender: address, recipient: address, shares: uint256) -> uint256: @view @internal -def _assess_share_of_unrealised_losses(strategy: address, assets_needed: uint256) -> uint256: +def _assess_share_of_unrealised_losses(strategy: address, strategy_current_debt: uint256, assets_needed: uint256) -> uint256: """ Returns the share of losses that a user would take if withdrawing from this strategy This accounts for losses that have been realized at the strategy level but not yet @@ -720,8 +721,6 @@ def _assess_share_of_unrealised_losses(strategy: address, assets_needed: uint256 e.g. if the strategy has unrealised losses for 10% of its current debt and the user wants to withdraw 1_000 tokens, the losses that they will take is 100 token """ - # Minimum of how much debt the debt should be worth. - strategy_current_debt: uint256 = self.strategies[strategy].current_debt # The actual amount that the debt is currently worth. vault_shares: uint256 = IStrategy(strategy).balanceOf(self) strategy_assets: uint256 = IStrategy(strategy).convertToAssets(vault_shares) @@ -850,7 +849,7 @@ def _redeem( # NOTE: strategies need to manage the fact that realising part of the loss can # mean the realisation of 100% of the loss!! (i.e. if for withdrawing 10% of the # strategy it needs to unwind the whole position, generated losses might be bigger) - unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw) + unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, current_debt, assets_to_withdraw) if unrealised_losses_share > 0: # If max withdraw is limiting the amount to pull, we need to adjust the portion of # the unrealized loss the user should take. @@ -1061,7 +1060,7 @@ def _update_debt(strategy: address, target_debt: uint256, max_loss: uint256) -> assets_to_withdraw = withdrawable # If there are unrealised losses we don't let the vault reduce its debt until there is a new report - unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw) + unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, current_debt, assets_to_withdraw) assert unrealised_losses_share == 0, "strategy has unrealised losses" # Cache for repeated use. @@ -2114,9 +2113,10 @@ def assess_share_of_unrealised_losses(strategy: address, assets_needed: uint256) @param assets_needed The amount of assets needed to be withdrawn. @return The share of unrealised losses that the strategy has. """ - assert self.strategies[strategy].current_debt >= assets_needed + current_debt: uint256 = self.strategies[strategy].current_debt + assert current_debt >= assets_needed - return self._assess_share_of_unrealised_losses(strategy, assets_needed) + return self._assess_share_of_unrealised_losses(strategy, current_debt, assets_needed) ## Profit locking getter functions ## From d2c57b2d4def34884477069ea5ed1ecd9249c298 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:45:08 -0600 Subject: [PATCH 11/20] feat: report on self (#205) * feat: report on self * chore: comment * chore: add back --- contracts/VaultV3.vy | 57 +++++++++++++++-------- tests/unit/vault/test_vault_accounting.py | 36 ++++++++++++++ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 4bf5ef85..f601507a 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -1167,19 +1167,33 @@ def _process_report(strategy: address) -> (uint256, uint256): Any applicable fees are charged and distributed during the report as well to the specified recipients. - """ - # Make sure we have a valid strategy. - assert self.strategies[strategy].activation != 0, "inactive strategy" - # Vault assesses profits using 4626 compliant interface. - # NOTE: It is important that a strategies `convertToAssets` implementation - # cannot be manipulated or else the vault could report incorrect gains/losses. - strategy_shares: uint256 = IStrategy(strategy).balanceOf(self) - # How much the vaults position is worth. - total_assets: uint256 = IStrategy(strategy).convertToAssets(strategy_shares) - # How much the vault had deposited to the strategy. - current_debt: uint256 = self.strategies[strategy].current_debt + Can update the vaults `totalIdle` to account for any airdropped tokens by + passing the vaults address in as the parameter. + """ + # Cache `asset` for repeated use. + _asset: address = self.asset + total_assets: uint256 = 0 + current_debt: uint256 = 0 + + if strategy != self: + # Make sure we have a valid strategy. + assert self.strategies[strategy].activation != 0, "inactive strategy" + + # Vault assesses profits using 4626 compliant interface. + # NOTE: It is important that a strategies `convertToAssets` implementation + # cannot be manipulated or else the vault could report incorrect gains/losses. + strategy_shares: uint256 = IStrategy(strategy).balanceOf(self) + # How much the vaults position is worth. + total_assets = IStrategy(strategy).convertToAssets(strategy_shares) + # How much the vault had deposited to the strategy. + current_debt = self.strategies[strategy].current_debt + else: + # Accrue any airdropped `asset` into `total_idle` + total_assets = ERC20(_asset).balanceOf(self) + current_debt = self.total_idle + gain: uint256 = 0 loss: uint256 = 0 @@ -1192,9 +1206,6 @@ def _process_report(strategy: address) -> (uint256, uint256): else: # We have a loss. loss = unsafe_sub(current_debt, total_assets) - - # Cache `asset` for repeated use. - _asset: address = self.asset ### Asses Fees and Refunds ### @@ -1281,15 +1292,21 @@ def _process_report(strategy: address) -> (uint256, uint256): if gain > 0: # NOTE: this will increase total_assets current_debt = unsafe_add(current_debt, gain) - self.strategies[strategy].current_debt = current_debt - self.total_debt += gain - + if strategy != self: + self.strategies[strategy].current_debt = current_debt + self.total_debt += gain + else: + self.total_idle += gain + # Or record any reported loss elif loss > 0: current_debt = unsafe_sub(current_debt, loss) - self.strategies[strategy].current_debt = current_debt - self.total_debt -= loss - + if strategy != self: + self.strategies[strategy].current_debt = current_debt + self.total_debt -= loss + else: + self.total_idle -= loss + # Issue shares for fees that were calculated above if applicable. if total_fees_shares > 0: # Accountant fees are (total_fees - protocol_fees). diff --git a/tests/unit/vault/test_vault_accounting.py b/tests/unit/vault/test_vault_accounting.py index 0a9b9a46..85708813 100644 --- a/tests/unit/vault/test_vault_accounting.py +++ b/tests/unit/vault/test_vault_accounting.py @@ -14,3 +14,39 @@ def test_vault_airdrop_do_not_increase( price_per_share = vault.pricePerShare() airdrop_asset(gov, asset, vault, int(vault_balance / 10)) assert vault.pricePerShare() == price_per_share + + +def test_vault_airdrop_do_not_increase_report_records_it( + gov, asset, vault, mint_and_deposit_into_vault, airdrop_asset +): + mint_and_deposit_into_vault(vault, gov) + vault_balance = asset.balanceOf(vault) + assert vault_balance != 0 + # vault. + # aidrop to vault + price_per_share = vault.pricePerShare() + + to_airdrop = int(vault_balance / 10) + airdrop_asset(gov, asset, vault, to_airdrop) + + assert vault.pricePerShare() == price_per_share + assert vault.totalIdle() == vault_balance + assert asset.balanceOf(vault) == vault_balance + to_airdrop + + tx = vault.process_report(vault.address, sender=gov) + + event = list(tx.decode_logs(vault.StrategyReported))[0] + + assert event.strategy == vault.address + assert event.gain == to_airdrop + assert event.loss == 0 + assert event.current_debt == vault_balance + to_airdrop + assert event.total_fees == 0 + + # Profit is locked + assert vault.pricePerShare() == price_per_share + assert vault.totalIdle() == vault_balance + to_airdrop + assert asset.balanceOf(vault) == vault_balance + to_airdrop + + chain.pending_timestamp = chain.pending_timestamp + vault.profitMaxUnlockTime() - 1 + chain.mine(timestamp=chain.pending_timestamp) From 5bc4ad566be16efe8df7f28049f585ef42fffaaf Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:32:35 -0600 Subject: [PATCH 12/20] build: auto allocate (#203) * build: auto allocate * build: dont revert debt increase * chore: remove decrease revert * chore: update spech * chore: spech * chore: comments * fix: event --- TECH_SPEC.md | 13 +- contracts/VaultV3.vy | 49 ++- tests/unit/vault/test_auto_allocate.py | 437 +++++++++++++++++++++++ tests/unit/vault/test_debt_management.py | 34 +- 4 files changed, 514 insertions(+), 19 deletions(-) create mode 100644 tests/unit/vault/test_auto_allocate.py diff --git a/TECH_SPEC.md b/TECH_SPEC.md index 418c5560..cafbf216 100644 --- a/TECH_SPEC.md +++ b/TECH_SPEC.md @@ -129,11 +129,7 @@ Every role can be filled by an EOA, multi-sig or other smart contracts. Each rol The account that manages roles is a single account, set in `role_manager`. -This role_manager can be an EOA, a multi-sig or a Governance Module that relays calls. - -The vault comes with the ability to "open" every role. Meaning that any function that requires the caller to hold that role would be come permsissionless. - -The vault imposes no restrictions on the role managers ability to open or close any role. **But this should be done with extreme care as most of the roles are not meant to be opened and can lead to loss of funds if done incorrectly**. +This role_manager can be an EOA, a multi-sig or a Governance contract that relays calls. ### Strategy Management This responsibility is taken by callers with ADD_STRATEGY_MANAGER, REVOKE_STRATEGY_MANAGER and FORCE_REVOKE_MANAGER roles @@ -173,6 +169,13 @@ The vault checks that the `minimumTotalIdle` parameter is respected (i.e. there' If the strategy has more debt than the max_debt, the vault will request the funds back. These funds may be locked in the strategy, which will result in the strategy returning less funds than requested by the vault. +#### Auto Allocations +The DEBT_MANAGER can set the vaults `auto_allocate` flag to `True`. + +This will cause every deposit or mint call to end by the vault pushing as much debt as possible to the first strategy in the queue. + +NOTE: Not having at least 1 strategy in the `default_queue` with the `auto_allocate` flag will cause all deposits to revert. + #### Setting maximum debt for a specific strategy The MAX_DEBT_MANAGER can set the maximum amount of tokens the vault will allow a strategy to owe at any moment in time. diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index f601507a..90665969 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -130,6 +130,9 @@ event UpdateDefaultQueue: event UpdateUseDefaultQueue: use_default_queue: bool +event UpdateAutoAllocate: + auto_allocate: bool + event UpdatedMaxDebtForStrategy: sender: indexed(address) strategy: indexed(address) @@ -214,6 +217,8 @@ strategies: public(HashMap[address, StrategyParams]) default_queue: public(DynArray[address, MAX_QUEUE]) # Should the vault use the default_queue regardless whats passed in. use_default_queue: public(bool) +# Should the vault automatically allocate funds to the first strategy in queue. +auto_allocate: public(bool) ### ACCOUNTING ### # ERC20 - amount of shares per account @@ -683,6 +688,10 @@ def _deposit(sender: address, recipient: address, assets: uint256) -> uint256: 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 @@ -708,6 +717,10 @@ def _mint(sender: address, recipient: address, shares: uint256) -> uint256: self._issue_shares(shares, recipient) log Deposit(sender, recipient, assets, shares) + + if self.auto_allocate: + self._update_debt(self.default_queue[0], max_value(uint256), 0) + return assets @view @@ -1053,12 +1066,14 @@ def _update_debt(strategy: address, target_debt: uint256, max_loss: uint256) -> withdrawable: uint256 = IStrategy(strategy).convertToAssets( IStrategy(strategy).maxRedeem(self) ) - assert withdrawable != 0, "nothing to withdraw" # If insufficient withdrawable, withdraw what we can. if withdrawable < assets_to_withdraw: assets_to_withdraw = withdrawable + if assets_to_withdraw == 0: + return current_debt + # If there are unrealised losses we don't let the vault reduce its debt until there is a new report unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, current_debt, assets_to_withdraw) assert unrealised_losses_share == 0, "strategy has unrealised losses" @@ -1093,12 +1108,18 @@ def _update_debt(strategy: address, target_debt: uint256, max_loss: uint256) -> else: # We are increasing the strategies debt - # Revert if target_debt cannot be achieved due to configured max_debt for given strategy - assert new_debt <= self.strategies[strategy].max_debt, "target debt higher than max debt" + # Respect the maximum amount allowed. + max_debt: uint256 = self.strategies[strategy].max_debt + if new_debt > max_debt: + new_debt = max_debt + # Possible for current to be greater than max from reports. + if new_debt < current_debt: + return current_debt # Vault is increasing debt with the strategy by sending more funds. max_deposit: uint256 = IStrategy(strategy).maxDeposit(self) - assert max_deposit != 0, "nothing to deposit" + if max_deposit == 0: + return current_debt # Deposit the difference between desired and current. assets_to_deposit: uint256 = new_debt - current_debt @@ -1110,7 +1131,9 @@ def _update_debt(strategy: address, target_debt: uint256, max_loss: uint256) -> minimum_total_idle: uint256 = self.minimum_total_idle total_idle: uint256 = self.total_idle - assert total_idle > minimum_total_idle, "no funds to deposit" + if total_idle <= minimum_total_idle: + return current_debt + available_idle: uint256 = unsafe_sub(total_idle, minimum_total_idle) # If insufficient funds to deposit, transfer only what is free. @@ -1425,6 +1448,21 @@ def set_use_default_queue(use_default_queue: bool): log UpdateUseDefaultQueue(use_default_queue) +@external +def set_auto_allocate(auto_allocate: bool): + """ + @notice Set new value for `auto_allocate` + @dev If `True` every {deposit} and {mint} call will + try and allocate the deposited amount to the strategy + at position 0 of the `default_queue` atomically. + NOTE: An empty `default_queue` will cause deposits to fail. + @param auto_allocate new value. + """ + self._enforce_role(msg.sender, Roles.DEBT_MANAGER) + self.auto_allocate = auto_allocate + + log UpdateAutoAllocate(auto_allocate) + @external def set_deposit_limit(deposit_limit: uint256, override: bool = False): """ @@ -1766,6 +1804,7 @@ def update_debt( ) -> uint256: """ @notice Update the debt for a strategy. + @dev Pass max uint256 to allocate as much idle as possible. @param strategy The strategy to update the debt for. @param target_debt The target debt for the strategy. @param max_loss Optional to check realized losses on debt decreases. diff --git a/tests/unit/vault/test_auto_allocate.py b/tests/unit/vault/test_auto_allocate.py new file mode 100644 index 00000000..39b6c1dd --- /dev/null +++ b/tests/unit/vault/test_auto_allocate.py @@ -0,0 +1,437 @@ +import ape +import pytest +from utils.constants import DAY + + +def test_deposit__auto_update_debt( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, assets * 2, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == assets + + assert vault.totalAssets() == assets + assert vault.totalIdle() == 0 + assert vault.totalDebt() == assets + assert strategy.totalAssets() == assets + assert strategy.balanceOf(vault) == assets + assert vault.strategies(strategy)["current_debt"] == assets + assert vault.balanceOf(fish) == assets + + +def test_mint__auto_update_debt( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, assets * 2, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.mint(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == assets + + assert vault.totalAssets() == assets + assert vault.totalIdle() == 0 + assert vault.totalDebt() == assets + assert strategy.totalAssets() == assets + assert strategy.balanceOf(vault) == assets + assert vault.strategies(strategy)["current_debt"] == assets + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__max_debt( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + max_debt = assets // 10 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, max_debt, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] < assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == max_debt + + assert vault.totalAssets() == assets + assert vault.totalIdle() == assets - max_debt + assert vault.totalDebt() == max_debt + assert strategy.totalAssets() == max_debt + assert strategy.balanceOf(vault) == max_debt + assert vault.strategies(strategy)["current_debt"] == max_debt + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__max_deposit( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + max_deposit = assets // 10 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=gov) + strategy.setMaxDebt(max_deposit, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) == max_deposit + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == max_deposit + + assert vault.totalAssets() == assets + assert vault.totalIdle() == assets - max_deposit + assert vault.totalDebt() == max_deposit + assert strategy.totalAssets() == max_deposit + assert strategy.balanceOf(vault) == max_deposit + assert vault.strategies(strategy)["current_debt"] == max_deposit + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__max_deposit_zero( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + max_deposit = 0 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=gov) + strategy.setMaxDebt(max_deposit, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) == max_deposit + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 0 + + assert vault.totalAssets() == assets + assert vault.totalIdle() == assets - max_deposit + assert vault.totalDebt() == max_deposit + assert strategy.totalAssets() == max_deposit + assert strategy.balanceOf(vault) == max_deposit + assert vault.strategies(strategy)["current_debt"] == max_deposit + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__min_idle( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + min_idle = assets // 10 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=gov) + vault.set_minimum_total_idle(min_idle, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == min_idle + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == assets - min_idle + + assert vault.totalAssets() == assets + assert vault.totalIdle() == min_idle + assert vault.totalDebt() == assets - min_idle + assert strategy.totalAssets() == assets - min_idle + assert strategy.balanceOf(vault) == assets - min_idle + assert vault.strategies(strategy)["current_debt"] == assets - min_idle + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__min_idle( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + min_idle = assets // 10 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=gov) + vault.set_minimum_total_idle(min_idle, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == min_idle + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == assets - min_idle + + assert vault.totalAssets() == assets + assert vault.totalIdle() == min_idle + assert vault.totalDebt() == assets - min_idle + assert strategy.totalAssets() == assets - min_idle + assert strategy.balanceOf(vault) == assets - min_idle + assert vault.strategies(strategy)["current_debt"] == assets - min_idle + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__min_idle_not_met( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount + min_idle = assets * 2 + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, 2**256 - 1, sender=gov) + vault.set_minimum_total_idle(min_idle, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] > assets + assert vault.minimum_total_idle() == min_idle + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 0 + + assert vault.totalAssets() == assets + assert vault.totalIdle() == assets + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == assets + + +def test_deposit__auto_update_debt__current_debt_more_than_max_debt( + asset, fish, fish_amount, gov, vault, strategy, user_deposit +): + assets = fish_amount // 2 + max_debt = assets + + assert vault.auto_allocate() == False + + vault.set_auto_allocate(True, sender=gov) + vault.update_max_debt_for_strategy(strategy, max_debt, sender=gov) + + assert vault.auto_allocate() + assert strategy.maxDeposit(vault) > assets + assert vault.strategies(strategy)["max_debt"] == assets + assert vault.minimum_total_idle() == 0 + + assert vault.totalAssets() == 0 + assert vault.totalIdle() == 0 + assert vault.totalDebt() == 0 + assert strategy.totalAssets() == 0 + assert strategy.balanceOf(vault) == 0 + assert vault.strategies(strategy)["current_debt"] == 0 + assert vault.balanceOf(fish) == 0 + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 1 + event = event[0] + + assert event.strategy == strategy + assert event.current_debt == 0 + assert event.new_debt == max_debt + + assert vault.totalAssets() == assets + assert vault.totalIdle() == 0 + assert vault.totalDebt() == max_debt + assert strategy.totalAssets() == max_debt + assert strategy.balanceOf(vault) == max_debt + assert vault.strategies(strategy)["current_debt"] == max_debt + assert vault.balanceOf(fish) == assets + + profit = assets // 10 + # Report profit to go over max debt + asset.mint(strategy, profit, sender=gov) + strategy.report(sender=gov) + vault.process_report(strategy, sender=gov) + + assert ( + vault.strategies(strategy)["current_debt"] + > vault.strategies(strategy)["max_debt"] + ) + + asset.approve(vault, assets, sender=fish) + + tx = vault.deposit(assets, fish, sender=fish) + + event = tx.decode_logs(vault.DebtUpdated) + + assert len(event) == 0 + + assert vault.totalAssets() == assets * 2 + profit + assert vault.totalIdle() == assets + assert vault.totalDebt() == max_debt + profit + assert strategy.totalAssets() == max_debt + profit + assert strategy.balanceOf(vault) == max_debt + assert vault.strategies(strategy)["current_debt"] == max_debt + profit + assert vault.balanceOf(fish) > assets diff --git a/tests/unit/vault/test_debt_management.py b/tests/unit/vault/test_debt_management.py index 37eccf0f..4679bfc5 100644 --- a/tests/unit/vault/test_debt_management.py +++ b/tests/unit/vault/test_debt_management.py @@ -33,7 +33,7 @@ def test_update_debt__without_permission__reverts(gov, vault, asset, strategy, b vault.update_debt(strategy.address, new_debt, sender=bunny) -def test_update_debt__with_strategy_max_debt_less_than_new_debt__reverts( +def test_update_debt__with_strategy_max_debt_less_than_new_debt( gov, asset, vault, strategy ): vault_balance = asset.balanceOf(vault) @@ -41,8 +41,22 @@ def test_update_debt__with_strategy_max_debt_less_than_new_debt__reverts( vault.update_max_debt_for_strategy(strategy.address, new_debt, sender=gov) - with ape.reverts("target debt higher than max debt"): - vault.update_debt(strategy.address, new_debt + 1, sender=gov) + tx = vault.update_debt(strategy.address, new_debt + 10, sender=gov) + + assert tx.return_value == new_debt + + event = list(tx.decode_logs(vault.DebtUpdated)) + + assert len(event) == 1 + assert event[0].strategy == strategy.address + assert event[0].current_debt == 0 + assert event[0].new_debt == new_debt + + assert vault.strategies(strategy.address).current_debt == new_debt + assert asset.balanceOf(strategy) == new_debt + assert asset.balanceOf(vault) == vault_balance - new_debt + assert vault.totalIdle() == vault_balance - new_debt + assert vault.totalDebt() == new_debt def test_update_debt__with_current_debt_less_than_new_debt(gov, asset, vault, strategy): @@ -82,7 +96,7 @@ def test_update_debt__with_current_debt_equal_to_new_debt__reverts( vault.update_debt(strategy.address, new_debt, sender=gov) -def test_update_debt__with_current_debt_greater_than_new_debt_and_zero_withdrawable__reverts( +def test_update_debt__with_current_debt_greater_than_new_debt_and_zero_withdrawable( gov, asset, vault, locked_strategy, add_debt_to_strategy ): vault_balance = asset.balanceOf(vault) @@ -95,8 +109,9 @@ def test_update_debt__with_current_debt_greater_than_new_debt_and_zero_withdrawa # reduce debt in strategy vault.update_max_debt_for_strategy(locked_strategy.address, new_debt, sender=gov) - with ape.reverts("nothing to withdraw"): - vault.update_debt(locked_strategy.address, new_debt, sender=gov) + tx = vault.update_debt(locked_strategy.address, new_debt, sender=gov) + + assert tx.return_value == current_debt def test_update_debt__with_current_debt_greater_than_new_debt_and_strategy_has_losses__reverts( @@ -269,7 +284,7 @@ def test_update_debt__with_current_debt_less_than_new_debt_and_minimum_total_idl assert vault.totalIdle() > vault.minimum_total_idle() -def test_update_debt__with_current_debt_less_than_new_debt_and_total_idle_lower_than_minimum_total_idle__revert( +def test_update_debt__with_current_debt_less_than_new_debt_and_total_idle_lower_than_minimum_total_idle( gov, asset, vault, strategy ): """ @@ -287,8 +302,9 @@ def test_update_debt__with_current_debt_less_than_new_debt_and_total_idle_lower_ # increase debt in strategy vault.update_max_debt_for_strategy(strategy.address, new_debt, sender=gov) - with ape.reverts("no funds to deposit"): - vault.update_debt(strategy.address, new_debt, sender=gov) + tx = vault.update_debt(strategy.address, new_debt, sender=gov) + + assert tx.return_value == 0 def test_update_debt__with_current_debt_less_than_new_debt_and_minimum_total_idle_reducing_new_debt( From 541d75cb40f9cb251b61976a5b16e9f8a03e3148 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Mon, 16 Sep 2024 21:38:19 -0600 Subject: [PATCH 13/20] fix: comments Co-authored-by: spalen0 <116267321+spalen0@users.noreply.github.com> --- contracts/VaultV3.vy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index 90665969..0c32b764 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -1397,7 +1397,7 @@ def setName(name: String[64]): @external def setSymbol(symbol: String[32]): """ - @notice Change the vault name. + @notice Change the vault symbol. @dev Can only be called by the Role Manager. @param symbol The new name for the vault. """ From 1c68d3e6c98afa74931667397b8c9cba0de7c341 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:21:54 -0600 Subject: [PATCH 14/20] feat: pack pf config (#211) * feat: pack pf config * chore: remove event * chore: formatting * test: interface updates * chore: comments --- contracts/VaultFactory.vy | 128 +++++++++++++----- .../unit/factory/test_protocol_fees_config.py | 61 ++++----- tests/unit/vault/test_protocol_fees.py | 4 +- 3 files changed, 117 insertions(+), 76 deletions(-) diff --git a/contracts/VaultFactory.vy b/contracts/VaultFactory.vy index 41530749..21755a3c 100644 --- a/contracts/VaultFactory.vy +++ b/contracts/VaultFactory.vy @@ -66,11 +66,6 @@ event UpdateGovernance: event NewPendingGovernance: pending_governance: indexed(address) -struct PFConfig: - # Percent of protocol's split of fees in Basis Points. - fee_bps: uint16 - # Address the protocol fees get paid to. - fee_recipient: address # Identifier for this version of the vault. API_VERSION: constant(String[28]) = "3.0.3" @@ -78,6 +73,9 @@ API_VERSION: constant(String[28]) = "3.0.3" # The max amount the protocol fee can be set to. MAX_FEE_BPS: constant(uint16) = 5_000 # 50% +# Mask used to unpack the protocol fee bps. +FEE_BPS_MASK: constant(uint256) = 2**16-1 + # The address that all newly deployed vaults are based from. VAULT_ORIGINAL: immutable(address) @@ -92,12 +90,13 @@ pending_governance: public(address) # Name for identification. name: public(String[64]) +# Protocol Fee Data is packed into a uint256 slot +# 72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag + # The default config for assessing protocol fees. -default_protocol_fee_config: public(PFConfig) +default_protocol_fee_data: uint256 # Custom fee to charge for a specific vault or strategy. -custom_protocol_fee: public(HashMap[address, uint16]) -# Represents if a custom protocol fee should be used. -use_custom_protocol_fee: public(HashMap[address, bool]) +custom_protocol_fee_data: HashMap[address, uint256] @external def __init__(name: String[64], vault_original: address, governance: address): @@ -163,24 +162,73 @@ def apiVersion() -> String[28]: @view @external -def protocol_fee_config(vault: address = msg.sender) -> PFConfig: +def protocol_fee_config(vault: address = msg.sender) -> (uint16, address): """ @notice Called during vault and strategy reports to retrieve the protocol fee to charge and address to receive the fees. @param vault Address of the vault that would be reporting. - @return The protocol fee config for the msg sender. + @return Fee in bps + @return Address of fee recipient """ # If there is a custom protocol fee set we return it. - if self.use_custom_protocol_fee[vault]: + config_data: uint256 = self.custom_protocol_fee_data[vault] + if self._unpack_custom_flag(config_data): # Always use the default fee recipient even with custom fees. - return PFConfig({ - fee_bps: self.custom_protocol_fee[vault], - fee_recipient: self.default_protocol_fee_config.fee_recipient - }) + return ( + self._unpack_protocol_fee(config_data), + self._unpack_fee_recipient(self.default_protocol_fee_data) + ) else: # Otherwise return the default config. - return self.default_protocol_fee_config + config_data = self.default_protocol_fee_data + return ( + self._unpack_protocol_fee(config_data), + self._unpack_fee_recipient(config_data) + ) + +@view +@external +def use_custom_protocol_fee(vault: address) -> bool: + """ + @notice If a custom protocol fee is used for a vault. + @param vault Address of the vault to check. + @return If a custom protocol fee is used. + """ + return self._unpack_custom_flag(self.custom_protocol_fee_data[vault]) + +@view +@internal +def _unpack_protocol_fee(config_data: uint256) -> uint16: + """ + Unpacks the protocol fee from the packed data uint. + """ + return convert(shift(config_data, -8) & FEE_BPS_MASK, uint16) + +@view +@internal +def _unpack_fee_recipient(config_data: uint256) -> address: + """ + Unpacks the fee recipient from the packed data uint. + """ + return convert(shift(config_data, -24), address) + +@view +@internal +def _unpack_custom_flag(config_data: uint256) -> bool: + """ + Unpacks the custom fee flag from the packed data uint. + """ + return config_data & 1 == 1 + +@internal +def _pack_protocol_fee_data(recipient: address, fee: uint16, custom: bool) -> uint256: + """ + Packs the full protocol fee data into a single uint256 slot. + This is used for both the default fee storage as well as for custom fees. + 72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag + """ + return shift(convert(recipient, uint256), 24) | shift(convert(fee, uint256), 8) | convert(custom, uint256) @external def set_protocol_fee_bps(new_protocol_fee_bps: uint16): @@ -194,14 +242,20 @@ def set_protocol_fee_bps(new_protocol_fee_bps: uint16): assert new_protocol_fee_bps <= MAX_FEE_BPS, "fee too high" # Cache the current default protocol fee. - default_config: PFConfig = self.default_protocol_fee_config - assert default_config.fee_recipient != empty(address), "no recipient" + default_fee_data: uint256 = self.default_protocol_fee_data + recipient: address = self._unpack_fee_recipient(default_fee_data) + + assert recipient != empty(address), "no recipient" # Set the new fee - self.default_protocol_fee_config.fee_bps = new_protocol_fee_bps + self.default_protocol_fee_data = self._pack_protocol_fee_data( + recipient, + new_protocol_fee_bps, + False + ) log UpdateProtocolFeeBps( - default_config.fee_bps, + self._unpack_protocol_fee(default_fee_data), new_protocol_fee_bps ) @@ -216,10 +270,15 @@ def set_protocol_fee_recipient(new_protocol_fee_recipient: address): assert msg.sender == self.governance, "not governance" assert new_protocol_fee_recipient != empty(address), "zero address" - old_recipient: address = self.default_protocol_fee_config.fee_recipient - - self.default_protocol_fee_config.fee_recipient = new_protocol_fee_recipient + default_fee_data: uint256 = self.default_protocol_fee_data + old_recipient: address = self._unpack_fee_recipient(default_fee_data) + self.default_protocol_fee_data = self._pack_protocol_fee_data( + new_protocol_fee_recipient, + self._unpack_protocol_fee(default_fee_data), + False + ) + log UpdateProtocolFeeRecipient( old_recipient, new_protocol_fee_recipient @@ -238,14 +297,13 @@ def set_custom_protocol_fee_bps(vault: address, new_custom_protocol_fee: uint16) """ assert msg.sender == self.governance, "not governance" assert new_custom_protocol_fee <= MAX_FEE_BPS, "fee too high" - assert self.default_protocol_fee_config.fee_recipient != empty(address), "no recipient" + assert self._unpack_fee_recipient(self.default_protocol_fee_data) != empty(address), "no recipient" - self.custom_protocol_fee[vault] = new_custom_protocol_fee - - # If this is the first time a custom fee is set for this vault - # set the bool indicator so it returns the correct fee. - if not self.use_custom_protocol_fee[vault]: - self.use_custom_protocol_fee[vault] = True + self.custom_protocol_fee_data[vault] = self._pack_protocol_fee_data( + empty(address), + new_custom_protocol_fee, + True + ) log UpdateCustomProtocolFee(vault, new_custom_protocol_fee) @@ -259,11 +317,8 @@ def remove_custom_protocol_fee(vault: address): """ assert msg.sender == self.governance, "not governance" - # Reset the custom fee to 0. - self.custom_protocol_fee[vault] = 0 - - # Set custom fee bool back to false. - self.use_custom_protocol_fee[vault] = False + # Reset the custom fee to 0 and flag to False. + self.custom_protocol_fee_data[vault] = self._pack_protocol_fee_data(empty(address), 0, False) log RemovedCustomProtocolFee(vault) @@ -304,4 +359,3 @@ def accept_governance(): self.pending_governance = empty(address) log UpdateGovernance(msg.sender) - diff --git a/tests/unit/factory/test_protocol_fees_config.py b/tests/unit/factory/test_protocol_fees_config.py index 479c7fe5..43a5194a 100644 --- a/tests/unit/factory/test_protocol_fees_config.py +++ b/tests/unit/factory/test_protocol_fees_config.py @@ -10,7 +10,7 @@ def test__set_protocol_fee_recipient(gov, vault_factory): assert event[0].old_fee_recipient == ZERO_ADDRESS assert event[0].new_fee_recipient == gov.address - assert vault_factory.protocol_fee_config().fee_recipient == gov.address + assert vault_factory.protocol_fee_config()[1] == gov.address def test__set_protocol_fee_recipient__zero_address__reverts(gov, vault_factory): @@ -19,7 +19,7 @@ def test__set_protocol_fee_recipient__zero_address__reverts(gov, vault_factory): def test__set_protocol_fees(gov, vault_factory): - assert vault_factory.protocol_fee_config().fee_bps == 0 + assert vault_factory.protocol_fee_config()[0] == 0 # Need to set the fee recipient first vault_factory.set_protocol_fee_recipient(gov.address, sender=gov) @@ -30,26 +30,20 @@ def test__set_protocol_fees(gov, vault_factory): assert event[0].old_fee_bps == 0 assert event[0].new_fee_bps == 20 - assert vault_factory.protocol_fee_config().fee_bps == 20 + assert vault_factory.protocol_fee_config()[0] == 20 def test__set_custom_protocol_fee(gov, vault_factory, create_vault, asset): # Set the default protocol fee recipient vault_factory.set_protocol_fee_recipient(gov.address, sender=gov) - assert vault_factory.protocol_fee_config().fee_recipient == gov.address - assert vault_factory.protocol_fee_config().fee_bps == 0 + assert vault_factory.protocol_fee_config() == (0, gov.address) vault = create_vault(asset) # Make sure its currently set to the default settings. - assert ( - vault_factory.protocol_fee_config(sender=vault.address).fee_recipient - == gov.address - ) - assert vault_factory.protocol_fee_config(sender=vault.address).fee_bps == 0 - assert vault_factory.protocol_fee_config(vault.address).fee_recipient == gov.address - assert vault_factory.protocol_fee_config(vault.address).fee_bps == 0 + assert vault_factory.protocol_fee_config(vault.address) == (0, gov.address) + assert vault_factory.protocol_fee_config(sender=vault.address) == (0, gov.address) new_fee = int(20) # Set custom fee for new vault. @@ -62,20 +56,17 @@ def test__set_custom_protocol_fee(gov, vault_factory, create_vault, asset): assert event[0].new_custom_protocol_fee == new_fee assert vault_factory.use_custom_protocol_fee(vault.address) == True - assert vault_factory.custom_protocol_fee(vault.address) == new_fee + assert vault_factory.protocol_fee_config(vault.address)[0] == new_fee # Should now be different than default - assert ( - vault_factory.protocol_fee_config(sender=vault.address).fee_recipient - == gov.address + assert vault_factory.protocol_fee_config(vault.address) == (new_fee, gov.address) + assert vault_factory.protocol_fee_config(sender=vault.address) == ( + new_fee, + gov.address, ) - assert vault_factory.protocol_fee_config(sender=vault.address).fee_bps == new_fee - assert vault_factory.protocol_fee_config(vault.address).fee_recipient == gov.address - assert vault_factory.protocol_fee_config(vault.address).fee_bps == new_fee # Make sure the default is not changed. - assert vault_factory.protocol_fee_config().fee_recipient == gov.address - assert vault_factory.protocol_fee_config().fee_bps == 0 + assert vault_factory.protocol_fee_config() == (0, gov.address) def test__remove_custom_protocol_fee(gov, vault_factory, create_vault, asset): @@ -98,13 +89,11 @@ def test__remove_custom_protocol_fee(gov, vault_factory, create_vault, asset): assert event[0].new_custom_protocol_fee == new_fee # Should now be different than default - assert ( - vault_factory.protocol_fee_config(sender=vault.address).fee_recipient - == gov.address + assert vault_factory.protocol_fee_config(vault.address) == (new_fee, gov.address) + assert vault_factory.protocol_fee_config(sender=vault.address) == ( + new_fee, + gov.address, ) - assert vault_factory.protocol_fee_config(sender=vault.address).fee_bps == new_fee - assert vault_factory.protocol_fee_config(vault.address).fee_recipient == gov.address - assert vault_factory.protocol_fee_config(vault.address).fee_bps == new_fee # Now remove the custom fee config tx = vault_factory.remove_custom_protocol_fee(vault.address, sender=gov) @@ -115,29 +104,27 @@ def test__remove_custom_protocol_fee(gov, vault_factory, create_vault, asset): assert event[0].vault == vault.address # Should now be the default - assert ( - vault_factory.protocol_fee_config(sender=vault.address).fee_recipient - == gov.address + assert vault_factory.protocol_fee_config(vault.address) == ( + generic_fee, + gov.address, ) - assert ( - vault_factory.protocol_fee_config(sender=vault.address).fee_bps == generic_fee + assert vault_factory.protocol_fee_config(sender=vault.address) == ( + generic_fee, + gov.address, ) - assert vault_factory.protocol_fee_config(vault.address).fee_recipient == gov.address - assert vault_factory.protocol_fee_config(vault.address).fee_bps == generic_fee assert vault_factory.use_custom_protocol_fee(vault.address) == False - assert vault_factory.custom_protocol_fee(vault.address) == 0 def test__set_protocol_fee_before_recipient__reverts(gov, vault_factory): - assert vault_factory.protocol_fee_config().fee_recipient == ZERO_ADDRESS + assert vault_factory.protocol_fee_config()[1] == ZERO_ADDRESS with ape.reverts("no recipient"): vault_factory.set_protocol_fee_bps(20, sender=gov) def test__set_custom_fee_before_recipient__reverts(gov, vault_factory, vault): - assert vault_factory.protocol_fee_config().fee_recipient == ZERO_ADDRESS + assert vault_factory.protocol_fee_config()[1] == ZERO_ADDRESS with ape.reverts("no recipient"): vault_factory.set_custom_protocol_fee_bps(vault.address, 20, sender=gov) diff --git a/tests/unit/vault/test_protocol_fees.py b/tests/unit/vault/test_protocol_fees.py index 9dc85135..83dd8dee 100644 --- a/tests/unit/vault/test_protocol_fees.py +++ b/tests/unit/vault/test_protocol_fees.py @@ -1,6 +1,6 @@ from ape import chain import pytest -from utils.constants import ROLES, YEAR, MAX_BPS_ACCOUNTANT +from utils.constants import ROLES, YEAR, MAX_BPS_ACCOUNTANT, ZERO_ADDRESS from utils.utils import days_to_secs @@ -18,7 +18,7 @@ def test__report_with_no_protocol_fees__no_accountant_fees( ): amount = fish_amount // 10 - assert vault_factory.protocol_fee_config().fee_bps == 0 + assert vault_factory.protocol_fee_config() == (0, ZERO_ADDRESS) vault = create_vault(asset) strategy = create_strategy(vault) From 42afe0ca131fcb80e623952f803ba4230096c4b8 Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Thu, 19 Sep 2024 15:04:31 -0600 Subject: [PATCH 15/20] fix: deposit flow (#209) * forge install: openzeppelin-contracts v4.9.5 * chore: oz sub module * test: fix foundry tests * fix: deposit flow * fix: zero total assets * fix: flow * test: full loss * chore: comment * test: add invariants * fix: comments * fix: user msg sender * fix: comments * fix: comment --- contracts/VaultV3.vy | 105 +++------ foundry.toml | 4 +- foundry_tests/handlers/VaultHandler.sol | 291 ++++++++++++++++++++++++ foundry_tests/tests/Invariants.t.sol | 99 ++++++++ foundry_tests/utils/BaseInvariant.sol | 145 ++++++++++++ foundry_tests/utils/LibAddressSet.sol | 44 ++++ foundry_tests/utils/Setup.sol | 33 +++ tests/unit/vault/test_shares.py | 91 +++++++- 8 files changed, 729 insertions(+), 83 deletions(-) create mode 100644 foundry_tests/handlers/VaultHandler.sol create mode 100644 foundry_tests/tests/Invariants.t.sol create mode 100644 foundry_tests/utils/BaseInvariant.sol create mode 100644 foundry_tests/utils/LibAddressSet.sol 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 From ded02b3d06a263501b64c5725b0516be1d82d49a Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Thu, 19 Sep 2024 12:14:35 -0600 Subject: [PATCH 16/20] chore: add to interfaces --- contracts/VaultFactory.vy | 1 + contracts/interfaces/IVault.sol | 9 +++++++++ contracts/interfaces/IVaultFactory.sol | 4 ---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/VaultFactory.vy b/contracts/VaultFactory.vy index 21755a3c..8f08bcee 100644 --- a/contracts/VaultFactory.vy +++ b/contracts/VaultFactory.vy @@ -221,6 +221,7 @@ def _unpack_custom_flag(config_data: uint256) -> bool: """ return config_data & 1 == 1 +@view @internal def _pack_protocol_fee_data(recipient: address, fee: uint16, custom: bool) -> uint256: """ diff --git a/contracts/interfaces/IVault.sol b/contracts/interfaces/IVault.sol index 27e6b7c4..e45a343d 100644 --- a/contracts/interfaces/IVault.sol +++ b/contracts/interfaces/IVault.sol @@ -33,6 +33,7 @@ interface IVault is IERC4626 { address indexed strategy, uint256 new_debt ); + event UpdateAutoAllocate(bool auto_allocate); event UpdateDepositLimit(uint256 deposit_limit); event UpdateMinimumTotalIdle(uint256 minimum_total_idle); event UpdateProfitMaxUnlockTime(uint256 profit_max_unlock_time); @@ -54,6 +55,8 @@ interface IVault is IERC4626 { function use_default_queue() external view returns (bool); + function auto_allocate() external view returns (bool); + function minimum_total_idle() external view returns (uint256); function deposit_limit() external view returns (uint256); @@ -82,12 +85,18 @@ interface IVault is IERC4626 { uint256 ) external; + function setName(string memory) external; + + function setSymbol(string memory) external; + function set_accountant(address new_accountant) external; function set_default_queue(address[] memory new_default_queue) external; function set_use_default_queue(bool) external; + function set_auto_allocate(bool) external; + function set_deposit_limit(uint256 deposit_limit) external; function set_deposit_limit( diff --git a/contracts/interfaces/IVaultFactory.sol b/contracts/interfaces/IVaultFactory.sol index 56ea4025..880e4941 100644 --- a/contracts/interfaces/IVaultFactory.sol +++ b/contracts/interfaces/IVaultFactory.sol @@ -27,10 +27,6 @@ interface IVaultFactory { function name() external view returns (string memory); - function default_protocol_fee_config() external view returns (uint256); - - function custom_protocol_fee(address) external view returns (uint16); - function use_custom_protocol_fee(address) external view returns (bool); function deploy_new_vault( From aff0b5a6598a504c8b7574cd79ee4e654fdc24c3 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 20 Sep 2024 09:33:20 -0600 Subject: [PATCH 17/20] fix: comments --- README.md | 4 +++- TECH_SPEC.md | 10 ++++++---- contracts/VaultFactory.vy | 11 +++++------ contracts/VaultV3.vy | 5 +++-- scripts/deploy.py | 2 +- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a78d1e42..967fc858 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This repository contains the Smart Contracts for Yearns V3 vault implementation. [Vault.vy](contracts/VaultV3.vy) - The ERC4626 compliant Vault that will handle all logic associated with deposits, withdraws, strategy management, profit reporting etc. +For the most updated deployment addresses see the [docs](https://docs.yearn.fi/developers/addresses/v3-contracts). And read more about V3 and how to manage your own multi strategy vault here https://docs.yearn.fi/developers/v3/overview + For the V3 strategy implementation see the [Tokenized Strategy](https://github.com/yearn/tokenized-strategy) repo. ## Requirements @@ -72,7 +74,7 @@ forge test Deployments of the Vault Factory are done using create2 to be at a deterministic address on any EVM chain. -Check the [docs](https://docs.yearn.fi/developers/v3/overview) for the most updated deployment address. +Check the [docs](https://docs.yearn.fi/developers/addresses/v3-contracts) for the most updated deployment address. Deployments on new chains can be done permissionlessly by anyone using the included script. ``` diff --git a/TECH_SPEC.md b/TECH_SPEC.md index cafbf216..5b53ef93 100644 --- a/TECH_SPEC.md +++ b/TECH_SPEC.md @@ -27,9 +27,9 @@ This allows different players to deploy their own version and implement their ow ``` Example periphery contracts: -- Emergency module: it receives deposits of Vault Shares and allows the contract to call the shutdown function after a certain % of total Vault Shares have been deposited -- Debt Allocator: a smart contract that incentivize's APY / debt allocation optimization by rewarding the best debt allocation (see [yStarkDebtAllocator](https://github.com/jmonteer/ystarkdebtallocator)) -- Strategy Staking Module: a smart contract that allows players to sponsor specific strategies (so that they are added to the vault) by staking their YFI, making money if they do well and losing money if they don't. +- Role Manager: Governance contract that holds the vaults `role_manager` position to codify vault setup and ownership guidelines. (see [RoleManager](https://github.com/yearn/vault-periphery/tree/master/contracts/Managers)) +- Debt Allocator: a smart contract that optimizes between multiple strategies based on the optimal return. (see [DebAllocators](https://github.com/yearn/vault-periphery/tree/master/contracts/debtAllocators)) +- Safety Staking Module: a smart contract that allows players to sponsor specific strategies (so that they are added to the vault) by staking their YFI, making money if they do well and losing money if they don't. - Deposit Limit Module: Will dynamically adjust the deposit limit based on the depositor and arbitrary conditions. - ... ``` @@ -81,7 +81,7 @@ If totalAssets > currentDebt: the vault will record a profit Both loss and profit will impact strategy's debt, increasing the debt (current debt + profit) if there are profits, decreasing its debt (current debt - loss) if there are losses. #### Fees -Fee assessment and distribution are handled by the Accountant module. +Fee assessment and distribution are handled by the `accountant` module. It will report the amount of fees that need to be charged and the vault will issue shares for that amount of fees. @@ -131,6 +131,8 @@ The account that manages roles is a single account, set in `role_manager`. This role_manager can be an EOA, a multi-sig or a Governance contract that relays calls. +The `role_manager` can also update the vaults name and symbol as well as give out the vaults Roles. + ### Strategy Management This responsibility is taken by callers with ADD_STRATEGY_MANAGER, REVOKE_STRATEGY_MANAGER and FORCE_REVOKE_MANAGER roles diff --git a/contracts/VaultFactory.vy b/contracts/VaultFactory.vy index 8f08bcee..4699193f 100644 --- a/contracts/VaultFactory.vy +++ b/contracts/VaultFactory.vy @@ -90,8 +90,8 @@ pending_governance: public(address) # Name for identification. name: public(String[64]) -# Protocol Fee Data is packed into a uint256 slot -# 72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag +# Protocol Fee Data is packed into a single uint256 slot +# 72 bits Empty | 160 bits fee recipient | 16 bits fee bps | 8 bits custom flag # The default config for assessing protocol fees. default_protocol_fee_data: uint256 @@ -227,7 +227,7 @@ def _pack_protocol_fee_data(recipient: address, fee: uint16, custom: bool) -> ui """ Packs the full protocol fee data into a single uint256 slot. This is used for both the default fee storage as well as for custom fees. - 72 Bits Empty | 160 Bits fee recipient | 16 bits fee bps | 8 bits custom flag + 72 bits Empty | 160 bits fee recipient | 16 bits fee bps | 8 bits custom flag """ return shift(convert(recipient, uint256), 24) | shift(convert(fee, uint256), 8) | convert(custom, uint256) @@ -272,8 +272,7 @@ def set_protocol_fee_recipient(new_protocol_fee_recipient: address): assert new_protocol_fee_recipient != empty(address), "zero address" default_fee_data: uint256 = self.default_protocol_fee_data - old_recipient: address = self._unpack_fee_recipient(default_fee_data) - + self.default_protocol_fee_data = self._pack_protocol_fee_data( new_protocol_fee_recipient, self._unpack_protocol_fee(default_fee_data), @@ -281,7 +280,7 @@ def set_protocol_fee_recipient(new_protocol_fee_recipient: address): ) log UpdateProtocolFeeRecipient( - old_recipient, + self._unpack_fee_recipient(default_fee_data), new_protocol_fee_recipient ) diff --git a/contracts/VaultV3.vy b/contracts/VaultV3.vy index c65d317e..d45ae5b7 100644 --- a/contracts/VaultV3.vy +++ b/contracts/VaultV3.vy @@ -1257,7 +1257,7 @@ def _process_report(strategy: address) -> (uint256, uint256): self.strategies[strategy].current_debt = current_debt self.total_debt += gain else: - self.total_idle += gain + self.total_idle = total_assets # Or record any reported loss elif loss > 0: @@ -1266,7 +1266,7 @@ def _process_report(strategy: address) -> (uint256, uint256): self.strategies[strategy].current_debt = current_debt self.total_debt -= loss else: - self.total_idle -= loss + self.total_idle = total_assets # Issue shares for fees that were calculated above if applicable. if total_fees_shares > 0: @@ -1783,6 +1783,7 @@ def shutdown_vault(): def deposit(assets: uint256, receiver: address) -> uint256: """ @notice Deposit assets into the vault. + @dev Pass max uint256 to deposit full asset balance. @param assets The amount of assets to deposit. @param receiver The address to receive the shares. @return The amount of shares minted. diff --git a/scripts/deploy.py b/scripts/deploy.py index fd3d65d9..f62db50d 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -19,7 +19,7 @@ def deploy_original_and_factory(): "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed" ) - salt_string = "v3.0.2" + salt_string = "v3.0.3" # Create a SHA-256 hash object hash_object = hashlib.sha256() From d268bbd763bb1481d423c235d6c4a7b39df74fcb Mon Sep 17 00:00:00 2001 From: Schlag <89420541+Schlagonia@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:36:09 -0600 Subject: [PATCH 18/20] chore: match gov abi (#213) --- contracts/VaultFactory.vy | 30 +++++++++++--------- scripts/deploy.py | 2 +- tests/unit/factory/test_ownership.py | 42 ++++++++++++++-------------- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/contracts/VaultFactory.vy b/contracts/VaultFactory.vy index 4699193f..4e97a8c0 100644 --- a/contracts/VaultFactory.vy +++ b/contracts/VaultFactory.vy @@ -60,11 +60,12 @@ event RemovedCustomProtocolFee: event FactoryShutdown: pass -event UpdateGovernance: - governance: indexed(address) +event GovernanceTransferred: + previousGovernance: indexed(address) + newGovernance: indexed(address) -event NewPendingGovernance: - pending_governance: indexed(address) +event UpdatePendingGovernance: + newPendingGovernance: indexed(address) # Identifier for this version of the vault. @@ -85,7 +86,7 @@ shutdown: public(bool) # Address that can set or change the fee configs. governance: public(address) # Pending governance waiting to be accepted. -pending_governance: public(address) +pendingGovernance: public(address) # Name for identification. name: public(String[64]) @@ -272,7 +273,7 @@ def set_protocol_fee_recipient(new_protocol_fee_recipient: address): assert new_protocol_fee_recipient != empty(address), "zero address" default_fee_data: uint256 = self.default_protocol_fee_data - + self.default_protocol_fee_data = self._pack_protocol_fee_data( new_protocol_fee_recipient, self._unpack_protocol_fee(default_fee_data), @@ -339,23 +340,26 @@ def shutdown_factory(): log FactoryShutdown() @external -def set_governance(new_governance: address): +def transferGovernance(new_governance: address): """ @notice Set the governance address @param new_governance The new governance address """ assert msg.sender == self.governance, "not governance" - self.pending_governance = new_governance + self.pendingGovernance = new_governance - log NewPendingGovernance(new_governance) + log UpdatePendingGovernance(new_governance) @external -def accept_governance(): +def acceptGovernance(): """ @notice Accept the governance address """ - assert msg.sender == self.pending_governance, "not pending governance" + assert msg.sender == self.pendingGovernance, "not pending governance" + + old_governance: address = self.governance + self.governance = msg.sender - self.pending_governance = empty(address) + self.pendingGovernance = empty(address) - log UpdateGovernance(msg.sender) + log GovernanceTransferred(old_governance, msg.sender) diff --git a/scripts/deploy.py b/scripts/deploy.py index f62db50d..097a2b6e 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -52,7 +52,7 @@ def deploy_original_and_factory(): print(f"Deploying factory...") factory_constructor = vault_factory.constructor.encode_input( - "Yearn v3.0.2 Vault Factory", + "Yearn v3.0.3 Vault Factory", original_address, "0x33333333D5eFb92f19a5F94a43456b3cec2797AE", ) diff --git a/tests/unit/factory/test_ownership.py b/tests/unit/factory/test_ownership.py index 9de28d00..efdebbe5 100644 --- a/tests/unit/factory/test_ownership.py +++ b/tests/unit/factory/test_ownership.py @@ -5,66 +5,66 @@ def test_gov_transfers_ownership(vault_factory, gov, strategist): assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS - vault_factory.set_governance(strategist, sender=gov) + vault_factory.transferGovernance(strategist, sender=gov) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == strategist + assert vault_factory.pendingGovernance() == strategist - vault_factory.accept_governance(sender=strategist) + vault_factory.acceptGovernance(sender=strategist) assert vault_factory.governance() == strategist - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS def test_gov_transfers_ownership__gov_cant_accept(vault_factory, gov, strategist): assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS - vault_factory.set_governance(strategist, sender=gov) + vault_factory.transferGovernance(strategist, sender=gov) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == strategist + assert vault_factory.pendingGovernance() == strategist with ape.reverts("not pending governance"): - vault_factory.accept_governance(sender=gov) + vault_factory.acceptGovernance(sender=gov) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == strategist + assert vault_factory.pendingGovernance() == strategist def test_random_transfers_ownership__fails(vault_factory, gov, strategist): assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS with ape.reverts("not governance"): - vault_factory.set_governance(strategist, sender=strategist) + vault_factory.transferGovernance(strategist, sender=strategist) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS def test_gov_transfers_ownership__can_change_pending( vault_factory, gov, bunny, strategist ): assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS - vault_factory.set_governance(strategist, sender=gov) + vault_factory.transferGovernance(strategist, sender=gov) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == strategist + assert vault_factory.pendingGovernance() == strategist - vault_factory.set_governance(bunny, sender=gov) + vault_factory.transferGovernance(bunny, sender=gov) assert vault_factory.governance() == gov - assert vault_factory.pending_governance() == bunny + assert vault_factory.pendingGovernance() == bunny with ape.reverts("not pending governance"): - vault_factory.accept_governance(sender=strategist) + vault_factory.acceptGovernance(sender=strategist) - vault_factory.accept_governance(sender=bunny) + vault_factory.acceptGovernance(sender=bunny) assert vault_factory.governance() == bunny - assert vault_factory.pending_governance() == ZERO_ADDRESS + assert vault_factory.pendingGovernance() == ZERO_ADDRESS From 6b46a731e23e205f00bb52023bc1925a0f984083 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 24 Sep 2024 07:49:58 -0600 Subject: [PATCH 19/20] chore: deployment --- .gitignore | 3 +- foundry_tests/script/Deploy.s.sol | 67 +++++++++++++++++++++++++++++++ scripts/deploy.py | 4 +- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 foundry_tests/script/Deploy.s.sol diff --git a/.gitignore b/.gitignore index 1f52a787..05926a83 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ yarn.lock env cache/ out/ -.gas-snapshot \ No newline at end of file +.gas-snapshot +broadcast/ \ No newline at end of file diff --git a/foundry_tests/script/Deploy.s.sol b/foundry_tests/script/Deploy.s.sol new file mode 100644 index 00000000..37c36596 --- /dev/null +++ b/foundry_tests/script/Deploy.s.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import "forge-std/Script.sol"; + +///@notice This cheat codes interface is named _CheatCodes so you can use the CheatCodes interface in other testing files without errors +interface _CheatCodes { + function ffi(string[] calldata) external returns (bytes memory); +} + +// Deploy a contract to a deterministic address with create2 +contract Deploy is Script { + address constant HEVM_ADDRESS = + address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + + /// @notice Initializes cheat codes in order to use ffi to compile Vyper contracts + _CheatCodes cheatCodes = _CheatCodes(HEVM_ADDRESS); + + Deployer public deployer = + Deployer(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + + function run() external { + vm.startBroadcast(); + + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = "contracts/VaultFactory.vy"; + + ///@notice compile the Vyper contract and return the bytecode + bytes memory _bytecode = cheatCodes.ffi(cmds); + + bytes memory args = abi.encode( + "Yearn v3.0.3 Vault Factory", + 0xcA78AF7443f3F8FA0148b746Cb18FF67383CDF3f, + 0x6f3cBE2ab3483EC4BA7B672fbdCa0E9B33F88db8 + ); + + //add args to the deployment bytecode + bytes memory bytecode = abi.encodePacked(_bytecode, args); + + // Pick an unique salt + uint256 salt = 48628676351035099281129189787297157113420477883337005618231542152101559208037; + + address contractAddress = deployer.deployCreate2( + bytes32(salt), + bytecode + ); + + console.log("Address is ", contractAddress); + + vm.stopBroadcast(); + } +} + +interface Deployer { + event ContractCreation(address indexed newContract, bytes32 indexed salt); + + function deployCreate3( + bytes32 salt, + bytes memory initCode + ) external payable returns (address newContract); + + function deployCreate2( + bytes32 salt, + bytes memory initCode + ) external payable returns (address newContract); +} diff --git a/scripts/deploy.py b/scripts/deploy.py index 097a2b6e..391822a7 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -51,10 +51,12 @@ def deploy_original_and_factory(): # deploy factory print(f"Deploying factory...") + init_gov = "0x6f3cBE2ab3483EC4BA7B672fbdCa0E9B33F88db8" + factory_constructor = vault_factory.constructor.encode_input( "Yearn v3.0.3 Vault Factory", original_address, - "0x33333333D5eFb92f19a5F94a43456b3cec2797AE", + init_gov, ) factory_deploy_bytecode = HexBytes( From 4b672614cc01b9751e47f67776a9d2dcb8b2343d Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Tue, 24 Sep 2024 10:49:31 -0600 Subject: [PATCH 20/20] chore: deployed --- ape-config.yaml | 1 + foundry.toml | 1 + foundry_tests/script/Deploy.s.sol | 67 ------------------------------- 3 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 foundry_tests/script/Deploy.s.sol diff --git a/ape-config.yaml b/ape-config.yaml index f2fad8ad..2500d557 100644 --- a/ape-config.yaml +++ b/ape-config.yaml @@ -20,5 +20,6 @@ solidity: version: 0.8.18 ethereum: + evm_version: paris local: default_provider: hardhat \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index fbce3b5a..46acc547 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,6 +4,7 @@ test = 'foundry_tests' out = 'out' solc = "0.8.18" libs = ['lib'] +evm_version = "paris" remappings = [ 'forge-std/=lib/forge-std/src/', diff --git a/foundry_tests/script/Deploy.s.sol b/foundry_tests/script/Deploy.s.sol deleted file mode 100644 index 37c36596..00000000 --- a/foundry_tests/script/Deploy.s.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.18; - -import "forge-std/Script.sol"; - -///@notice This cheat codes interface is named _CheatCodes so you can use the CheatCodes interface in other testing files without errors -interface _CheatCodes { - function ffi(string[] calldata) external returns (bytes memory); -} - -// Deploy a contract to a deterministic address with create2 -contract Deploy is Script { - address constant HEVM_ADDRESS = - address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); - - /// @notice Initializes cheat codes in order to use ffi to compile Vyper contracts - _CheatCodes cheatCodes = _CheatCodes(HEVM_ADDRESS); - - Deployer public deployer = - Deployer(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); - - function run() external { - vm.startBroadcast(); - - string[] memory cmds = new string[](2); - cmds[0] = "vyper"; - cmds[1] = "contracts/VaultFactory.vy"; - - ///@notice compile the Vyper contract and return the bytecode - bytes memory _bytecode = cheatCodes.ffi(cmds); - - bytes memory args = abi.encode( - "Yearn v3.0.3 Vault Factory", - 0xcA78AF7443f3F8FA0148b746Cb18FF67383CDF3f, - 0x6f3cBE2ab3483EC4BA7B672fbdCa0E9B33F88db8 - ); - - //add args to the deployment bytecode - bytes memory bytecode = abi.encodePacked(_bytecode, args); - - // Pick an unique salt - uint256 salt = 48628676351035099281129189787297157113420477883337005618231542152101559208037; - - address contractAddress = deployer.deployCreate2( - bytes32(salt), - bytecode - ); - - console.log("Address is ", contractAddress); - - vm.stopBroadcast(); - } -} - -interface Deployer { - event ContractCreation(address indexed newContract, bytes32 indexed salt); - - function deployCreate3( - bytes32 salt, - bytes memory initCode - ) external payable returns (address newContract); - - function deployCreate2( - bytes32 salt, - bytes memory initCode - ) external payable returns (address newContract); -}