diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0ed6b0a..2aba55f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,6 +22,11 @@ jobs: with: submodules: recursive + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install Vyper run: pip install vyper==0.3.7 diff --git a/.gitignore b/.gitignore index 4836c80..c9d26e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +/broadcast/ # Docs docs/ diff --git a/.prettierignore b/.prettierignore index dcf7dcd..940145c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,4 +6,5 @@ venv/ venv/ lib/ out/ -cache/ \ No newline at end of file +cache/ +broadcast/ \ No newline at end of file diff --git a/README.md b/README.md index dbe22f8..3483c58 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,22 @@ - First you will need to install [Foundry](https://book.getfoundry.sh/getting-started/installation). NOTE: If you are on a windows machine it is recommended to use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) -- Install [Node.js](https://nodejs.org/en/download/package-manager/) ### Fork this repository - git clone https://github.com/yearn/vault-periphery +```sh +git clone --recursive https://github.com/yearn/vault-periphery - cd vault-periphery +cd vault-periphery - -### Deployment +pip install vyper==0.3.7 + +make build -Deployment of periphery contracts such as the [Registry Factory](https://github.com/yearn/vault-periphery/blob/master/contracts/registry/RegistryFactory.sol) or [Address Provider](https://github.com/yearn/vault-periphery/blob/master/contracts/AddressProvider.vy) are done using a create2 factory in order to get a deterministic address that is the same on each EVM chain. +make test +``` +### Deployment -This can be done permissionlessly if the most recent contract has not yet been deployed on a chain you would like to use it on. +Deployment of periphery contracts are done using a create2 factory in order to get a deterministic address that is the same on each EVM chain. -1. [Add an Ape account](https://docs.apeworx.io/ape/stable/commands/accounts.html) -2. Run the deployment the contracts specific deployment script under `scripts/` - ```sh - ape run scripts/deploy_contract_name.py --network YOUR_RPC_URL - ``` - - For chains that don't support 1559 tx's you may need to add a `type="0x0"` argument at the end of the deployment tx. - - ie `tx = deployer_contract.deployCreate2(salt, init_code, sender=deployer, type="0x0")` -3. The address the contract was deployed at will print in the console and should match any other chain the same version has been deployed on. \ No newline at end of file +This can be done permissionlessly if the most recent contract has not yet been deployed on a chain you would like to use it on using this repo https://github.com/wavey0x/yearn-v3-deployer diff --git a/lib/forge-std b/lib/forge-std index 8f24d6b..4f57c59 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa +Subproject commit 4f57c59f066a03d13de8c65bb34fca8247f5fcb2 diff --git a/scripts/Deploy.s.sol b/scripts/Deploy.s.sol new file mode 100644 index 0000000..e65bc19 --- /dev/null +++ b/scripts/Deploy.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.18; + +import "forge-std/Script.sol"; + +// Deploy a contract to a deterministic address with create2 factory. +contract Deploy is Script { + // Create X address. + Deployer public deployer = + Deployer(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + + address public initGov = 0x6f3cBE2ab3483EC4BA7B672fbdCa0E9B33F88db8; + + function run() external { + vm.startBroadcast(); + + // Append constructor args to the bytecode + bytes memory bytecode = abi.encodePacked( + vm.getCode("registry/ReleaseRegistry.sol:ReleaseRegistry"), + abi.encode(initGov) + ); + + // Use salt of 0. + bytes32 salt; + + address contractAddress = deployer.deployCreate2(salt, bytecode); + + console.log("Address is ", contractAddress); + + vm.stopBroadcast(); + } +} + +contract Deployer { + event ContractCreation(address indexed newContract, bytes32 indexed salt); + + function deployCreate2( + bytes32 salt, + bytes memory initCode + ) public payable returns (address newContract) {} +} diff --git a/scripts/DeployVyper.s.sol b/scripts/DeployVyper.s.sol new file mode 100644 index 0000000..b4daf37 --- /dev/null +++ b/scripts/DeployVyper.s.sol @@ -0,0 +1,65 @@ +// 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 factory. +contract DeployVyper 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); + + // Create X address. + Deployer public deployer = + Deployer(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + + address public initGov = 0x6f3cBE2ab3483EC4BA7B672fbdCa0E9B33F88db8; + + function run() external { + vm.startBroadcast(); + + ///@notice compile the Vyper contract and return the bytecode + bytes memory bytecode = compileVyper( + "src/addressProviders/", + "ProtocolAddressProvider" + ); + + bytecode = abi.encodePacked(bytecode, abi.encode(initGov)); + + // Use salt of 0. + bytes32 salt; + + address contractAddress = deployer.deployCreate2(salt, bytecode); + + console.log("Address is ", contractAddress); + + vm.stopBroadcast(); + } + + function compileVyper( + string memory path, + string memory fileName + ) public returns (bytes memory) { + string[] memory cmds = new string[](2); + cmds[0] = "vyper"; + cmds[1] = string.concat(path, fileName, ".vy"); + + return cheatCodes.ffi(cmds); + } +} + +contract Deployer { + event ContractCreation(address indexed newContract, bytes32 indexed salt); + + function deployCreate2( + bytes32 salt, + bytes memory initCode + ) public payable returns (address newContract) {} +} diff --git a/scripts/deploy_accountant.py b/scripts/deploy_accountant.py deleted file mode 100644 index 9d366a5..0000000 --- a/scripts/deploy_accountant.py +++ /dev/null @@ -1,78 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_accountant(): - print("Deploying an Accountant on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - accountant - salt = getSalt(f"Accountant {chain.pending_timestamp}") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - version = input( - "Would you like to deploy a normal Accountant a Refund Accountant? g/r " - ).lower() - - if version == "g": - print("Deploying an Accountant.") - accountant = project.Accountant - - else: - print("Deploying a Refund accountant.") - accountant = project.RefundAccountant - - print("Enter the default amounts to use in Base Points. (100% == 10_000)") - - management_fee = input("Default management fee? ") - assert int(management_fee) <= 200 - - performance_fee = input("Default performance fee? ") - assert int(performance_fee) <= 5_000 - - refund_ratio = input("Default refund ratio? ") - assert int(refund_ratio) <= 2**16 - 1 - - max_fee = input("Default max fee? ") - assert int(max_fee) <= 2**16 - 1 - - max_gain = input("Default max gain? ") - assert int(max_gain) <= 2**16 - 1 - - max_loss = input("Default max loss? ") - assert int(max_loss) <= 10_000 - - constructor = accountant.constructor.encode_input( - deployer.address, - deployer.address, - management_fee, - performance_fee, - refund_ratio, - max_fee, - max_gain, - max_loss, - ) - - # generate and deploy - deploy_bytecode = HexBytes( - HexBytes(accountant.contract_type.deployment_bytecode.bytecode) + constructor - ) - - print(f"Deploying Accountant...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") - - -def main(): - deploy_accountant() diff --git a/scripts/deploy_accountant_factory.py b/scripts/deploy_accountant_factory.py deleted file mode 100644 index f520cdc..0000000 --- a/scripts/deploy_accountant_factory.py +++ /dev/null @@ -1,35 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_accountant_factory(): - print("Deploying an Accountant Factory on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - accountant_factory = project.AccountantFactory - - salt = getSalt(f"Accountant Factory") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - # generate and deploy - deploy_bytecode = HexBytes( - accountant_factory.contract_type.deployment_bytecode.bytecode - ) - - print(f"Deploying Accountant actory...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - - -def main(): - deploy_accountant_factory() diff --git a/scripts/deploy_address_provider.py b/scripts/deploy_address_provider.py deleted file mode 100644 index ae9732f..0000000 --- a/scripts/deploy_address_provider.py +++ /dev/null @@ -1,41 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_address_provider(): - print("Deploying Address Provider on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - address_provider = project.ProtocolAddressProvider - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("Protocol Address Provider") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - # generate and deploy - constructor = address_provider.constructor.encode_input( - "0x33333333D5eFb92f19a5F94a43456b3cec2797AE" - ) - - deploy_bytecode = HexBytes( - HexBytes(address_provider.contract_type.deployment_bytecode.bytecode) - + constructor - ) - - print(f"Deploying Address Provider...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") - - -def main(): - deploy_address_provider() diff --git a/scripts/deploy_allocator_factory.py b/scripts/deploy_allocator_factory.py deleted file mode 100644 index bd9c8e8..0000000 --- a/scripts/deploy_allocator_factory.py +++ /dev/null @@ -1,43 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_allocator_factory(): - print("Deploying Debt Allocator Factory on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - allocator_factory = project.DebtAllocatorFactory - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("Debt Allocator Factory") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - gov = input("Governance? ") - - allocator_constructor = allocator_factory.constructor.encode_input(gov) - - # generate and deploy - deploy_bytecode = HexBytes( - HexBytes(allocator_factory.contract_type.deployment_bytecode.bytecode) - + allocator_constructor - ) - - print(f"Deploying the Factory...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - print( - f"Encoded Constructor to use for verifaction {allocator_constructor.hex()[2:]}" - ) - - -def main(): - deploy_allocator_factory() diff --git a/scripts/deploy_keeper.py b/scripts/deploy_keeper.py deleted file mode 100644 index 863b6f8..0000000 --- a/scripts/deploy_keeper.py +++ /dev/null @@ -1,35 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_keeper(): - print("Deploying Keeper on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - keeper = project.Keeper - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("Keeper") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - # generate and deploy - deploy_bytecode = HexBytes( - HexBytes(keeper.contract_type.deployment_bytecode.bytecode) - ) - - print(f"Deploying the Keeper...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - - -def main(): - deploy_keeper() diff --git a/scripts/deploy_registry.py b/scripts/deploy_registry.py deleted file mode 100644 index 5e27e5d..0000000 --- a/scripts/deploy_registry.py +++ /dev/null @@ -1,72 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_release_and_factory(): - print("Deploying Vault Registry on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - release_registry = project.ReleaseRegistry - factory = project.RegistryFactory - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("registry") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - release_address = "0x990089173D5d5287c344092Be0bB37950A67d17B" - - if input("Do you want to deploy a new Release Registry? ") == "y": - - # generate and deploy release registry - release_constructor = release_registry.constructor.encode_input( - "0x33333333D5eFb92f19a5F94a43456b3cec2797AE" - ) - - release_deploy_bytecode = HexBytes( - HexBytes(release_registry.contract_type.deployment_bytecode.bytecode) - + release_constructor - ) - - print(f"Deploying Release Registry...") - - # Use old deployer contract to get the same address. - deployer_contract = project.Deployer.at( - "0x8D85e7c9A4e369E53Acc8d5426aE1568198b0112" - ) - - release_tx = deployer_contract.deploy( - release_deploy_bytecode, salt, sender=deployer - ) - - release_event = list(release_tx.decode_logs(deployer_contract.Deployed)) - - release_address = release_event[0].addr - - print(f"Deployed the vault release to {release_address}") - print("------------------") - print(f"Encoded Constructor to use for verifaction {release_constructor.hex()}") - - # Deploy factory - print(f"Deploying factory...") - - factory_constructor = factory.constructor.encode_input(release_address) - - factory_deploy_bytecode = HexBytes( - HexBytes(factory.contract_type.deployment_bytecode.bytecode) - + factory_constructor - ) - - deploy_contract(factory_deploy_bytecode, salt, deployer) - - print("------------------") - print(f"Encoded Constructor to use for verifaction {factory_constructor.hex()[2:]}") - - -def main(): - deploy_release_and_factory() diff --git a/scripts/deploy_role_manager.py b/scripts/deploy_role_manager.py deleted file mode 100644 index e1cab53..0000000 --- a/scripts/deploy_role_manager.py +++ /dev/null @@ -1,49 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_role_manager(): - - print("Deploying Role Manager on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - role_manager = project.RoleManager - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("Role Manager") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - print(f"Deploying the Role Manager...") - print("Enter the addresses to use on deployment.") - - gov = input("Governance? ") - daddy = input("Daddy? ") - brain = input("Brain? ") - security = input("Security? ") - keeper = input("Keeper? ") - strategy_manager = input("Strategy manager? ") - registry = input("Registry? ") - - constructor = role_manager.constructor.encode_input( - gov, daddy, brain, security, keeper, strategy_manager, registry - ) - - deploy_bytecode = HexBytes( - HexBytes(role_manager.contract_type.deployment_bytecode.bytecode) + constructor - ) - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - print(f"Encoded Constructor to use for verifaction {constructor.hex()[2:]}") - - -def main(): - deploy_role_manager() diff --git a/scripts/deploy_splitter_factory.py b/scripts/deploy_splitter_factory.py deleted file mode 100644 index 7bba33a..0000000 --- a/scripts/deploy_splitter_factory.py +++ /dev/null @@ -1,52 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -from scripts.deployments import getSalt, deploy_contract - - -def deploy_splitter_factory(): - print("Deploying Splitter Factory on ChainID", chain.chain_id) - - if input("Do you want to continue? ") == "n": - return - - splitter = project.Splitter - splitter_factory = project.SplitterFactory - - deployer = input("Name of account to use? ") - deployer = accounts.load(deployer) - - salt = getSalt("Splitter Factory") - - print(f"Salt we are using {salt}") - print("Init balance:", deployer.balance / 1e18) - - print(f"Deploying Original.") - - original_deploy_bytecode = HexBytes( - HexBytes(splitter.contract_type.deployment_bytecode.bytecode) - ) - - original_address = deploy_contract(original_deploy_bytecode, salt, deployer) - - print(f"Original deployed to {original_address}") - - allocator_constructor = splitter_factory.constructor.encode_input(original_address) - - # generate and deploy - deploy_bytecode = HexBytes( - HexBytes(splitter_factory.contract_type.deployment_bytecode.bytecode) - + allocator_constructor - ) - - print(f"Deploying the Factory...") - - deploy_contract(deploy_bytecode, salt, deployer) - - print("------------------") - print( - f"Encoded Constructor to use for verifaction {allocator_constructor.hex()[2:]}" - ) - - -def main(): - deploy_splitter_factory() diff --git a/scripts/deployments.py b/scripts/deployments.py deleted file mode 100644 index 92e36ec..0000000 --- a/scripts/deployments.py +++ /dev/null @@ -1,31 +0,0 @@ -from ape import project, accounts, Contract, chain, networks -from hexbytes import HexBytes -import hashlib - - -def getSalt(salt_string): - # Create a SHA-256 hash object - hash_object = hashlib.sha256() - # Update the hash object with the string data - hash_object.update(salt_string.encode("utf-8")) - # Get the hexadecimal representation of the hash - hex_hash = hash_object.hexdigest() - # Convert the hexadecimal hash to an integer - return int(hex_hash, 16) - - -def deploy_contract(init_code, salt, deployer): - deployer_contract = project.Deployer.at( - "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed" - ) - - tx = deployer_contract.deployCreate2(salt, init_code, sender=deployer) - - event = list(tx.decode_logs(deployer_contract.ContractCreation)) - - address = event[0].newContract - - print("------------------") - print(f"Deployed the contract to {address}") - - return address diff --git a/src/accountants/Accountant.sol b/src/accountants/Accountant.sol index 8b90305..acc63ff 100644 --- a/src/accountants/Accountant.sol +++ b/src/accountants/Accountant.sol @@ -306,8 +306,8 @@ contract Accountant { } /** - * @notice Function to update the default fee configuration used for - all strategies that don't have a custom config set. + * @notice Function to update the default fee configuration used for + * all strategies that don't have a custom config set. * @param defaultManagement Default annual management fee to charge. * @param defaultPerformance Default performance fee to charge. * @param defaultRefund Default refund ratio to give back on losses. diff --git a/src/addressProviders/ProtocolAddressProvider.vy b/src/addressProviders/ProtocolAddressProvider.vy index d1305da..e99108a 100644 --- a/src/addressProviders/ProtocolAddressProvider.vy +++ b/src/addressProviders/ProtocolAddressProvider.vy @@ -7,6 +7,10 @@ @notice Protocol Address provider for the periphery contracts for the Yearn V3 system. """ + +interface IAddressProvider: + def getAddress(address_id: bytes32) -> address: view + #### EVENTS #### event UpdatedAddress: @@ -36,9 +40,9 @@ COMMON_REPORT_TRIGGER: constant(bytes32) = keccak256("Common Report Trigger") AUCTION_FACTORY: constant(bytes32) = keccak256("Auction Factory") SPLITTER_FACTORY: constant(bytes32) = keccak256("Splitter Factory") REGISTRY_FACTORY: constant(bytes32) = keccak256("Registry Factory") -DEBT_ALLOCATOR_FACTORY: constant(bytes32) = keccak256("Debt Allocator Factory") ACCOUNTANT_FACTORY: constant(bytes32) = keccak256("Accountant Factory") ROLE_MANAGER_FACTORY: constant(bytes32) = keccak256("Role Manager Factory") +DEBT_ALLOCATOR_FACTORY: constant(bytes32) = keccak256("Debt Allocator Factory") name: public(constant(String[34])) = "Yearn V3 Protocol Address Provider" diff --git a/src/debtAllocators/DebtAllocator.sol b/src/debtAllocators/DebtAllocator.sol index 68a649d..a7c20bb 100644 --- a/src/debtAllocators/DebtAllocator.sol +++ b/src/debtAllocators/DebtAllocator.sol @@ -17,6 +17,7 @@ interface IBaseFee { * Yearn V3 vaults to provide the needed triggers for a keeper * to perform automated debt updates for the vaults strategies. * + * @dev * Each vault that should be managed by this allocator will * need to be added by first setting a `minimumChange` for the * vault, which will act as the minimum amount of funds to move that will @@ -26,7 +27,7 @@ interface IBaseFee { * The allocator aims to allocate debt between the strategies * based on their set target ratios. Which are denominated in basis * points and represent the percent of total assets that specific - * strategy should hold. + * strategy should hold (i.e 1_000 == 10% of the vaults `totalAssets`). * * The trigger will attempt to allocate up to the `maxRatio` when * the strategy has `minimumChange` amount less than the `targetRatio`. @@ -120,7 +121,6 @@ contract DebtAllocator is Governance { uint256 maxDebt; uint256 currentIdle; uint256 minIdle; - uint256 max; uint256 toChange; } @@ -247,8 +247,9 @@ contract DebtAllocator is Governance { strategyDebtInfo.vaultConfig = getVaultConfig(_vault); // Don't do anything if paused. - if (strategyDebtInfo.vaultConfig.paused) + if (strategyDebtInfo.vaultConfig.paused) { return (false, bytes("Paused")); + } // Check the base fee isn't too high. if (!isCurrentBaseFeeAcceptable()) return (false, bytes("Base Fee")); @@ -257,8 +258,9 @@ contract DebtAllocator is Governance { strategyDebtInfo.strategyConfig = getStrategyConfig(_vault, _strategy); // Make sure the strategy has been added to the allocator. - if (!strategyDebtInfo.strategyConfig.added) + if (!strategyDebtInfo.strategyConfig.added) { return (false, bytes("!added")); + } if ( block.timestamp - strategyDebtInfo.strategyConfig.lastUpdate <= @@ -296,7 +298,6 @@ contract DebtAllocator is Governance { if (strategyDebtInfo.targetDebt > params.current_debt) { strategyDebtInfo.currentIdle = IVault(_vault).totalIdle(); strategyDebtInfo.minIdle = IVault(_vault).minimum_total_idle(); - strategyDebtInfo.max = IVault(_strategy).maxDeposit(_vault); // We can't add more than the available idle. if (strategyDebtInfo.minIdle >= strategyDebtInfo.currentIdle) { @@ -309,7 +310,7 @@ contract DebtAllocator is Governance { // Can't take more than is available. Math.min( strategyDebtInfo.currentIdle - strategyDebtInfo.minIdle, - strategyDebtInfo.max + IVault(_strategy).maxDeposit(_vault) ) ); @@ -339,9 +340,6 @@ contract DebtAllocator is Governance { strategyDebtInfo.currentIdle = IVault(_vault).totalIdle(); strategyDebtInfo.minIdle = IVault(_vault).minimum_total_idle(); - strategyDebtInfo.max = IVault(_strategy).convertToAssets( - IVault(_strategy).maxRedeem(_vault) - ); if (strategyDebtInfo.minIdle > strategyDebtInfo.currentIdle) { // Pull at least the amount needed for minIdle. @@ -356,7 +354,9 @@ contract DebtAllocator is Governance { strategyDebtInfo.toChange, // Account for the current liquidity constraints. // Use max redeem to match vault logic. - strategyDebtInfo.max + IVault(_strategy).convertToAssets( + IVault(_strategy).maxRedeem(_vault) + ) ); // Check if it's over the threshold. @@ -408,9 +408,11 @@ contract DebtAllocator is Governance { address _strategy, uint256 _increase ) external virtual { - uint256 _currentRatio = getStrategyConfig(_vault, _strategy) - .targetRatio; - setStrategyDebtRatio(_vault, _strategy, _currentRatio + _increase); + setStrategyDebtRatio( + _vault, + _strategy, + getStrategyTargetRatio(_vault, _strategy) + _increase + ); } /** @@ -423,9 +425,11 @@ contract DebtAllocator is Governance { address _strategy, uint256 _decrease ) external virtual { - uint256 _currentRatio = getStrategyConfig(_vault, _strategy) - .targetRatio; - setStrategyDebtRatio(_vault, _strategy, _currentRatio - _decrease); + setStrategyDebtRatio( + _vault, + _strategy, + getStrategyTargetRatio(_vault, _strategy) - _decrease + ); } /** @@ -460,7 +464,7 @@ contract DebtAllocator is Governance { uint256 _targetRatio, uint256 _maxRatio ) public virtual onlyManagers { - VaultConfig storage vaultConfig = _vaultConfigs[_vault]; + VaultConfig memory vaultConfig = getVaultConfig(_vault); // Make sure a minimumChange has been set. require(vaultConfig.minimumChange != 0, "!minimum"); // Cannot be more than 100%. @@ -494,7 +498,7 @@ contract DebtAllocator is Governance { // Write to storage. _strategyConfigs[_vault][_strategy] = strategyConfig; - vaultConfig.totalDebtRatio = uint16(newTotalDebtRatio); + _vaultConfigs[_vault].totalDebtRatio = uint16(newTotalDebtRatio); emit UpdateStrategyDebtRatio( _vault, @@ -734,7 +738,7 @@ contract DebtAllocator is Governance { function getStrategyTargetRatio( address _vault, address _strategy - ) external view virtual returns (uint256) { + ) public view virtual returns (uint256) { return getStrategyConfig(_vault, _strategy).targetRatio; } @@ -747,7 +751,7 @@ contract DebtAllocator is Governance { function getStrategyMaxRatio( address _vault, address _strategy - ) external view virtual returns (uint256) { + ) public view virtual returns (uint256) { return getStrategyConfig(_vault, _strategy).maxRatio; } diff --git a/src/debtAllocators/DebtOptimizerApplicator.sol b/src/debtAllocators/DebtOptimizerApplicator.sol index f6d41ea..4b96e72 100644 --- a/src/debtAllocators/DebtOptimizerApplicator.sol +++ b/src/debtAllocators/DebtOptimizerApplicator.sol @@ -43,7 +43,7 @@ contract DebtOptimizerApplicator { ); } - /// @notice The address of the debt allocator factory to use for some role checks. + /// @notice The address of the debt allocator. address public immutable debtAllocator; /// @notice Mapping of addresses that are allowed to update debt ratios. diff --git a/src/managers/RoleManager.sol b/src/managers/RoleManager.sol index 960bbb3..2d50f69 100644 --- a/src/managers/RoleManager.sol +++ b/src/managers/RoleManager.sol @@ -6,9 +6,8 @@ import {Registry} from "../registry/Registry.sol"; import {Accountant} from "../accountants/Accountant.sol"; import {Roles} from "@yearn-vaults/interfaces/Roles.sol"; import {IVault} from "@yearn-vaults/interfaces/IVault.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {DebtAllocatorFactory} from "../debtAllocators/DebtAllocatorFactory.sol"; +import {ReleaseRegistry} from "../registry/ReleaseRegistry.sol"; +import {IVaultFactory} from "@yearn-vaults/interfaces/IVaultFactory.sol"; /// @title Yearn V3 Vault Role Manager. contract RoleManager is Positions { @@ -53,22 +52,23 @@ contract RoleManager is Positions { keccak256("Pending Governance"); /// @notice Position ID for "Governance". bytes32 public constant GOVERNANCE = keccak256("Governance"); - /// @notice Position ID for "brain". + /// @notice Position ID for "Brain". bytes32 public constant MANAGEMENT = keccak256("Management"); - /// @notice Position ID for "keeper". + + /// @notice Position ID for "Keeper". bytes32 public constant KEEPER = keccak256("Keeper"); - /// @notice Position ID for the Registry. + /// @notice Position ID for the "Registry". bytes32 public constant REGISTRY = keccak256("Registry"); - /// @notice Position ID for the Accountant. + /// @notice Position ID for the "Accountant". bytes32 public constant ACCOUNTANT = keccak256("Accountant"); - /// @notice Position ID for Debt Allocator + /// @notice Position ID for the "Debt Allocator". bytes32 public constant DEBT_ALLOCATOR = keccak256("Debt Allocator"); /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ - /// @notice Immutable address that the RoleManager position + /// @notice Immutable address that the `role_manager` position // will be transferred to when a vault is removed. address public chad; @@ -83,12 +83,13 @@ contract RoleManager is Positions { /// @notice Mapping of vault addresses to its config. mapping(address => VaultConfig) public vaultConfig; + /// @notice Mapping of underlying asset, api version and category to vault. mapping(address => mapping(string => mapping(uint256 => address))) internal _assetToVault; constructor() { - chad == address(1); + chad = address(1); } function initialize( @@ -103,10 +104,9 @@ contract RoleManager is Positions { require(chad == address(0), "initialized"); require(_governance != address(0), "ZERO ADDRESS"); - projectName = _projectName; chad = _governance; - - defaultProfitMaxUnlockTime = 10 days; + projectName = _projectName; + defaultProfitMaxUnlockTime = 7 days; // Governance gets all the roles. _setPositionHolder(GOVERNANCE, _governance); @@ -207,10 +207,11 @@ contract RoleManager is Positions { // Check that a vault does not exist for that asset, api and category. // This reverts late to not waste gas when used correctly. string memory _apiVersion = IVault(_vault).apiVersion(); - if (_assetToVault[_asset][_apiVersion][_category] != address(0)) + if (_assetToVault[_asset][_apiVersion][_category] != address(0)) { revert AlreadyDeployed( _assetToVault[_asset][_apiVersion][_category] ); + } address _debtAllocator = getPositionHolder(DEBT_ALLOCATOR); // Give out roles on the new vault. @@ -364,10 +365,11 @@ contract RoleManager is Positions { // Check that a vault does not exist for that asset, api and category. address _asset = IVault(_vault).asset(); string memory _apiVersion = IVault(_vault).apiVersion(); - if (_assetToVault[_asset][_apiVersion][_category] != address(0)) + if (_assetToVault[_asset][_apiVersion][_category] != address(0)) { revert AlreadyDeployed( _assetToVault[_asset][_apiVersion][_category] ); + } // If not the current role manager. if (IVault(_vault).role_manager() != address(this)) { @@ -650,6 +652,44 @@ contract RoleManager is Positions { return _assetToVault[_asset][_apiVersion][_category]; } + /** + * @notice Get the latest vault for a specific asset. + * @dev This will default to using category 1. + * @param _asset The underlying asset used. + * @return _vault latest vault for the specified `_asset` if any. + */ + function latestVault( + address _asset + ) external view virtual returns (address) { + return latestVault(_asset, 1); + } + + /** + * @notice Get the latest vault for a specific asset. + * @param _asset The underlying asset used. + * @param _category The category of the vault. + * @return _vault latest vault for the specified `_asset` if any. + */ + function latestVault( + address _asset, + uint256 _category + ) public view virtual returns (address _vault) { + address releaseRegistry = Registry(getPositionHolder(REGISTRY)) + .releaseRegistry(); + uint256 numReleases = ReleaseRegistry(releaseRegistry).numReleases(); + + for (uint256 i = numReleases; i > 0; --i) { + string memory apiVersion = IVaultFactory( + ReleaseRegistry(releaseRegistry).factories(i - 1) + ).apiVersion(); + + _vault = _assetToVault[_asset][apiVersion][_category]; + if (_vault != address(0)) { + break; + } + } + } + /** * @notice Check if a vault is managed by this contract. * @dev This will check if the `asset` variable in the struct has been diff --git a/src/managers/RoleManagerFactory.sol b/src/managers/RoleManagerFactory.sol index ef6c4a2..ea535fa 100644 --- a/src/managers/RoleManagerFactory.sol +++ b/src/managers/RoleManagerFactory.sol @@ -24,17 +24,17 @@ contract RoleManagerFactory is Clonable { address debtAllocator; } - bytes32 public constant KEEPER = keccak256("Keeper"); - /// @notice Position ID for the Registry. - bytes32 public constant REGISTRY_FACTORY = keccak256("Registry Factory"); /// @notice Position ID for the Accountant. bytes32 public constant ACCOUNTANT_FACTORY = keccak256("Accountant Factory"); /// @notice Position ID for Debt Allocator Factory bytes32 public constant DEBT_ALLOCATOR_FACTORY = keccak256("Debt Allocator Factory"); + bytes32 public constant KEEPER = keccak256("Keeper"); + /// @notice Position ID for the Registry. + bytes32 public constant REGISTRY_FACTORY = keccak256("Registry Factory"); - string public apiVersion = "v3.0.3"; + string public apiVersion = "3.0.3"; address public immutable protocolAddressProvider; @@ -83,11 +83,11 @@ contract RoleManagerFactory is Clonable { /** * @notice Create a new project with associated periphery contracts. - This will deploy and complete full setup with default configuration for - a new V3 project to exist. + * This will deploy and complete full setup with default configuration for + * a new V3 project to exist. * @param _name The name of the project * @param _governance The address of governance to use - * @param _management The address of management to use + * @param _management The address of management to use if any * @return _roleManager address of the newly created RoleManager for the project */ function newProject( @@ -98,7 +98,7 @@ contract RoleManagerFactory is Clonable { bytes32 _id = getProjectId(_name, _governance); require(projects[_id].roleManager == address(0), "project exists"); - // Deploy new Registry + // Deploy the needed periphery contracts. address _registry = RegistryFactory( _fromAddressProvider(REGISTRY_FACTORY) ).createNewRegistry(string(abi.encodePacked(_name, " Registry"))); @@ -107,10 +107,14 @@ contract RoleManagerFactory is Clonable { _fromAddressProvider(ACCOUNTANT_FACTORY) ).newAccountant(address(this), _governance); + // If management is not used, use governance as the default owner of the debt allocator. address _debtAllocator = DebtAllocatorFactory( _fromAddressProvider(DEBT_ALLOCATOR_FACTORY) - ).newDebtAllocator(_management); + ).newDebtAllocator( + _management != address(0) ? _management : _governance + ); + // Clone and initialize the RoleManager. _roleManager = _clone(); RoleManager(_roleManager).initialize( diff --git a/src/registry/Registry.sol b/src/registry/Registry.sol index c197c01..cf7d224 100644 --- a/src/registry/Registry.sol +++ b/src/registry/Registry.sol @@ -21,7 +21,7 @@ interface IVaultFactory { * @title YearnV3 Registry * @author yearn.finance * @notice - * Serves as an on chain registry to track any Yearn + * Serves as an on chain registry to track any Yearn V3 * vaults and strategies that a certain party wants to * endorse. * @@ -105,6 +105,9 @@ contract Registry is Governance { // Custom name for this Registry. string public name; + // Old version of the registry to fall back to if exists. + address public legacyRegistry; + // Mapping for any address that is allowed to tag a vault. mapping(address => bool) public taggers; @@ -209,7 +212,20 @@ contract Registry is Governance { * @return . The vaults endorsement status. */ function isEndorsed(address _vault) external view virtual returns (bool) { - return vaultInfo[_vault].asset != address(0); + return vaultInfo[_vault].asset != address(0) || isLegacyVault(_vault); + } + + /** + * @notice Check if a vault is endorsed in the legacy registry. + * @param _vault The vault to check. + * @return True if the vault is endorsed in the legacy registry, false otherwise. + */ + function isLegacyVault(address _vault) public view virtual returns (bool) { + address _legacy = legacyRegistry; + + if (_legacy == address(0)) return false; + + return Registry(_legacy).isEndorsed(_vault); } /** @@ -306,7 +322,7 @@ contract Registry is Governance { * @notice Endorse an already deployed multi strategy vault. * @dev To be used with default values for `_releaseDelta`, `_vaultType` * and `_deploymentTimestamp`. - + * * @param _vault Address of the vault to endorse. */ function endorseMultiStrategyVault(address _vault) external virtual { @@ -533,4 +549,14 @@ contract Registry is Governance { emit UpdateTagger(_account, _canTag); } + + /** + * @notice Set a legacy registry if one exists. + * @param _legacyRegistry The address of the legacy registry. + */ + function setLegacyRegistry( + address _legacyRegistry + ) external virtual onlyGovernance { + legacyRegistry = _legacyRegistry; + } } diff --git a/src/registry/RegistryFactory.sol b/src/registry/RegistryFactory.sol index 22bd878..dcc2135 100644 --- a/src/registry/RegistryFactory.sol +++ b/src/registry/RegistryFactory.sol @@ -24,7 +24,7 @@ contract RegistryFactory { } function name() external pure virtual returns (string memory) { - return "Custom Vault Registry Factory"; + return "Yearn V3 Vault Registry Factory"; } /** diff --git a/src/registry/ReleaseRegistry.sol b/src/registry/ReleaseRegistry.sol index bafdbeb..55c006b 100644 --- a/src/registry/ReleaseRegistry.sol +++ b/src/registry/ReleaseRegistry.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: GNU AGPLv3 pragma solidity >=0.8.18; -import {Governance} from "@periphery/utils/Governance.sol"; +import {Governance2Step} from "@periphery/utils/Governance2Step.sol"; interface IFactory { function apiVersion() external view returns (string memory); } +interface ITokenizedStrategy { + function apiVersion() external view returns (string memory); +} + /** * @title YearnV3 Release Registry * @author yearn.finance @@ -14,10 +18,11 @@ interface IFactory { * Used by Yearn Governance to track on chain all * releases of the V3 vaults by API Version. */ -contract ReleaseRegistry is Governance { +contract ReleaseRegistry is Governance2Step { event NewRelease( uint256 indexed releaseId, address indexed factory, + address indexed tokenizedStrategy, string apiVersion ); @@ -30,28 +35,44 @@ contract ReleaseRegistry is Governance { // of the corresponding factory for that release. mapping(uint256 => address) public factories; + // Mapping of release id starting at 0 to the address + // of the corresponding Tokenized Strategy for that release. + mapping(uint256 => address) public tokenizedStrategies; + // Mapping of the API version for a specific release to the // place in the order it was released. mapping(string => uint256) public releaseTargets; - constructor(address _governance) Governance(_governance) {} + constructor(address _governance) Governance2Step(_governance) {} /** * @notice Returns the latest factory. - * @dev Throws if no releases are registered yet. * @return The address of the factory for the latest release. */ function latestFactory() external view virtual returns (address) { + uint256 _numReleases = numReleases; + if (_numReleases == 0) return address(0); return factories[numReleases - 1]; } + /** + * @notice Returns the latest tokenized strategy. + * @return The address of the tokenized strategy for the latest release. + */ + function latestTokenizedStrategy() external view virtual returns (address) { + uint256 _numReleases = numReleases; + if (_numReleases == 0) return address(0); + return tokenizedStrategies[numReleases - 1]; + } + /** * @notice Returns the api version of the latest release. - * @dev Throws if no releases are registered yet. * @return The api version of the latest release. */ function latestRelease() external view virtual returns (string memory) { - return IFactory(factories[numReleases - 1]).apiVersion(); // dev: no release + uint256 _numReleases = numReleases; + if (_numReleases == 0) return ""; + return IFactory(factories[numReleases - 1]).apiVersion(); } /** @@ -61,15 +82,28 @@ contract ReleaseRegistry is Governance { * * Throws if caller isn't `governance`. * Throws if the api version is the same as the previous release. + * Throws if the factory does not have the same api version as the tokenized strategy. * Emits a `NewRelease` event. * * @param _factory The factory that will be used create new vaults. */ - function newRelease(address _factory) external virtual onlyGovernance { + function newRelease( + address _factory, + address _tokenizedStrategy + ) external virtual onlyGovernance { // Check if the release is different from the current one uint256 releaseId = numReleases; string memory apiVersion = IFactory(_factory).apiVersion(); + string memory tokenizedStrategyApiVersion = ITokenizedStrategy( + _tokenizedStrategy + ).apiVersion(); + + require( + keccak256(bytes(apiVersion)) == + keccak256(bytes(tokenizedStrategyApiVersion)), + "ReleaseRegistry: api version mismatch" + ); if (releaseId > 0) { // Make sure this isn't the same as the last one @@ -83,6 +117,7 @@ contract ReleaseRegistry is Governance { // Update latest release. factories[releaseId] = _factory; + tokenizedStrategies[releaseId] = _tokenizedStrategy; // Set the api to the target. releaseTargets[apiVersion] = releaseId; @@ -91,6 +126,6 @@ contract ReleaseRegistry is Governance { numReleases = releaseId + 1; // Log the release for external listeners - emit NewRelease(releaseId, _factory, apiVersion); + emit NewRelease(releaseId, _factory, _tokenizedStrategy, apiVersion); } } diff --git a/src/test/managers/TestRoleManager.t.sol b/src/test/managers/TestRoleManager.t.sol index 8c14cfc..1289fba 100644 --- a/src/test/managers/TestRoleManager.t.sol +++ b/src/test/managers/TestRoleManager.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import "forge-std/console2.sol"; +import {MockFactory} from "../../mocks/MockFactory.sol"; import {Setup, RoleManager, IVault, Roles, MockStrategy, DebtAllocator} from "../utils/Setup.sol"; contract TestRoleManager is Setup { @@ -64,7 +65,7 @@ contract TestRoleManager is Setup { strategy = createStrategy(address(asset)); vm.prank(daddy); - releaseRegistry.newRelease(address(vaultFactory)); + releaseRegistry.newRelease(address(vaultFactory), address(strategy)); } function test_role_manager_setup() public { @@ -551,7 +552,7 @@ contract TestRoleManager is Setup { assertEq(newVault.roles(brain), brain_roles); assertEq(newVault.roles(address(keeper)), keeper_roles); assertEq(newVault.roles(vaultDebtAllocator), debt_allocator_roles); - assertEq(newVault.profitMaxUnlockTime(), 10 days); + assertEq(newVault.profitMaxUnlockTime(), 7 days); assertEq(address(newVault.accountant()), address(accountant)); assertTrue(accountant.vaults(address(newVault))); @@ -1156,4 +1157,54 @@ contract TestRoleManager is Setup { assertEq(newVault.role_manager(), daddy); } + + function test_latestVault() public { + // Deploy multiple vaults with different API versions + vm.prank(daddy); + address vault1 = roleManager.newVault( + address(asset), + 1, + "Vault1", + "V1" + ); + + assertEq(roleManager.latestVault(address(asset)), vault1); + + vm.prank(daddy); + address vault2 = roleManager.newVault( + address(asset), + 2, + "Vault2", + "V2" + ); + + assertEq(roleManager.latestVault(address(asset)), vault1); + assertEq(roleManager.latestVault(address(asset), 2), vault2); + + MockFactory newFactory = new MockFactory("1.0.0"); + MockStrategy newStrategy = new MockStrategy(address(asset), "1.0.0"); + + vm.prank(daddy); + address vault4 = roleManager.newVault( + address(newStrategy), + 1, + "Vault4", + "V2" + ); + + assertEq(roleManager.latestVault(address(asset)), vault1); + assertEq(roleManager.latestVault(address(asset), 2), vault2); + assertEq(roleManager.latestVault(address(newStrategy)), vault4); + // Check for a non-existent asset + assertEq(roleManager.latestVault(address(0x123)), address(0)); + + releaseRegistry.newRelease(address(newFactory), address(newStrategy)); + + assertEq(roleManager.latestVault(address(asset)), vault1); + assertEq(roleManager.latestVault(address(asset), 2), vault2); + // Check that the latest vault is still vault3 + assertEq(roleManager.latestVault(address(newStrategy)), vault4); + // Check for a non-existent asset + assertEq(roleManager.latestVault(address(0x123)), address(0)); + } } diff --git a/src/test/registry/TestRegistry.t.sol b/src/test/registry/TestRegistry.t.sol index 9248370..5b01507 100644 --- a/src/test/registry/TestRegistry.t.sol +++ b/src/test/registry/TestRegistry.t.sol @@ -44,7 +44,7 @@ contract TestRegistry is Setup { } function test__deploy_new_vault() public { - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -103,7 +103,7 @@ contract TestRegistry is Setup { function test__endorse_deployed_vault() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -168,7 +168,7 @@ contract TestRegistry is Setup { function test__endorse_deployed_strategy() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -212,7 +212,7 @@ contract TestRegistry is Setup { function test__endorse_deployed_vault__default_values() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -270,7 +270,7 @@ contract TestRegistry is Setup { function test__endorse_deployed_strategy__default_values() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -315,13 +315,14 @@ contract TestRegistry is Setup { function test__deploy_vault_with_new_release() public { // Add a mock factory for version release 1 MockFactory mockFactory = new MockFactory("2.0.0"); + MockStrategy mockStrategy = new MockStrategy(address(asset), "2.0.0"); vm.prank(daddy); - releaseRegistry.newRelease(address(mockFactory)); + releaseRegistry.newRelease(address(mockFactory), address(mockStrategy)); assertEq(releaseRegistry.numReleases(), 1); // Add the factory as the second release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 2); @@ -380,14 +381,15 @@ contract TestRegistry is Setup { function test__deploy_vault_with_old_release() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); // Add a mock factory for version release 2 MockFactory mockFactory = new MockFactory("2.0.0"); + MockStrategy mockStrategy = new MockStrategy(address(asset), "2.0.0"); vm.prank(daddy); - releaseRegistry.newRelease(address(mockFactory)); + releaseRegistry.newRelease(address(mockFactory), address(mockStrategy)); assertEq(releaseRegistry.numReleases(), 2); @@ -443,16 +445,18 @@ contract TestRegistry is Setup { function test__endorse_deployed_vault_wrong_api__reverts() public { // Add a mock factory for version release 1 MockFactory mockFactory = new MockFactory("6.9"); + MockStrategy mockStrategy = new MockStrategy(address(asset), "6.9"); addNewRelease( releaseRegistry, IVaultFactory(address(mockFactory)), + address(mockStrategy), daddy ); assertEq(releaseRegistry.numReleases(), 1); // Set the factory as the second release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 2); @@ -483,16 +487,18 @@ contract TestRegistry is Setup { function test__endorse_strategy_wrong_api__reverts() public { // Add a mock factory for version release 1 MockFactory mockFactory = new MockFactory("6.9"); + MockStrategy mockStrategy = new MockStrategy(address(asset), "6.9"); addNewRelease( releaseRegistry, IVaultFactory(address(mockFactory)), + address(mockStrategy), daddy ); assertEq(releaseRegistry.numReleases(), 1); // Set the factory as the second release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 2); @@ -508,7 +514,7 @@ contract TestRegistry is Setup { function test__remove_vault() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -585,7 +591,7 @@ contract TestRegistry is Setup { function test__remove_vault__two_vaults() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -655,7 +661,7 @@ contract TestRegistry is Setup { function test__remove_strategy() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -711,7 +717,7 @@ contract TestRegistry is Setup { function test__remove_strategy__two_strategies() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -770,7 +776,7 @@ contract TestRegistry is Setup { function test__remove_asset() public { // Add the factory as the first release - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -804,7 +810,7 @@ contract TestRegistry is Setup { } function test__tag_vault() public { - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); assertEq(releaseRegistry.numReleases(), 1); @@ -863,7 +869,7 @@ contract TestRegistry is Setup { } function test__access() public { - addNewRelease(releaseRegistry, vaultFactory, daddy); + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); string memory name = "New vaults"; string memory symbol = "yvTest"; @@ -963,6 +969,94 @@ contract TestRegistry is Setup { registry.transferGovernance(user); } + function test__set_legacy_registry() public { + address mockLegacyRegistry = address(1234569); + + assertEq(registry.legacyRegistry(), address(0)); + + // Non-governance address can't set legacy registry + vm.prank(user); + vm.expectRevert("!governance"); + registry.setLegacyRegistry(mockLegacyRegistry); + + // Initially, legacy registry should be zero address + assertEq(registry.legacyRegistry(), address(0)); + + // Set legacy registry + vm.prank(daddy); + registry.setLegacyRegistry(mockLegacyRegistry); + + // Check if legacy registry is set correctly + assertEq(registry.legacyRegistry(), mockLegacyRegistry); + + // Can set to zero address + vm.prank(daddy); + registry.setLegacyRegistry(address(0)); + + assertEq(registry.legacyRegistry(), address(0)); + } + + function test__is_endorsed_legacy_vault() public { + addNewRelease(releaseRegistry, vaultFactory, address(strategy), daddy); + + // Deploy a mock legacy registry + address mockLegacyRegistry = registryFactory.createNewRegistry( + "mock ", + daddy + ); + + // Deploy a new vault + string memory name = "Legacy Vault"; + string memory symbol = "lvTest"; + + vm.prank(daddy); + address legacyVaultAddress = vaultFactory.deploy_new_vault( + address(asset), + name, + symbol, + daddy, + WEEK + ); + + assertEq(registry.isEndorsed(legacyVaultAddress), false); + assertEq( + Registry(mockLegacyRegistry).isEndorsed(legacyVaultAddress), + false + ); + + // Endorse the vault in the legacy registry + vm.prank(daddy); + Registry(mockLegacyRegistry).endorseMultiStrategyVault( + legacyVaultAddress + ); + + assertEq(registry.isEndorsed(legacyVaultAddress), false); + assertEq( + Registry(mockLegacyRegistry).isEndorsed(legacyVaultAddress), + true + ); + + // Set the mock legacy registry in the main registry + vm.prank(daddy); + registry.setLegacyRegistry(address(mockLegacyRegistry)); + + assertEq(registry.isEndorsed(legacyVaultAddress), true); + assertEq( + Registry(mockLegacyRegistry).isEndorsed(legacyVaultAddress), + true + ); + + // Check that the vault is not endorsed in the main registry + assertEq(registry.isLegacyVault(legacyVaultAddress), true); + assertEq( + Registry(mockLegacyRegistry).isLegacyVault(legacyVaultAddress), + false + ); + + (address vaultAsset, , , , , ) = registry.vaultInfo(legacyVaultAddress); + assertEq(vaultAsset, address(0)); + } + function test__transfer_governance() public { assertEq(registry.governance(), daddy); @@ -982,9 +1076,10 @@ contract TestRegistry is Setup { function addNewRelease( ReleaseRegistry _releaseRegistry, IVaultFactory _factory, + address _tokenizedStrategy, address _owner ) internal { vm.prank(_owner); - _releaseRegistry.newRelease(address(_factory)); + _releaseRegistry.newRelease(address(_factory), _tokenizedStrategy); } } diff --git a/src/test/registry/TestRegistryFactory.t.sol b/src/test/registry/TestRegistryFactory.t.sol index db192f3..821c2db 100644 --- a/src/test/registry/TestRegistryFactory.t.sol +++ b/src/test/registry/TestRegistryFactory.t.sol @@ -13,7 +13,7 @@ contract TestRegistryFactory is Setup { address(registryFactory.releaseRegistry()), address(releaseRegistry) ); - assertEq(registryFactory.name(), "Custom Vault Registry Factory"); + assertEq(registryFactory.name(), "Yearn V3 Vault Registry Factory"); } function test__new_registry() public { diff --git a/src/test/registry/TestReleaseRegistry.t.sol b/src/test/registry/TestReleaseRegistry.t.sol index 60aa546..9e04516 100644 --- a/src/test/registry/TestReleaseRegistry.t.sol +++ b/src/test/registry/TestReleaseRegistry.t.sol @@ -8,6 +8,7 @@ contract TestReleaseRegistry is Setup { event NewRelease( uint256 indexed releaseId, address indexed factory, + address indexed tokenizedStrategy, string apiVersion ); event GovernanceTransferred( @@ -15,50 +16,73 @@ contract TestReleaseRegistry is Setup { address indexed newGovernance ); + address public tokenizedStrategy; + function setUp() public override { super.setUp(); + tokenizedStrategy = address( + new MockStrategy(address(asset), vaultFactory.apiVersion()) + ); } function test__deployment() public { assertEq(releaseRegistry.governance(), daddy); assertEq(releaseRegistry.numReleases(), 0); assertEq(releaseRegistry.factories(0), address(0)); + assertEq(releaseRegistry.tokenizedStrategies(0), address(0)); assertEq(releaseRegistry.releaseTargets("3.0.3"), 0); } function test_new_release() public { assertEq(releaseRegistry.numReleases(), 0); assertEq(releaseRegistry.factories(0), address(0)); - + assertEq(releaseRegistry.tokenizedStrategies(0), address(0)); vm.prank(daddy); vm.expectEmit(true, true, false, true); - emit NewRelease(0, address(vaultFactory), vaultFactory.apiVersion()); - releaseRegistry.newRelease(address(vaultFactory)); + emit NewRelease( + 0, + address(vaultFactory), + tokenizedStrategy, + vaultFactory.apiVersion() + ); + releaseRegistry.newRelease(address(vaultFactory), tokenizedStrategy); assertEq(releaseRegistry.numReleases(), 1); assertEq(releaseRegistry.factories(0), address(vaultFactory)); + assertEq(releaseRegistry.tokenizedStrategies(0), tokenizedStrategy); assertEq(releaseRegistry.releaseTargets(vaultFactory.apiVersion()), 0); assertEq(releaseRegistry.latestFactory(), address(vaultFactory)); assertEq(releaseRegistry.latestRelease(), vaultFactory.apiVersion()); - + assertEq(releaseRegistry.latestTokenizedStrategy(), tokenizedStrategy); string memory new_api = "4.3.2"; // Deploy a new mock factory with a different api MockFactory new_factory = new MockFactory(new_api); + MockStrategy new_strategy = new MockStrategy(address(asset), new_api); vm.prank(daddy); vm.expectEmit(true, true, false, true); - emit NewRelease(1, address(new_factory), new_api); - releaseRegistry.newRelease(address(new_factory)); + emit NewRelease( + 1, + address(new_factory), + address(new_strategy), + new_api + ); + releaseRegistry.newRelease(address(new_factory), address(new_strategy)); assertEq(releaseRegistry.numReleases(), 2); assertEq(releaseRegistry.factories(1), address(new_factory)); + assertEq(releaseRegistry.tokenizedStrategies(1), address(new_strategy)); assertEq(releaseRegistry.releaseTargets(new_api), 1); assertEq(releaseRegistry.latestFactory(), address(new_factory)); assertEq(releaseRegistry.latestRelease(), new_api); - + assertEq( + releaseRegistry.latestTokenizedStrategy(), + address(new_strategy) + ); // make sure the first factory is still returning assertEq(releaseRegistry.factories(0), address(vaultFactory)); assertEq(releaseRegistry.releaseTargets(vaultFactory.apiVersion()), 0); + assertEq(releaseRegistry.tokenizedStrategies(0), tokenizedStrategy); } function test_access() public { @@ -68,16 +92,27 @@ contract TestReleaseRegistry is Setup { // only daddy should be able to set a new release vm.prank(user); vm.expectRevert(); - releaseRegistry.newRelease(address(vaultFactory)); + releaseRegistry.newRelease( + address(vaultFactory), + address(tokenizedStrategy) + ); assertEq(releaseRegistry.numReleases(), 0); assertEq(releaseRegistry.factories(0), address(0)); + assertEq(releaseRegistry.tokenizedStrategies(0), address(0)); vm.prank(daddy); - releaseRegistry.newRelease(address(vaultFactory)); + releaseRegistry.newRelease( + address(vaultFactory), + address(tokenizedStrategy) + ); assertEq(releaseRegistry.numReleases(), 1); assertEq(releaseRegistry.factories(0), address(vaultFactory)); + assertEq( + releaseRegistry.tokenizedStrategies(0), + address(tokenizedStrategy) + ); } function test__add_same_factory() public { @@ -86,35 +121,68 @@ contract TestReleaseRegistry is Setup { vm.prank(daddy); vm.expectEmit(true, true, false, true); - emit NewRelease(0, address(vaultFactory), vaultFactory.apiVersion()); - releaseRegistry.newRelease(address(vaultFactory)); + emit NewRelease( + 0, + address(vaultFactory), + tokenizedStrategy, + vaultFactory.apiVersion() + ); + releaseRegistry.newRelease(address(vaultFactory), tokenizedStrategy); assertEq(releaseRegistry.numReleases(), 1); assertEq(releaseRegistry.factories(0), address(vaultFactory)); assertEq(releaseRegistry.latestFactory(), address(vaultFactory)); assertEq(releaseRegistry.latestRelease(), vaultFactory.apiVersion()); + assertEq(releaseRegistry.tokenizedStrategies(0), tokenizedStrategy); + assertEq(releaseRegistry.latestTokenizedStrategy(), tokenizedStrategy); vm.prank(daddy); vm.expectRevert("ReleaseRegistry: same api version"); - releaseRegistry.newRelease(address(vaultFactory)); + releaseRegistry.newRelease(address(vaultFactory), tokenizedStrategy); assertEq(releaseRegistry.numReleases(), 1); } - function test__transfer_governance() public { + function test_revert_mismatched_api_versions() public { + // Deploy a new vault factory with a different API version + MockFactory newVaultFactory = new MockFactory("2.0.0"); + + vm.prank(daddy); + vm.expectRevert("ReleaseRegistry: api version mismatch"); + releaseRegistry.newRelease(address(newVaultFactory), tokenizedStrategy); + + // Ensure no new release was added + assertEq(releaseRegistry.numReleases(), 0); + assertEq(releaseRegistry.factories(0), address(0)); + assertEq(releaseRegistry.tokenizedStrategies(0), address(0)); + } + + function test__transfer_governance_two_step() public { + address newGovernance = user; + + // Initial state assertEq(releaseRegistry.governance(), daddy); + assertEq(releaseRegistry.pendingGovernance(), address(0)); + // Step 1: Current governance initiates transfer vm.prank(daddy); - vm.expectRevert("ZERO ADDRESS"); - releaseRegistry.transferGovernance(address(0)); + releaseRegistry.transferGovernance(newGovernance); + // Check intermediate state assertEq(releaseRegistry.governance(), daddy); + assertEq(releaseRegistry.pendingGovernance(), newGovernance); + // Attempt to accept from wrong address vm.prank(daddy); - vm.expectEmit(true, true, false, true); - emit GovernanceTransferred(daddy, user); - releaseRegistry.transferGovernance(user); + vm.expectRevert("!pending governance"); + releaseRegistry.acceptGovernance(); + + // Step 2: New governance accepts transfer + vm.prank(newGovernance); + releaseRegistry.acceptGovernance(); - assertEq(releaseRegistry.governance(), user); + // Check final state + assertEq(releaseRegistry.governance(), newGovernance); + assertEq(releaseRegistry.pendingGovernance(), address(0)); } } diff --git a/src/test/utils/VyperDeployer.sol b/src/test/utils/VyperDeployer.sol index b739b6e..fe9cd1f 100644 --- a/src/test/utils/VyperDeployer.sol +++ b/src/test/utils/VyperDeployer.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.13; + import "forge-std/console.sol"; ///@notice This cheat codes interface is named _CheatCodes so you can use the CheatCodes interface in other testing files without errors @@ -15,7 +16,6 @@ interface _CheatCodes { * and deploys the corresponding Vyper contract, returning the address * that the bytecode was deployed to. */ - contract VyperDeployer { address constant HEVM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));