diff --git a/contracts/interfaces/IEthErc20MetaVault.sol b/contracts/interfaces/IEthErc20MetaVault.sol new file mode 100644 index 00000000..c1b757ef --- /dev/null +++ b/contracts/interfaces/IEthErc20MetaVault.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IKeeperRewards} from "./IKeeperRewards.sol"; +import {IVaultAdmin} from "./IVaultAdmin.sol"; +import {IVaultVersion} from "./IVaultVersion.sol"; +import {IVaultFee} from "./IVaultFee.sol"; +import {IVaultState} from "./IVaultState.sol"; +import {IVaultEnterExit} from "./IVaultEnterExit.sol"; +import {IVaultOsToken} from "./IVaultOsToken.sol"; +import {IVaultSubVaults} from "./IVaultSubVaults.sol"; +import {IVaultToken} from "./IVaultToken.sol"; +import {IMulticall} from "./IMulticall.sol"; + +/** + * @title IEthErc20MetaVault + * @author StakeWise + * @notice Defines the interface for the EthErc20MetaVault contract + */ +interface IEthErc20MetaVault is + IVaultAdmin, + IVaultVersion, + IVaultFee, + IVaultState, + IVaultEnterExit, + IVaultOsToken, + IVaultToken, + IVaultSubVaults, + IMulticall +{ + /** + * @dev Struct for deploying the EthErc20MetaVault contract + * @param keeper The address of the Keeper contract + * @param vaultsRegistry The address of the VaultsRegistry contract + * @param osTokenVaultController The address of the OsTokenVaultController contract + * @param osTokenConfig The address of the OsTokenConfig contract + * @param osTokenVaultEscrow The address of the OsTokenVaultEscrow contract + * @param subVaultsRegistryFactory The address of the factory for creating SubVaultsRegistry contracts + * @param exitingAssetsClaimDelay The delay after which the assets can be claimed after exiting from staking + */ + struct EthErc20MetaVaultConstructorArgs { + address keeper; + address vaultsRegistry; + address osTokenVaultController; + address osTokenConfig; + address osTokenVaultEscrow; + address subVaultsRegistryFactory; + uint64 exitingAssetsClaimDelay; + } + + /** + * @dev Struct for initializing the EthErc20MetaVault contract + * @param subVaultsCurator The address of the initial sub-vaults curator + * @param capacity The Vault stops accepting deposits after exceeding the capacity + * @param feePercent The fee percent that is charged by the Vault + * @param name The name of the ERC20 token + * @param symbol The symbol of the ERC20 token + * @param metadataIpfsHash The IPFS hash of the Vault's metadata file + */ + struct EthErc20MetaVaultInitParams { + address subVaultsCurator; + uint256 capacity; + uint16 feePercent; + string name; + string symbol; + string metadataIpfsHash; + } + + /** + * @notice Initializes or upgrades the EthErc20MetaVault contract. Must transfer security deposit during the deployment. + * @param params The encoded parameters for initializing the EthErc20MetaVault contract + */ + function initialize(bytes calldata params) external payable; + + /** + * @notice Deposit ETH to the Vault + * @param receiver The address that will receive Vault's shares + * @param referrer The address of the referrer. Set to zero address if not used. + * @return shares The number of shares minted + */ + function deposit(address receiver, address referrer) external payable returns (uint256 shares); + + /** + * @notice Updates Vault state and deposits ETH to the Vault + * @param receiver The address that will receive Vault's shares + * @param referrer The address of the referrer. Set to zero address if not used. + * @param harvestParams The parameters for harvesting Keeper rewards + * @return shares The number of shares minted + */ + function updateStateAndDeposit( + address receiver, + address referrer, + IKeeperRewards.HarvestParams calldata harvestParams + ) external payable returns (uint256 shares); + + /** + * @notice Deposits assets to the vault and mints OsToken shares to the receiver + * @param receiver The address to receive the OsToken + * @param osTokenShares The amount of OsToken shares to mint. + * If set to type(uint256).max, max OsToken shares will be minted. + * @param referrer The address of the referrer + * @return The amount of OsToken assets minted + */ + function depositAndMintOsToken(address receiver, uint256 osTokenShares, address referrer) + external + payable + returns (uint256); + + /** + * @notice Updates the state, deposits assets to the vault and mints OsToken shares to the receiver + * @param receiver The address to receive the OsToken + * @param osTokenShares The amount of OsToken shares to mint. + * If set to type(uint256).max, max OsToken shares will be minted. + * @param referrer The address of the referrer + * @param harvestParams The parameters for the harvest + * @return The amount of OsToken assets minted + */ + function updateStateAndDepositAndMintOsToken( + address receiver, + uint256 osTokenShares, + address referrer, + IKeeperRewards.HarvestParams calldata harvestParams + ) external payable returns (uint256); + + /** + * @notice Donate assets to the Vault. Must transfer ETH together with the call. + */ + function donateAssets() external payable; +} diff --git a/contracts/interfaces/IEthMetaVault.sol b/contracts/interfaces/IEthMetaVault.sol index 9e1ddb35..5f91f9fa 100644 --- a/contracts/interfaces/IEthMetaVault.sol +++ b/contracts/interfaces/IEthMetaVault.sol @@ -3,17 +3,67 @@ pragma solidity ^0.8.22; import {IKeeperRewards} from "./IKeeperRewards.sol"; -import {IMetaVault} from "./IMetaVault.sol"; +import {IVaultAdmin} from "./IVaultAdmin.sol"; +import {IVaultVersion} from "./IVaultVersion.sol"; +import {IVaultFee} from "./IVaultFee.sol"; +import {IVaultState} from "./IVaultState.sol"; +import {IVaultEnterExit} from "./IVaultEnterExit.sol"; +import {IVaultOsToken} from "./IVaultOsToken.sol"; +import {IVaultSubVaults} from "./IVaultSubVaults.sol"; +import {IMulticall} from "./IMulticall.sol"; /** * @title IEthMetaVault * @author StakeWise * @notice Defines the interface for the EthMetaVault contract */ -interface IEthMetaVault is IMetaVault { +interface IEthMetaVault is + IVaultAdmin, + IVaultVersion, + IVaultFee, + IVaultState, + IVaultEnterExit, + IVaultOsToken, + IVaultSubVaults, + IMulticall +{ + /** + * @dev Struct for deploying the EthMetaVault contract + * @param keeper The address of the Keeper contract + * @param vaultsRegistry The address of the VaultsRegistry contract + * @param osTokenVaultController The address of the OsTokenVaultController contract + * @param osTokenConfig The address of the OsTokenConfig contract + * @param osTokenVaultEscrow The address of the OsTokenVaultEscrow contract + * @param subVaultsRegistryFactory The address of the factory for creating SubVaultsRegistry contracts + * @param exitingAssetsClaimDelay The delay after which the assets can be claimed after exiting from staking + */ + struct EthMetaVaultConstructorArgs { + address keeper; + address vaultsRegistry; + address osTokenVaultController; + address osTokenConfig; + address osTokenVaultEscrow; + address subVaultsRegistryFactory; + uint64 exitingAssetsClaimDelay; + } + + /** + * @dev Struct for initializing the EthMetaVault contract + * @param subVaultsCurator The address of the initial sub-vaults curator + * @param capacity The Vault stops accepting deposits after exceeding the capacity + * @param feePercent The fee percent that is charged by the Vault + * @param metadataIpfsHash The IPFS hash of the Vault's metadata file + */ + struct EthMetaVaultInitParams { + address subVaultsCurator; + uint256 capacity; + uint16 feePercent; + string metadataIpfsHash; + } + /** * @notice Initializes or upgrades the EthMetaVault contract. Must transfer security deposit during the deployment. - * @param params The encoded parameters for initializing the EthVault contract + * @param params The encoded parameters for initializing the EthMetaVault contract */ function initialize(bytes calldata params) external payable; diff --git a/contracts/interfaces/IEthPrivErc20MetaVault.sol b/contracts/interfaces/IEthPrivErc20MetaVault.sol new file mode 100644 index 00000000..5d8e1546 --- /dev/null +++ b/contracts/interfaces/IEthPrivErc20MetaVault.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IVaultWhitelist} from "./IVaultWhitelist.sol"; +import {IEthErc20MetaVault} from "./IEthErc20MetaVault.sol"; + +/** + * @title IEthPrivErc20MetaVault + * @author StakeWise + * @notice Defines the interface for the EthPrivErc20MetaVault contract + */ +interface IEthPrivErc20MetaVault is IEthErc20MetaVault, IVaultWhitelist {} diff --git a/contracts/interfaces/IGnoMetaVault.sol b/contracts/interfaces/IGnoMetaVault.sol index 439b19a3..f4525d3d 100644 --- a/contracts/interfaces/IGnoMetaVault.sol +++ b/contracts/interfaces/IGnoMetaVault.sol @@ -2,17 +2,67 @@ pragma solidity ^0.8.22; -import {IMetaVault} from "./IMetaVault.sol"; +import {IVaultAdmin} from "./IVaultAdmin.sol"; +import {IVaultVersion} from "./IVaultVersion.sol"; +import {IVaultFee} from "./IVaultFee.sol"; +import {IVaultState} from "./IVaultState.sol"; +import {IVaultEnterExit} from "./IVaultEnterExit.sol"; +import {IVaultOsToken} from "./IVaultOsToken.sol"; +import {IVaultSubVaults} from "./IVaultSubVaults.sol"; +import {IMulticall} from "./IMulticall.sol"; /** * @title IGnoMetaVault * @author StakeWise * @notice Defines the interface for the GnoMetaVault contract */ -interface IGnoMetaVault is IMetaVault { +interface IGnoMetaVault is + IVaultAdmin, + IVaultVersion, + IVaultFee, + IVaultState, + IVaultEnterExit, + IVaultOsToken, + IVaultSubVaults, + IMulticall +{ + /** + * @dev Struct for deploying the GnoMetaVault contract + * @param keeper The address of the Keeper contract + * @param vaultsRegistry The address of the VaultsRegistry contract + * @param osTokenVaultController The address of the OsTokenVaultController contract + * @param osTokenConfig The address of the OsTokenConfig contract + * @param osTokenVaultEscrow The address of the OsTokenVaultEscrow contract + * @param subVaultsRegistryFactory The address of the factory for creating SubVaultsRegistry contracts + * @param exitingAssetsClaimDelay The delay after which the assets can be claimed after exiting from staking + */ + struct GnoMetaVaultConstructorArgs { + address keeper; + address vaultsRegistry; + address osTokenVaultController; + address osTokenConfig; + address osTokenVaultEscrow; + address subVaultsRegistryFactory; + uint64 exitingAssetsClaimDelay; + } + + /** + * @dev Struct for initializing the GnoMetaVault contract + * @param subVaultsCurator The address of the initial sub-vaults curator + * @param capacity The Vault stops accepting deposits after exceeding the capacity + * @param feePercent The fee percent that is charged by the Vault + * @param metadataIpfsHash The IPFS hash of the Vault's metadata file + */ + struct GnoMetaVaultInitParams { + address subVaultsCurator; + uint256 capacity; + uint16 feePercent; + string metadataIpfsHash; + } + /** * @notice Initializes or upgrades the GnoMetaVault contract. Must transfer security deposit during the deployment. - * @param params The encoded parameters for initializing the GnoVault contract + * @param params The encoded parameters for initializing the GnoMetaVault contract */ function initialize(bytes calldata params) external; diff --git a/contracts/interfaces/IGnoPrivMetaVault.sol b/contracts/interfaces/IGnoPrivMetaVault.sol deleted file mode 100644 index 6ea8513e..00000000 --- a/contracts/interfaces/IGnoPrivMetaVault.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.22; - -import {IVaultWhitelist} from "./IVaultWhitelist.sol"; -import {IGnoMetaVault} from "./IGnoMetaVault.sol"; - -/** - * @title IGnoPrivMetaVault - * @author StakeWise - * @notice Defines the interface for the GnoPrivMetaVault contract - */ -interface IGnoPrivMetaVault is IGnoMetaVault, IVaultWhitelist {} diff --git a/contracts/interfaces/IMetaVault.sol b/contracts/interfaces/IMetaVault.sol deleted file mode 100644 index 49be4645..00000000 --- a/contracts/interfaces/IMetaVault.sol +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.22; - -import {IVaultAdmin} from "./IVaultAdmin.sol"; -import {IVaultVersion} from "./IVaultVersion.sol"; -import {IVaultFee} from "./IVaultFee.sol"; -import {IVaultState} from "./IVaultState.sol"; -import {IVaultEnterExit} from "./IVaultEnterExit.sol"; -import {IVaultOsToken} from "./IVaultOsToken.sol"; -import {IVaultSubVaults} from "./IVaultSubVaults.sol"; -import {IMulticall} from "./IMulticall.sol"; -import {ISubVaultsCurator} from "./ISubVaultsCurator.sol"; - -/** - * @title IMetaVault - * @author StakeWise - * @notice Defines the interface for the MetaVault contract - */ -interface IMetaVault is - IVaultAdmin, - IVaultVersion, - IVaultFee, - IVaultState, - IVaultEnterExit, - IVaultOsToken, - IVaultSubVaults, - IMulticall -{ - /** - * @notice Event emitted when assets are redeemed from sub-vaults - * @param assetsRedeemed The amount of assets redeemed to the meta vault - */ - event SubVaultsAssetsRedeemed(uint256 assetsRedeemed); - - /** - * @dev Struct for deploying the MetaVault contract - * @param keeper The address of the Keeper contract - * @param vaultsRegistry The address of the VaultsRegistry contract - * @param osTokenVaultController The address of the OsTokenVaultController contract - * @param osTokenConfig The address of the OsTokenConfig contract - * @param osTokenVaultEscrow The address of the OsTokenVaultEscrow contract - * @param curatorsRegistry The address of the CuratorsRegistry contract - * @param exitingAssetsClaimDelay The delay after which the assets can be claimed after exiting from staking - */ - struct MetaVaultConstructorArgs { - address keeper; - address vaultsRegistry; - address osTokenVaultController; - address osTokenConfig; - address osTokenVaultEscrow; - address curatorsRegistry; - uint64 exitingAssetsClaimDelay; - } - - /** - * @dev Struct for initializing the MetaVault contract - * @param subVaultsCurator The address of the initial sub-vaults curator - * @param capacity The Vault stops accepting deposits after exceeding the capacity - * @param feePercent The fee percent that is charged by the Vault - * @param metadataIpfsHash The IPFS hash of the Vault's metadata file - */ - struct MetaVaultInitParams { - address subVaultsCurator; - uint256 capacity; - uint16 feePercent; - string metadataIpfsHash; - } - - /** - * @notice Calculates the required sub-vaults exit requests to fulfill the assets to redeem - * @param assetsToRedeem The amount of assets to redeem - * @return redeemRequests The array of sub-vaults exit requests - */ - function calculateSubVaultsRedemptions(uint256 assetsToRedeem) - external - view - returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests); - - /** - * @notice Redeems assets from sub-vaults to the meta vault. Can only be called by the redeemer. - * @param assetsToRedeem The amount of assets to redeem to the meta vault - * @return totalRedeemedAssets The total amount of assets redeemed from sub-vaults - */ - function redeemSubVaultsAssets(uint256 assetsToRedeem) external returns (uint256 totalRedeemedAssets); -} diff --git a/contracts/interfaces/IOsTokenRedeemer.sol b/contracts/interfaces/IOsTokenRedeemer.sol index 609532c3..15d2ea94 100644 --- a/contracts/interfaces/IOsTokenRedeemer.sol +++ b/contracts/interfaces/IOsTokenRedeemer.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.22; +import {IKeeperRewards} from "./IKeeperRewards.sol"; import {IMulticall} from "./IMulticall.sol"; /** @@ -289,6 +290,13 @@ interface IOsTokenRedeemer is IMulticall { bool[] calldata proofFlags ) external; + /** + * @notice Updates the vault state. To be used in multicall to update state and redeem positions. + * @param vault The address of the vault to update + * @param harvestParams The harvest parameters for the vault state update + */ + function updateVaultState(address vault, IKeeperRewards.HarvestParams calldata harvestParams) external; + /** * @notice Process the exit queue and checkpoint swapped or redeemed shares. Can only be called once per `exitQueueUpdateDelay`. */ diff --git a/contracts/interfaces/ISubVaultsRegistry.sol b/contracts/interfaces/ISubVaultsRegistry.sol new file mode 100644 index 00000000..9a873ffc --- /dev/null +++ b/contracts/interfaces/ISubVaultsRegistry.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IMulticall} from "./IMulticall.sol"; +import {ISubVaultsCurator} from "./ISubVaultsCurator.sol"; + +/** + * @title ISubVaultsRegistry + * @author StakeWise + * @notice Defines the interface for the SubVaultsRegistry contract + */ +interface ISubVaultsRegistry is IMulticall { + /** + * @notice Struct for sub vault exit request + * @param exitQueueIndex The index of the exit queue + * @param vault The address of the sub vault + * @param timestamp The timestamp of the exit request + */ + struct SubVaultExitRequest { + uint256 exitQueueIndex; + address vault; + uint64 timestamp; + } + + /** + * @notice Struct for migration data + * @param curator The address of the sub-vaults curator + * @param ejectingSubVault The address of the ejecting sub-vault + * @param ejectingSubVaultShares The number of shares of the ejecting sub-vault + * @param subVaultsRewardsNonce The rewards nonce + * @param subVaultsTotalAssets The total assets in sub-vaults + * @param totalProcessedExitQueueTickets The total processed exit queue tickets + * @param subVaults The array of sub-vault addresses + * @param subVaultsStates The array of sub-vault states + * @param subVaultsExits The array of sub-vault exits + */ + struct MigrationData { + address curator; + address ejectingSubVault; + uint256 ejectingSubVaultShares; + uint128 subVaultsRewardsNonce; + uint128 subVaultsTotalAssets; + uint256 totalProcessedExitQueueTickets; + address[] subVaults; + SubVaultState[] subVaultsStates; + bytes32[][] subVaultsExits; + } + + /** + * @notice Struct for sub vault state + * @param stakedShares The number of shares staked in the sub vault + * @param queuedShares The number of shares queued for exit in the sub vault + */ + struct SubVaultState { + uint128 stakedShares; + uint128 queuedShares; + } + + /** + * @notice Emitted when the sub vaults are harvested + * @param totalAssetsDelta The change in total assets after the harvest + */ + event SubVaultsHarvested(int256 totalAssetsDelta); + + /** + * @notice Emitted when the sub-vaults curator is updated + * @param curator The address of the new sub-vaults curator + */ + event SubVaultsCuratorUpdated(address indexed curator); + + /** + * @notice Emitted when the rewards nonce is updated + * @param rewardsNonce The new rewards nonce + */ + event RewardsNonceUpdated(uint256 rewardsNonce); + + /** + * @notice Emitted when a new sub-vault is added + * @param vault The address of the sub-vault + */ + event SubVaultAdded(address indexed vault); + + /** + * @notice Emitted when a new meta sub-vault is proposed + * @param vault The address of the meta sub-vault + */ + event MetaSubVaultProposed(address indexed vault); + + /** + * @notice Emitted when a meta sub-vault is rejected + * @param vault The address of the meta sub-vault + */ + event MetaSubVaultRejected(address indexed vault); + + /** + * @notice Emitted when a sub-vault is ejecting + * @param vault The address of the sub-vault + */ + event SubVaultEjecting(address indexed vault); + + /** + * @notice Emitted when a sub-vault is ejected + * @param vault The address of the sub-vault + */ + event SubVaultEjected(address indexed vault); + + /** + * @notice Event emitted when assets are redeemed from sub-vaults + * @param assetsRedeemed The amount of assets redeemed to the meta vault + */ + event SubVaultsAssetsRedeemed(uint256 assetsRedeemed); + + /** + * @notice Event emitted when state is migrated from meta vault + * @param metaVault The address of the meta vault + */ + event Migrated(address indexed metaVault); + + /** + * @notice The address of the meta vault + * @return The address of the meta vault + */ + function metaVault() external view returns (address); + + /** + * @notice The address of the sub-vaults curator + * @return The address of the sub-vaults curator + */ + function subVaultsCurator() external view returns (address); + + /** + * @notice Pending meta sub-vault waiting for approval + * @return The address of the pending meta sub-vault + */ + function pendingMetaSubVault() external view returns (address); + + /** + * @notice Function to get the rewards nonce of the sub-vaults + * @return The rewards nonce + */ + function subVaultsRewardsNonce() external view returns (uint128); + + /** + * @notice The address of the sub-vault being ejected + * @return The address of the ejecting sub-vault + */ + function ejectingSubVault() external view returns (address); + + /** + * @notice The number of shares of the ejecting sub-vault + * @return The number of shares + */ + function ejectingSubVaultShares() external view returns (uint256); + + /** + * @notice Returns the state of a sub-vault + * @param vault The address of the sub-vault + * @return The state of the sub-vault + */ + function subVaultsStates(address vault) external view returns (SubVaultState memory); + + /** + * @notice Returns the exits queue for a sub-vault + * @param vault The address of the sub-vault + * @return The array of packed exit data (positionTicket: uint160, shares: uint96) + */ + function subVaultsExits(address vault) external view returns (bytes32[] memory); + + /** + * @notice Returns the list of sub-vaults + * @return The array of sub-vault addresses + */ + function getSubVaults() external view returns (address[] memory); + + /** + * @notice Checks if the given address is a sub-vault + * @param vault The address to check + * @return True if the address is a sub-vault, false otherwise + */ + function isSubVault(address vault) external view returns (bool); + + /** + * @notice Initializes the SubVaultsRegistry + * @param metaVault The address of the meta vault + * @param curator The address of initial sub-vaults curator + */ + function initialize(address metaVault, address curator) external; + + /** + * @notice Migrates state from meta vault to SubVaultsRegistry. Can only be called once by the meta vault. + * @param data The migration data containing all state to migrate + */ + function migrate(MigrationData calldata data) external; + + /** + * @notice Function to update the sub-vaults curator. Can only be called by the meta vault admin. + * @param curator The address of the new sub-vaults curator + */ + function setSubVaultsCurator(address curator) external; + + /** + * @notice Function to add a new sub-vault. Can only be called by the meta vault admin. + * @param vault The address of the sub-vault to add + */ + function addSubVault(address vault) external; + + /** + * @notice Function to accept a meta sub-vault. Can only be called by the VaultsRegistry owner. + * @param metaSubVault The address of the meta sub-vault to accept + */ + function acceptMetaSubVault(address metaSubVault) external; + + /** + * @notice Function to reject a meta sub-vault. Can only be called by the VaultsRegistry owner or meta vault admin. + * @param metaSubVault The address of the meta sub-vault to reject + */ + function rejectMetaSubVault(address metaSubVault) external; + + /** + * @notice Function to eject a sub-vault. Can only be called by the meta vault admin. + * All the sub-vault shares will be added to the exit queue. + * @param vault The address of the sub-vault to eject + */ + function ejectSubVault(address vault) external; + + /** + * @notice Checks whether the state can be updated + * @return True if the state can be updated, false otherwise + */ + function canUpdateState() external view returns (bool); + + /** + * @notice Checks whether the meta vault is collateralized + * @return True if the meta vault is collateralized, false otherwise + */ + function isCollateralized() external view returns (bool); + + /** + * @notice The total assets deposited to sub-vaults + */ + function subVaultsTotalAssets() external view returns (uint128); + + /** + * @notice Checks whether the state update is required + * @return True if the state update is required, false otherwise + */ + function isStateUpdateRequired() external view returns (bool); + + /** + * @notice Deposit available assets to the sub vaults + */ + function depositToSubVaults() external; + + /** + * @notice Claims exited assets from sub vaults + * @param exitRequests The array of exit requests to claim + */ + function claimSubVaultsExitedAssets(SubVaultExitRequest[] calldata exitRequests) external; + + /** + * @notice Harvests sub-vaults assets. Can only be called by the meta vault. + * @return totalAssetsDelta The change in total assets after the harvest + * @return harvested Whether the sub-vaults were harvested + */ + function harvestSubVaultsAssets() external returns (int256 totalAssetsDelta, bool harvested); + + /** + * @notice Enters the exit queue for sub-vaults. Can only be called by the meta vault. + */ + function enterSubVaultsExitQueue() external; + + /** + * @notice Calculates the required sub-vaults exit requests to fulfill the assets to redeem + * @param assetsToRedeem The amount of assets to redeem + * @return redeemRequests The array of sub-vaults exit requests + */ + function calculateSubVaultsRedemptions(uint256 assetsToRedeem) + external + view + returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests); + + /** + * @notice Redeems assets from sub-vaults to the meta vault. Can only be called by the redeemer. + * @param assetsToRedeem The amount of assets to redeem to the meta vault + * @return totalRedeemedAssets The total amount of assets redeemed from sub-vaults + */ + function redeemSubVaultsAssets(uint256 assetsToRedeem) external returns (uint256 totalRedeemedAssets); +} diff --git a/contracts/interfaces/ISubVaultsRegistryFactory.sol b/contracts/interfaces/ISubVaultsRegistryFactory.sol new file mode 100644 index 00000000..a9ac1f10 --- /dev/null +++ b/contracts/interfaces/ISubVaultsRegistryFactory.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +/** + * @title ISubVaultsRegistryFactory + * @author StakeWise + * @notice Defines the interface for the SubVaultsRegistry Factory contract + */ +interface ISubVaultsRegistryFactory { + /** + * @notice Emitted when a new SubVaultsRegistry is created + * @param metaVault The address of the meta vault that created the registry + * @param subVaultsRegistry The address of the created SubVaultsRegistry + */ + event SubVaultsRegistryCreated(address indexed metaVault, address indexed subVaultsRegistry); + + /** + * @notice The address of the SubVaultsRegistry implementation contract used for proxy creation + * @return The address of the SubVaultsRegistry implementation contract + */ + function implementation() external view returns (address); + + /** + * @notice Creates a new SubVaultsRegistry contract + * @return The address of the created SubVaultsRegistry contract + */ + function createSubVaultsRegistry() external returns (address); +} diff --git a/contracts/interfaces/IVaultSubVaults.sol b/contracts/interfaces/IVaultSubVaults.sol index 1d79a8a6..c71464d0 100644 --- a/contracts/interfaces/IVaultSubVaults.sol +++ b/contracts/interfaces/IVaultSubVaults.sol @@ -2,69 +2,39 @@ pragma solidity ^0.8.22; +import {IVaultState} from "./IVaultState.sol"; + /** * @title IVaultSubVaults * @author StakeWise * @notice Defines the interface for the VaultSubVaults contract */ -interface IVaultSubVaults { - /** - * @notice Struct for sub vault state - * @param stakedShares The number of shares staked in the sub vault - * @param queuedShares The number of shares queued for exit in the sub vault - */ - struct SubVaultState { - uint128 stakedShares; - uint128 queuedShares; - } - - /** - * @notice Struct for submitting sub vault exit request - * @param exitQueueIndex The index of the exit queue - * @param vault The address of the vault - * @param timestamp The timestamp of the exit request - */ - struct SubVaultExitRequest { - uint256 exitQueueIndex; - address vault; - uint64 timestamp; - } - +interface IVaultSubVaults is IVaultState { /** * @notice Emitted when the rewards nonce is updated + * @dev Deprecated: moved to SubVaultsRegistry * @param rewardsNonce The new rewards nonce */ event RewardsNonceUpdated(uint256 rewardsNonce); /** * @notice Emitted when the sub vaults are harvested + * @dev Deprecated: moved to SubVaultsRegistry * @param totalAssetsDelta The change in total assets after the harvest */ event SubVaultsHarvested(int256 totalAssetsDelta); /** * @notice Emitted when the new sub-vault is added + * @dev Deprecated: moved to SubVaultsRegistry * @param caller The address of the caller * @param vault The address of the sub-vault */ event SubVaultAdded(address indexed caller, address indexed vault); - /** - * @notice Emitted when the new meta sub-vault is proposed - * @param caller The address of the caller - * @param vault The address of the meta sub-vault - */ - event MetaSubVaultProposed(address indexed caller, address indexed vault); - - /** - * @notice Emitted when the meta sub-vault is rejected - * @param caller The address of the caller - * @param vault The address of the meta sub-vault - */ - event MetaSubVaultRejected(address indexed caller, address indexed vault); - /** * @notice Emitted when the sub-vault is ejecting + * @dev Deprecated: moved to SubVaultsRegistry * @param caller The address of the caller * @param vault The address of the sub-vault */ @@ -72,6 +42,7 @@ interface IVaultSubVaults { /** * @notice Emitted when the sub-vault is ejected + * @dev Deprecated: moved to SubVaultsRegistry * @param caller The address of the caller * @param vault The address of the sub-vault */ @@ -79,99 +50,60 @@ interface IVaultSubVaults { /** * @notice Emitted when the sub-vaults curator is updated + * @dev Deprecated: moved to SubVaultsRegistry * @param caller The address of the caller * @param curator The address of the new sub-vaults curator */ event SubVaultsCuratorUpdated(address indexed caller, address indexed curator); /** - * @notice Sub-vaults curator contract - * @return The address of the Sub-vaults curator contract - */ - function subVaultsCurator() external view returns (address); - - /** - * @notice Ejecting sub-vault - * @return The address of the ejecting sub-vault - */ - function ejectingSubVault() external view returns (address); - - /** - * @notice Pending meta sub-vault waiting for approval - * @return The address of the pending meta sub-vault - */ - function pendingMetaSubVault() external view returns (address); - - /** - * @notice Function to get the list sub-vaults - * @return An array of addresses of the sub-vaults + * @notice Returns the address of the SubVaultsRegistry contract + * @return The address of the SubVaultsRegistry */ - function getSubVaults() external view returns (address[] memory); + function subVaultsRegistry() external view returns (address); /** - * @notice Function to get the rewards nonce of the sub-vaults - * @return The rewards nonce - */ - function subVaultsRewardsNonce() external view returns (uint128); - - /** - * @notice Function to get the state of a sub-vault + * @notice Function to deposit assets to a sub vault. Can only be called by SubVaultsRegistry contract. * @param vault The address of the sub-vault - * @return The state of the sub-vault - */ - function subVaultsStates(address vault) external view returns (SubVaultState memory); - - /** - * @notice Checks whether the meta vault can be updated - * @return `true` if the meta vault can be updated, `false` otherwise - */ - function canUpdateState() external view returns (bool); - - /** - * @notice Checks whether the vault is collateralized - * @return `true` if the vault is collateralized, `false` otherwise - */ - function isCollateralized() external view returns (bool); - - /** - * @notice Function to update the the sub-vaults curator. Can only be called by the admin. - * @param curator The address of the new sub-vaults curator + * @param assets The amount of assets to deposit + * @return shares The amount of vault shares received */ - function setSubVaultsCurator(address curator) external; + function depositToSubVault(address vault, uint256 assets) external returns (uint256 shares); /** - * @notice Function to add a new sub-vault. Can only be called by the admin. - * @param vault The address of the sub-vault to add - */ - function addSubVault(address vault) external; - - /** - * @notice Function to accept a meta sub-vault. Can only be called by the VaultsRegistry owner. - * @param metaSubVault The address of the meta sub-vault to accept - */ - function acceptMetaSubVault(address metaSubVault) external; - - /** - * @notice Function to reject a meta sub-vault. Can only be called by the VaultsRegistry owner or admin. - * @param metaSubVault The address of the meta sub-vault to reject + * @notice Function to enter sub-vault exit queue. Can only be called by SubVaultsRegistry contract. + * @param vault The address of the sub-vault + * @param shares The amount of shares to exit + * @return positionTicket The position ticket in the exit queue */ - function rejectMetaSubVault(address metaSubVault) external; + function enterSubVaultExitQueue(address vault, uint256 shares) external returns (uint256 positionTicket); /** - * @notice Function to remove a sub-vault. Can only be called by the admin. - * All the sub-vault shares will be added to the exit queue. - * @param vault The address of the sub-vault to remove + * @notice Function to claim exited assets from a sub-vault. Can only be called by SubVaultsRegistry contract. + * @param vault The address of the sub-vault + * @param positionTicket The position ticket in the exit queue + * @param timestamp The timestamp of the exit request + * @param exitQueueIndex The index of the exit queue */ - function ejectSubVault(address vault) external; + function claimSubVaultExitedAssets(address vault, uint256 positionTicket, uint256 timestamp, uint256 exitQueueIndex) + external; /** - * @notice Deposit available assets to the sub vaults + * @notice Function to mint osToken for a sub-vault. Can only be called by SubVaultsRegistry contract. + * @param vault The address of the sub-vault + * @param receiver The address that will receive the minted osToken shares + * @param osTokenShares The amount of osToken shares to mint */ - function depositToSubVaults() external; + function mintSubVaultOsToken(address vault, address receiver, uint256 osTokenShares) external; /** - * @notice Claim the exited assets from the sub vaults - * @param exitRequests The array of exit requests to claim + * @notice Function to redeem osToken from a sub-vault. Can only be called by SubVaultsRegistry contract. + * @param vault The address of the sub-vault + * @param redeemer The address of the OsToken redeemer + * @param osTokenShares The amount of osToken shares to redeem + * @return assets The amount of assets redeemed */ - function claimSubVaultsExitedAssets(SubVaultExitRequest[] calldata exitRequests) external; + function redeemSubVaultOsToken(address vault, address redeemer, uint256 osTokenShares) + external + returns (uint256 assets); } diff --git a/contracts/libraries/SubVaultUtils.sol b/contracts/libraries/SubVaultUtils.sol deleted file mode 100644 index 2c9dfee9..00000000 --- a/contracts/libraries/SubVaultUtils.sol +++ /dev/null @@ -1,340 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.22; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; -import {IVaultsRegistry} from "../interfaces/IVaultsRegistry.sol"; -import {IVaultSubVaults} from "../interfaces/IVaultSubVaults.sol"; -import {IVaultState} from "../interfaces/IVaultState.sol"; -import {IVaultOsToken} from "../interfaces/IVaultOsToken.sol"; -import {IVaultEnterExit} from "../interfaces/IVaultEnterExit.sol"; -import {ISubVaultsCurator} from "../interfaces/ISubVaultsCurator.sol"; -import {IOsTokenVaultController} from "../interfaces/IOsTokenVaultController.sol"; -import {IOsTokenRedeemer} from "../interfaces/IOsTokenRedeemer.sol"; -import {IKeeperRewards} from "../interfaces/IKeeperRewards.sol"; -import {SubVaultExits} from "./SubVaultExits.sol"; -import {Errors} from "./Errors.sol"; - -/** - * @title SubVaultUtils - * @author StakeWise - * @notice Includes the utility functions for managing the meta vault sub-vaults - */ -library SubVaultUtils { - using EnumerableSet for EnumerableSet.AddressSet; - using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - - uint256 private constant _maxSubVaults = 50; - - /** - * @dev Validates the addition of a sub-vault - * @param subVaults The set of currently added sub-vaults - * @param vaultsRegistry The address of the VaultsRegistry contract - * @param keeper The address of the Keeper contract - * @param vault The address of the sub-vault to be added - */ - function validateSubVault( - EnumerableSet.AddressSet storage subVaults, - address vaultsRegistry, - address keeper, - address vault - ) external view { - // check whether the vault is registered in the registry - if (vault == address(0) || vault == address(this) || !IVaultsRegistry(vaultsRegistry).vaults(vault)) { - revert Errors.InvalidVault(); - } - - // check whether the vault is not already added - if (subVaults.contains(vault)) { - revert Errors.AlreadyAdded(); - } - - // check whether the vault is not exceeding the limit - uint256 subVaultsCount = subVaults.length(); - if (subVaultsCount >= _maxSubVaults) { - revert Errors.CapacityExceeded(); - } - - // check whether vault is collateralized - if (!_isSubVaultCollateralized(keeper, vault)) { - revert Errors.NotCollateralized(); - } - - // check whether legacy exit queue is processed, will revert if vault doesn't have `getExitQueueData` function - (,, uint128 totalExitingTickets, uint128 totalExitingAssets,) = IVaultState(vault).getExitQueueData(); - if (totalExitingTickets != 0 || totalExitingAssets != 0) { - revert Errors.ExitRequestNotProcessed(); - } - } - - /** - * @dev Returns the balances of the given sub-vaults - * @param subVaultsStates The mapping of sub-vault addresses to their states - * @param vaults The addresses of the sub-vaults - * @param calcNewTotalAssets Whether to calculate the new total assets across all sub-vaults - * @return balances The balances of the sub-vaults - * @return newTotalAssets The new total assets across all sub-vaults - */ - function getSubVaultsBalances( - mapping(address vault => IVaultSubVaults.SubVaultState state) storage subVaultsStates, - address[] memory vaults, - bool calcNewTotalAssets - ) public view returns (uint256[] memory balances, uint256 newTotalAssets) { - uint256 vaultsLength = vaults.length; - balances = new uint256[](vaultsLength); - for (uint256 i = 0; i < vaultsLength;) { - address vault = vaults[i]; - IVaultSubVaults.SubVaultState memory vaultState = subVaultsStates[vault]; - if (calcNewTotalAssets) { - uint256 vaultTotalShares = vaultState.stakedShares + vaultState.queuedShares; - if (vaultTotalShares > 0) { - newTotalAssets += IVaultState(vault).convertToAssets(vaultTotalShares); - } - } - - if (vaultState.stakedShares > 0) { - balances[i] = IVaultState(vault).convertToAssets(vaultState.stakedShares); - } else { - balances[i] = 0; - } - unchecked { - // cannot realistically overflow - ++i; - } - } - } - - /** - * @dev Calculates the required sub-vaults exit requests to fulfill the assets to redeem - * @param subVaultsStates The mapping of sub-vault addresses to their states - * @param subVaultsCurator The address of the sub-vaults curator - * @param vaults The addresses of the sub-vaults - * @param assetsToRedeem The amount of assets to redeem - * @param withdrawableAssets The amount of withdrawable assets in the meta vault - * @param ejectingSubVault The address of the ejecting sub-vault - * @param ejectingSubVaultShares The shares of the ejecting sub-vault - * @return redeemRequests The array of sub-vaults exit requests - */ - function calculateSubVaultsRedemptions( - mapping(address vault => IVaultSubVaults.SubVaultState state) storage subVaultsStates, - address subVaultsCurator, - address[] memory vaults, - uint256 assetsToRedeem, - uint256 withdrawableAssets, - address ejectingSubVault, - uint256 ejectingSubVaultShares - ) external view returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests) { - // check whether enough assets available - unchecked { - assetsToRedeem -= Math.min(assetsToRedeem, withdrawableAssets); - } - if (assetsToRedeem == 0) { - // if enough withdrawable assets, return empty array - return redeemRequests; - } - - // check whether ejecting shares can be consumed - if (ejectingSubVault != address(0) && ejectingSubVaultShares != 0) { - uint256 ejectingVaultAssets = IVaultState(ejectingSubVault).convertToAssets(ejectingSubVaultShares); - unchecked { - assetsToRedeem -= Math.min(assetsToRedeem, ejectingVaultAssets); - } - } - - if (assetsToRedeem == 0) { - // if no assets to redeem, return empty array - return redeemRequests; - } - - // check vaults length - uint256 vaultsLength = vaults.length; - if (vaultsLength == 0) revert Errors.EmptySubVaults(); - - // fetch current sub-vaults balances - uint256[] memory balances; - (balances,) = getSubVaultsBalances(subVaultsStates, vaults, false); - - // fetch redeems from the curator - return ISubVaultsCurator(subVaultsCurator).getExitRequests(assetsToRedeem, vaults, balances, ejectingSubVault); - } - - /** - * @dev Processes the given redeem requests - * @param subVaultsStates The mapping of sub-vault addresses to their states - * @param osTokenVaultController The address of the osToken vault controller contract - * @param redeemer The address of the redeemer - * @param redeemRequests The redeem requests to process - * @return totalRedeemedAssets The total amount of redeemed assets - */ - function processRedeemRequests( - mapping(address vault => IVaultSubVaults.SubVaultState state) storage subVaultsStates, - address osTokenVaultController, - address redeemer, - ISubVaultsCurator.ExitRequest[] memory redeemRequests - ) external returns (uint256 totalRedeemedAssets) { - uint256 redeemRequestsLength = redeemRequests.length; - for (uint256 i = 0; i < redeemRequestsLength;) { - // calculate redeemable assets - ISubVaultsCurator.ExitRequest memory redeemRequest = redeemRequests[i]; - uint256 redeemAssets = Math.min(redeemRequest.assets, IVaultState(redeemRequest.vault).withdrawableAssets()); - if (redeemAssets == 0) { - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - - // mint osToken shares to redeemer - uint256 osTokenShares = IOsTokenVaultController(osTokenVaultController).convertToShares(redeemAssets); - if (osTokenShares == 0) { - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - IVaultOsToken(redeemRequest.vault).mintOsToken(redeemer, osTokenShares, address(0)); - - // get shares before redemption to track actual consumption - uint256 sharesBefore = IVaultState(redeemRequest.vault).getShares(address(this)); - - // execute redeem - redeemAssets = IOsTokenRedeemer(redeemer).redeemSubVaultOsToken(redeemRequest.vault, osTokenShares); - - // check position is closed - if (IVaultOsToken(redeemRequest.vault).osTokenPositions(address(this)) > 0) { - revert Errors.InvalidPosition(); - } - - uint256 redeemedShares = sharesBefore - IVaultState(redeemRequest.vault).getShares(address(this)); - subVaultsStates[redeemRequest.vault].stakedShares -= SafeCast.toUint128(redeemedShares); - totalRedeemedAssets += redeemAssets; - - unchecked { - // cannot realistically overflow - ++i; - } - } - } - - /** - * @dev Claims exited assets from sub-vaults based on the given exit requests - * @param subVaultsStates The mapping of sub-vault addresses to their states - * @param subVaultsExits The mapping of sub-vault addresses to their exit queues - * @param exitRequests The exit requests to process - * @return totalExitedAssets The total amount of exited assets claimed - */ - function claimSubVaultsExitedAssets( - mapping(address vault => IVaultSubVaults.SubVaultState state) storage subVaultsStates, - mapping(address vault => DoubleEndedQueue.Bytes32Deque) storage subVaultsExits, - IVaultSubVaults.SubVaultExitRequest[] calldata exitRequests - ) external returns (uint256 totalExitedAssets) { - uint256 exitRequestsLength = exitRequests.length; - for (uint256 i = 0; i < exitRequestsLength;) { - IVaultSubVaults.SubVaultExitRequest calldata exitRequest = exitRequests[i]; - IVaultSubVaults.SubVaultState memory subVaultState = subVaultsStates[exitRequest.vault]; - (uint256 positionTicket, uint256 positionShares) = - SubVaultExits.popSubVaultExit(subVaultsExits, exitRequest.vault); - (uint256 leftShares, uint256 exitedShares, uint256 exitedAssets) = IVaultEnterExit(exitRequest.vault) - .calculateExitedAssets(address(this), positionTicket, exitRequest.timestamp, exitRequest.exitQueueIndex); - - subVaultState.queuedShares -= SafeCast.toUint128(positionShares); - if (leftShares > 0) { - // exit request was not processed in full - SubVaultExits.pushSubVaultExit( - subVaultsExits, - exitRequest.vault, - SafeCast.toUint160(positionTicket + exitedShares), - SafeCast.toUint96(leftShares), - true - ); - subVaultState.queuedShares += SafeCast.toUint128(leftShares); - } - - // update total exited assets, vault state - totalExitedAssets += exitedAssets; - subVaultsStates[exitRequest.vault] = subVaultState; - - // claim exited assets from the vault - IVaultEnterExit(exitRequest.vault) - .claimExitedAssets(positionTicket, exitRequest.timestamp, exitRequest.exitQueueIndex); - - unchecked { - // cannot realistically overflow - ++i; - } - } - } - - /** - * @dev Ejects a sub-vault from the meta vault - * @param subVaults The set of currently added sub-vaults - * @param subVaultsStates The mapping of sub-vault addresses to their states - * @param subVaultsExits The mapping of sub-vault addresses to their exit queues - * @param currentEjectingSubVault The address of the currently ejecting sub-vault - * @param vault The address of the sub-vault to eject - * @return ejected Whether the vault was fully ejected (no queued shares) - * @return ejectingShares The amount of shares being ejected - */ - function ejectSubVault( - EnumerableSet.AddressSet storage subVaults, - mapping(address => IVaultSubVaults.SubVaultState) storage subVaultsStates, - mapping(address => DoubleEndedQueue.Bytes32Deque) storage subVaultsExits, - address currentEjectingSubVault, - address vault - ) external returns (bool ejected, uint128 ejectingShares) { - if (currentEjectingSubVault != address(0)) { - revert Errors.EjectingVault(); - } - if (!subVaults.contains(vault)) { - revert Errors.AlreadyRemoved(); - } - if (subVaults.length() == 1) { - revert Errors.EmptySubVaults(); - } - - // check the vault state - IVaultSubVaults.SubVaultState memory state = subVaultsStates[vault]; - if (state.stakedShares > 0) { - // enter exit queue for all the vault staked shares - uint256 positionTicket = IVaultEnterExit(vault).enterExitQueue(state.stakedShares, address(this)); - // add ejecting shares to the vault's exit positions - SubVaultExits.pushSubVaultExit( - subVaultsExits, vault, SafeCast.toUint160(positionTicket), SafeCast.toUint96(state.stakedShares), false - ); - state.queuedShares += state.stakedShares; - ejectingShares = state.stakedShares; - } - - // update state - if (state.queuedShares > 0) { - state.stakedShares = 0; - subVaultsStates[vault] = state; - return (false, ejectingShares); - } else { - // no shares left - subVaultsExits[vault].clear(); - // remove the vault from the list of sub vaults - subVaults.remove(vault); - return (true, 0); - } - } - - /** - * @dev Internal function to check whether a sub-vault is collateralized - * @param subVault The address of the sub-vault - * @return true if the sub-vault is collateralized - */ - function _isSubVaultCollateralized(address keeper, address subVault) private view returns (bool) { - try IVaultSubVaults(subVault).isCollateralized() returns (bool collateralized) { - return collateralized; - } catch {} - - return IKeeperRewards(keeper).isCollateralized(subVault); - } -} diff --git a/contracts/tokens/OsTokenRedeemer.sol b/contracts/tokens/OsTokenRedeemer.sol index 7611c20e..9c624890 100644 --- a/contracts/tokens/OsTokenRedeemer.sol +++ b/contracts/tokens/OsTokenRedeemer.sol @@ -9,11 +9,13 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IMetaVault} from "../interfaces/IMetaVault.sol"; +import {IKeeperRewards} from "../interfaces/IKeeperRewards.sol"; import {IOsTokenRedeemer} from "../interfaces/IOsTokenRedeemer.sol"; import {IOsTokenVaultController} from "../interfaces/IOsTokenVaultController.sol"; import {IVaultOsToken} from "../interfaces/IVaultOsToken.sol"; +import {IVaultState} from "../interfaces/IVaultState.sol"; import {IVaultSubVaults} from "../interfaces/IVaultSubVaults.sol"; +import {ISubVaultsRegistry} from "../interfaces/ISubVaultsRegistry.sol"; import {IVaultsRegistry} from "../interfaces/IVaultsRegistry.sol"; import {Multicall} from "../base/Multicall.sol"; import {Errors} from "../libraries/Errors.sol"; @@ -279,7 +281,7 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { revert Errors.InvalidVault(); } - return IMetaVault(metaVault).redeemSubVaultsAssets(assetsToRedeem); + return ISubVaultsRegistry(IVaultSubVaults(metaVault).subVaultsRegistry()).redeemSubVaultsAssets(assetsToRedeem); } /// @inheritdoc IOsTokenRedeemer @@ -420,6 +422,15 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { emit CheckpointCreated(processedShares, processedAssets); } + /// @inheritdoc IOsTokenRedeemer + function updateVaultState(address vault, IKeeperRewards.HarvestParams calldata harvestParams) external override { + // must be a registered vault + if (!_vaultsRegistry.vaults(vault)) { + revert Errors.InvalidVault(); + } + IVaultState(vault).updateState(harvestParams); + } + /** * @dev Internal function to swap assets to OsToken shares * @param receiver The address that will receive the OsToken shares @@ -461,7 +472,7 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } // must be a meta vault - try IVaultSubVaults(vault).getSubVaults() { + try IVaultSubVaults(vault).subVaultsRegistry() { return true; } catch { return false; diff --git a/contracts/vaults/SubVaultsRegistry.sol b/contracts/vaults/SubVaultsRegistry.sol new file mode 100644 index 00000000..8f5f6fe1 --- /dev/null +++ b/contracts/vaults/SubVaultsRegistry.sol @@ -0,0 +1,967 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {ICuratorsRegistry} from "../interfaces/ICuratorsRegistry.sol"; +import {ISubVaultsCurator} from "../interfaces/ISubVaultsCurator.sol"; +import {IKeeperRewards} from "../interfaces/IKeeperRewards.sol"; +import {ISubVaultsRegistry} from "../interfaces/ISubVaultsRegistry.sol"; +import {IVaultEnterExit} from "../interfaces/IVaultEnterExit.sol"; +import {IVaultAdmin} from "../interfaces/IVaultAdmin.sol"; +import {IVaultState} from "../interfaces/IVaultState.sol"; +import {IVaultSubVaults} from "../interfaces/IVaultSubVaults.sol"; +import {IVaultsRegistry} from "../interfaces/IVaultsRegistry.sol"; +import {IOsTokenConfig} from "../interfaces/IOsTokenConfig.sol"; +import {IOsTokenVaultController} from "../interfaces/IOsTokenVaultController.sol"; +import {IVaultOsToken} from "../interfaces/IVaultOsToken.sol"; +import {Multicall} from "../base/Multicall.sol"; +import {Errors} from "../libraries/Errors.sol"; +import {SubVaultExits} from "../libraries/SubVaultExits.sol"; + +/** + * @title SubVaultsRegistry + * @author StakeWise + * @notice Defines the functionality for managing the Vault sub-vaults. This contract is deployed per MetaVault. + */ +contract SubVaultsRegistry is + Initializable, + UUPSUpgradeable, + ReentrancyGuardUpgradeable, + Multicall, + ISubVaultsRegistry +{ + using EnumerableSet for EnumerableSet.AddressSet; + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + uint256 private constant _maxSubVaults = 50; + + address private immutable _curatorsRegistry; + address private immutable _vaultsRegistry; + address private immutable _keeper; + IOsTokenVaultController private immutable _osTokenVaultController; + IOsTokenConfig private immutable _osTokenConfig; + + /// @inheritdoc ISubVaultsRegistry + address public override metaVault; + + /// @inheritdoc ISubVaultsRegistry + address public override subVaultsCurator; + + /// @inheritdoc ISubVaultsRegistry + address public override pendingMetaSubVault; + + /// @inheritdoc ISubVaultsRegistry + uint128 public override subVaultsRewardsNonce; + + /// @inheritdoc ISubVaultsRegistry + address public override ejectingSubVault; + + /// @inheritdoc ISubVaultsRegistry + uint256 public override ejectingSubVaultShares; + + EnumerableSet.AddressSet private _subVaults; + mapping(address vault => DoubleEndedQueue.Bytes32Deque) private _subVaultsExits; + mapping(address vault => SubVaultState state) private _subVaultsStates; + + /// @inheritdoc ISubVaultsRegistry + uint128 public override subVaultsTotalAssets; + + uint256 private _totalProcessedExitQueueTickets; + + /** + * @dev Modifier to check if the caller is the meta vault admin + */ + modifier onlyMetaVaultAdmin() { + if (msg.sender != IVaultAdmin(metaVault).admin()) revert Errors.AccessDenied(); + _; + } + + /** + * @dev Constructor + * @param curatorsRegistry The address of the CuratorsRegistry contract + * @param vaultsRegistry The address of the VaultsRegistry contract + * @param keeper The address of the Keeper contract + * @param osTokenVaultController The address of the OsTokenVaultController contract + * @param osTokenConfig The address of the OsTokenConfig contract + */ + constructor( + address curatorsRegistry, + address vaultsRegistry, + address keeper, + address osTokenVaultController, + address osTokenConfig + ) { + _curatorsRegistry = curatorsRegistry; + _vaultsRegistry = vaultsRegistry; + _keeper = keeper; + _osTokenVaultController = IOsTokenVaultController(osTokenVaultController); + _osTokenConfig = IOsTokenConfig(osTokenConfig); + _disableInitializers(); + } + + /// @inheritdoc UUPSUpgradeable + function _authorizeUpgrade(address) internal view override { + if (msg.sender != metaVault) revert Errors.AccessDenied(); + } + + /// @inheritdoc ISubVaultsRegistry + function initialize(address _metaVault, address curator) external override initializer { + __ReentrancyGuard_init(); + if (_metaVault == address(0)) revert Errors.ZeroAddress(); + metaVault = _metaVault; + _setSubVaultsCurator(curator); + subVaultsRewardsNonce = SafeCast.toUint128(_getCurrentRewardsNonce()); + } + + /// @inheritdoc ISubVaultsRegistry + function migrate(MigrationData calldata data) external override initializer { + // can only be called once by the meta vault + if (metaVault != address(0)) revert Errors.AccessDenied(); + + // initialize contract + __ReentrancyGuard_init(); + metaVault = msg.sender; + subVaultsCurator = data.curator; + + // migrate ejecting sub-vault + ejectingSubVault = data.ejectingSubVault; + ejectingSubVaultShares = data.ejectingSubVaultShares; + + // migrate nonces and totals + subVaultsRewardsNonce = data.subVaultsRewardsNonce; + subVaultsTotalAssets = data.subVaultsTotalAssets; + _totalProcessedExitQueueTickets = data.totalProcessedExitQueueTickets; + + // migrate sub-vaults, states, and exits + uint256 subVaultsLength = data.subVaults.length; + for (uint256 i = 0; i < subVaultsLength;) { + address vault = data.subVaults[i]; + _subVaults.add(vault); + _subVaultsStates[vault] = data.subVaultsStates[i]; + + // migrate exits for this vault + bytes32[] calldata exits = data.subVaultsExits[i]; + uint256 exitsLength = exits.length; + for (uint256 j = 0; j < exitsLength;) { + _subVaultsExits[vault].pushBack(exits[j]); + unchecked { + ++j; + } + } + + unchecked { + ++i; + } + } + + emit Migrated(msg.sender); + } + + /// @inheritdoc ISubVaultsRegistry + function getSubVaults() public view override returns (address[] memory) { + return _subVaults.values(); + } + + /// @inheritdoc ISubVaultsRegistry + function isSubVault(address vault) public view override returns (bool) { + return _subVaults.contains(vault); + } + + /// @inheritdoc ISubVaultsRegistry + function setSubVaultsCurator(address curator) external override onlyMetaVaultAdmin { + _setSubVaultsCurator(curator); + } + + /// @inheritdoc ISubVaultsRegistry + function addSubVault(address vault) external override onlyMetaVaultAdmin { + // check new sub-vault validity + _validateSubVault(vault); + + if (_isMetaVault(vault)) { + // meta vault must be approved before being added as a sub vault + if (pendingMetaSubVault != address(0)) { + revert Errors.AlreadyAdded(); + } + pendingMetaSubVault = vault; + emit MetaSubVaultProposed(vault); + } else { + _addSubVault(vault); + } + } + + /// @inheritdoc ISubVaultsRegistry + function acceptMetaSubVault(address metaSubVault) external override { + // only the VaultsRegistry owner can accept a meta vault addition as a sub vault + if (msg.sender != Ownable(_vaultsRegistry).owner()) { + revert Errors.AccessDenied(); + } + + if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { + revert Errors.InvalidVault(); + } + + // check sub-vault validity + _validateSubVault(metaSubVault); + + // update state + delete pendingMetaSubVault; + _addSubVault(metaSubVault); + } + + /// @inheritdoc ISubVaultsRegistry + function rejectMetaSubVault(address metaSubVault) external override { + // only the VaultsRegistry owner or meta vault admin can reject a meta vault addition as a sub vault + if (msg.sender != Ownable(_vaultsRegistry).owner() && msg.sender != IVaultAdmin(metaVault).admin()) { + revert Errors.AccessDenied(); + } + + if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { + revert Errors.InvalidVault(); + } + + // update state + delete pendingMetaSubVault; + + // emit event + emit MetaSubVaultRejected(metaSubVault); + } + + /// @inheritdoc ISubVaultsRegistry + function ejectSubVault(address vault) external override onlyMetaVaultAdmin { + if (ejectingSubVault != address(0)) { + revert Errors.EjectingVault(); + } + if (!_subVaults.contains(vault)) { + revert Errors.AlreadyRemoved(); + } + if (_subVaults.length() == 1) { + revert Errors.EmptySubVaults(); + } + + // check the vault state + SubVaultState memory state = _subVaultsStates[vault]; + if (state.stakedShares > 0) { + // enter exit queue for all the vault staked shares + uint256 positionTicket = IVaultSubVaults(metaVault).enterSubVaultExitQueue(vault, state.stakedShares); + // add ejecting shares to the vault's exit positions + SubVaultExits.pushSubVaultExit( + _subVaultsExits, vault, SafeCast.toUint160(positionTicket), SafeCast.toUint96(state.stakedShares), false + ); + state.queuedShares += state.stakedShares; + } + + // update state + if (state.queuedShares > 0) { + ejectingSubVault = vault; + if (state.stakedShares > 0) { + ejectingSubVaultShares = state.stakedShares; + state.stakedShares = 0; + } + _subVaultsStates[vault] = state; + emit SubVaultEjecting(vault); + } else { + // no shares left + _subVaultsExits[vault].clear(); + // remove the vault from the list of sub vaults + _subVaults.remove(vault); + emit SubVaultEjected(vault); + } + } + + /// @inheritdoc ISubVaultsRegistry + function subVaultsStates(address vault) external view override returns (SubVaultState memory) { + return _subVaultsStates[vault]; + } + + /// @inheritdoc ISubVaultsRegistry + function subVaultsExits(address vault) external view override returns (bytes32[] memory) { + uint256 length = _subVaultsExits[vault].length(); + bytes32[] memory exits = new bytes32[](length); + for (uint256 i = 0; i < length;) { + exits[i] = _subVaultsExits[vault].at(i); + unchecked { + ++i; + } + } + return exits; + } + + /// @inheritdoc ISubVaultsRegistry + function canUpdateState() external view override returns (bool) { + uint256 nonce = subVaultsRewardsNonce; + return nonce != 0 && nonce < _getCurrentRewardsNonce(); + } + + /// @inheritdoc ISubVaultsRegistry + function isCollateralized() external view override returns (bool) { + return _subVaults.length() > 0; + } + + /// @inheritdoc ISubVaultsRegistry + function isStateUpdateRequired() public view override returns (bool) { + // SLOAD to memory + uint256 currentNonce = _getCurrentRewardsNonce(); + unchecked { + // cannot realistically overflow + return subVaultsRewardsNonce + 1 < currentNonce; + } + } + + /// @inheritdoc ISubVaultsRegistry + function depositToSubVaults() external override nonReentrant { + _checkHarvested(); + + address[] memory vaults = getSubVaults(); + uint256 vaultsLength = vaults.length; + if (vaultsLength == 0) revert Errors.EmptySubVaults(); + + // deposit accumulated assets to sub vaults + uint256 availableAssets = IVaultState(metaVault).withdrawableAssets(); + if (availableAssets == 0) { + revert Errors.InvalidAssets(); + } + ISubVaultsCurator.Deposit[] memory deposits = + ISubVaultsCurator(subVaultsCurator).getDeposits(availableAssets, vaults, ejectingSubVault); + + // process deposits + uint256 depositsLength = deposits.length; + // SLOAD to memory + uint256 totalAssets = subVaultsTotalAssets; + for (uint256 i = 0; i < depositsLength;) { + ISubVaultsCurator.Deposit memory depositData = deposits[i]; + if (depositData.assets == 0) { + // skip empty deposits + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + + // reverts if there are more deposits than available assets + availableAssets -= depositData.assets; + + // update state + uint128 vaultShares = + SafeCast.toUint128(IVaultSubVaults(metaVault).depositToSubVault(depositData.vault, depositData.assets)); + _subVaultsStates[depositData.vault].stakedShares += vaultShares; + totalAssets += depositData.assets; + unchecked { + // cannot realistically overflow + ++i; + } + } + // update last sync sub vaults assets + subVaultsTotalAssets = SafeCast.toUint128(totalAssets); + } + + /// @inheritdoc ISubVaultsRegistry + function claimSubVaultsExitedAssets(SubVaultExitRequest[] calldata exitRequests) external override nonReentrant { + uint256 exitRequestsLength = exitRequests.length; + // SLOAD to memory + uint256 _subVaultsTotalAssets = subVaultsTotalAssets; + address _ejectingSubVault = ejectingSubVault; + address _metaVault = metaVault; + for (uint256 i = 0; i < exitRequestsLength;) { + SubVaultExitRequest calldata exitRequest = exitRequests[i]; + SubVaultState memory subVaultState = _subVaultsStates[exitRequest.vault]; + (uint256 positionTicket, uint256 positionShares) = + SubVaultExits.popSubVaultExit(_subVaultsExits, exitRequest.vault); + (uint256 leftShares, uint256 exitedShares, uint256 exitedAssets) = IVaultEnterExit(exitRequest.vault) + .calculateExitedAssets(_metaVault, positionTicket, exitRequest.timestamp, exitRequest.exitQueueIndex); + + subVaultState.queuedShares -= SafeCast.toUint128(positionShares); + if (leftShares > 0) { + // exit request was not processed in full + SubVaultExits.pushSubVaultExit( + _subVaultsExits, + exitRequest.vault, + SafeCast.toUint160(positionTicket + exitedShares), + SafeCast.toUint96(leftShares), + true + ); + subVaultState.queuedShares += SafeCast.toUint128(leftShares); + } + + // update total assets, vault state + _subVaultsTotalAssets -= exitedAssets; + _subVaultsStates[exitRequest.vault] = subVaultState; + + // claim exited assets from the vault + IVaultSubVaults(_metaVault) + .claimSubVaultExitedAssets( + exitRequest.vault, positionTicket, exitRequest.timestamp, exitRequest.exitQueueIndex + ); + if (_ejectingSubVault == exitRequest.vault && subVaultState.queuedShares == 0) { + // clean up ejecting vault + delete ejectingSubVault; + delete ejectingSubVaultShares; + _subVaultsExits[exitRequest.vault].clear(); + _subVaults.remove(exitRequest.vault); + emit SubVaultEjected(exitRequest.vault); + } + + unchecked { + // cannot realistically overflow + ++i; + } + } + // update sub vaults total assets + subVaultsTotalAssets = SafeCast.toUint128(_subVaultsTotalAssets); + } + + /// @inheritdoc ISubVaultsRegistry + function harvestSubVaultsAssets() external override returns (int256 totalAssetsDelta, bool harvested) { + if (msg.sender != metaVault) { + revert Errors.AccessDenied(); + } + + // fetch all the vaults + address[] memory vaults = getSubVaults(); + uint256 vaultsLength = vaults.length; + if (vaultsLength == 0) revert Errors.EmptySubVaults(); + + // sync rewards nonce + harvested = _syncRewardsNonce(vaults); + if (!harvested) { + return (0, false); + } + + // check claims + _checkSubVaultsExitClaims(vaults); + + // calculate new total assets and save balances in each sub vault + uint256[] memory balances; + uint256 newSubVaultsTotalAssets; + (balances, newSubVaultsTotalAssets) = _getSubVaultsBalances(vaults, true); + + // store new sub vaults total assets delta + totalAssetsDelta = SafeCast.toInt256(newSubVaultsTotalAssets) - SafeCast.toInt256(subVaultsTotalAssets); + + // update state + subVaultsTotalAssets = SafeCast.toUint128(newSubVaultsTotalAssets); + emit SubVaultsHarvested(totalAssetsDelta); + } + + /// @inheritdoc ISubVaultsRegistry + function enterSubVaultsExitQueue() external override nonReentrant { + if (msg.sender != metaVault) { + revert Errors.AccessDenied(); + } + + // SLOAD to memory + address _metaVault = metaVault; + (uint128 queuedShares,,,, uint256 totalExitedTickets) = IVaultState(_metaVault).getExitQueueData(); + uint256 totalProcessedTickets = Math.max(_totalProcessedExitQueueTickets, totalExitedTickets); + + // calculate unprocessed exit queue tickets + uint256 unprocessedTickets = queuedShares - (totalProcessedTickets - totalExitedTickets); + if (unprocessedTickets == 0) { + // nothing to process + return; + } + + // update state + _totalProcessedExitQueueTickets = totalProcessedTickets + unprocessedTickets; + + // check whether ejecting vault has exiting assets + uint256 unprocessedAssets = IVaultState(_metaVault).convertToAssets(unprocessedTickets); + if (unprocessedAssets == 0) { + // nothing to process + return; + } + + unprocessedAssets -= _consumeEjectingSubVaultAssets(unprocessedAssets); + if (unprocessedAssets == 0) { + return; + } + + // fetch exit requests from the curator + address[] memory vaults = getSubVaults(); + uint256[] memory balances; + (balances,) = _getSubVaultsBalances(vaults, false); + ISubVaultsCurator.ExitRequest[] memory exits = + ISubVaultsCurator(subVaultsCurator).getExitRequests(unprocessedAssets, vaults, balances, ejectingSubVault); + + // process exits + uint256 processedAssets; + uint256 exitsLength = exits.length; + for (uint256 i = 0; i < exitsLength;) { + // submit exit request to the vault + ISubVaultsCurator.ExitRequest memory exitRequest = exits[i]; + if (exitRequest.assets == 0) { + // skip empty exit requests + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + SubVaultState memory vaultState = _subVaultsStates[exitRequest.vault]; + uint256 vaultShares = IVaultState(exitRequest.vault).convertToShares(exitRequest.assets); + if (vaultShares == 0) { + // skip exit requests with zero shares + processedAssets += exitRequest.assets; + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + uint256 positionTicket = IVaultSubVaults(_metaVault).enterSubVaultExitQueue(exitRequest.vault, vaultShares); + + // save exit request + SubVaultExits.pushSubVaultExit( + _subVaultsExits, + exitRequest.vault, + SafeCast.toUint160(positionTicket), + SafeCast.toUint96(vaultShares), + false + ); + + // update state + uint128 vaultShares128 = SafeCast.toUint128(vaultShares); + vaultState.queuedShares += vaultShares128; + vaultState.stakedShares -= vaultShares128; + + _subVaultsStates[exitRequest.vault] = vaultState; + processedAssets += exitRequest.assets; + + unchecked { + // cannot realistically overflow + ++i; + } + } + if (processedAssets > unprocessedAssets) { + revert Errors.InvalidAssets(); + } + } + + /// @inheritdoc ISubVaultsRegistry + function calculateSubVaultsRedemptions(uint256 assetsToRedeem) + external + view + override + returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests) + { + return _calculateSubVaultsRedemptions(assetsToRedeem, true); + } + + /// @inheritdoc ISubVaultsRegistry + function redeemSubVaultsAssets(uint256 assetsToRedeem) + external + override + nonReentrant + returns (uint256 totalRedeemedAssets) + { + // check only redeemer can call + address redeemer = _osTokenConfig.redeemer(); + if (msg.sender != redeemer) revert Errors.AccessDenied(); + + if (assetsToRedeem == 0) { + revert Errors.InvalidAssets(); + } + + // get redeem requests + ISubVaultsCurator.ExitRequest[] memory redeemRequests = _calculateSubVaultsRedemptions(assetsToRedeem, false); + if (redeemRequests.length == 0) { + return totalRedeemedAssets; + } + + // perform redemptions + totalRedeemedAssets = _processRedeemRequests(redeemer, redeemRequests); + + // update sub vaults total assets + subVaultsTotalAssets -= SafeCast.toUint128(totalRedeemedAssets); + + // emit event + emit SubVaultsAssetsRedeemed(totalRedeemedAssets); + } + + /** + * @dev Internal function to calculate the required sub-vaults exit requests to fulfill the assets to redeem + * @param assetsToRedeem The amount of assets to redeem + * @param useEjectingSubVaultShares Whether to use ejecting sub-vault shares + * @return redeemRequests The array of sub-vaults exit requests + */ + function _calculateSubVaultsRedemptions(uint256 assetsToRedeem, bool useEjectingSubVaultShares) + private + view + returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests) + { + _checkHarvested(); + + // check whether enough withdrawable assets available + uint256 withdrawableAssets = IVaultState(metaVault).withdrawableAssets(); + unchecked { + assetsToRedeem -= Math.min(assetsToRedeem, withdrawableAssets); + } + if (assetsToRedeem == 0) { + // if enough withdrawable assets, return empty array + return redeemRequests; + } + + // check whether ejecting shares can be consumed + if (useEjectingSubVaultShares) { + address _ejectingSubVault = ejectingSubVault; + uint256 _ejectingSubVaultShares = ejectingSubVaultShares; + if (_ejectingSubVault != address(0) && _ejectingSubVaultShares != 0) { + uint256 ejectingVaultAssets = IVaultState(_ejectingSubVault).convertToAssets(_ejectingSubVaultShares); + unchecked { + assetsToRedeem -= Math.min(assetsToRedeem, ejectingVaultAssets); + } + } + } + + if (assetsToRedeem == 0) { + // if no assets to redeem, return empty array + return redeemRequests; + } + + // fetch current sub-vaults balances + address[] memory vaults = getSubVaults(); + uint256 vaultsLength = vaults.length; + if (vaultsLength == 0) revert Errors.EmptySubVaults(); + + uint256[] memory balances; + (balances,) = _getSubVaultsBalances(vaults, false); + + // fetch redeems from the curator + return ISubVaultsCurator(subVaultsCurator).getExitRequests(assetsToRedeem, vaults, balances, ejectingSubVault); + } + + /** + * @dev Returns the balances of the given sub-vaults + * @param vaults The addresses of the sub-vaults + * @param calcNewTotalAssets Whether to calculate the new total assets across all sub-vaults + * @return balances The balances of the sub-vaults + * @return newTotalAssets The new total assets across all sub-vaults + */ + function _getSubVaultsBalances(address[] memory vaults, bool calcNewTotalAssets) + private + view + returns (uint256[] memory balances, uint256 newTotalAssets) + { + uint256 vaultsLength = vaults.length; + balances = new uint256[](vaultsLength); + for (uint256 i = 0; i < vaultsLength;) { + address vault = vaults[i]; + SubVaultState memory vaultState = _subVaultsStates[vault]; + if (calcNewTotalAssets) { + uint256 vaultTotalShares = vaultState.stakedShares + vaultState.queuedShares; + if (vaultTotalShares > 0) { + newTotalAssets += IVaultState(vault).convertToAssets(vaultTotalShares); + } + } + + if (vaultState.stakedShares > 0) { + balances[i] = IVaultState(vault).convertToAssets(vaultState.stakedShares); + } else { + balances[i] = 0; + } + unchecked { + // cannot realistically overflow + ++i; + } + } + } + + /** + * @dev Internal function to check whether the sub-vaults are harvested + */ + function _checkHarvested() private view { + if (isStateUpdateRequired()) { + revert Errors.NotHarvested(); + } + } + + /** + * @dev Internal function to check whether the sub vaults have claimed processed exit queue tickets + * @param vaults The addresses of the sub vaults + */ + function _checkSubVaultsExitClaims(address[] memory vaults) private view { + uint256 vaultsLength = vaults.length; + for (uint256 i = 0; i < vaultsLength;) { + address vault = vaults[i]; + (uint256 positionTicket, uint256 exitShares) = SubVaultExits.peekSubVaultExit(_subVaultsExits, vault); + if (positionTicket == 0 && exitShares == 0) { + // no queue positions + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + (,,,, uint256 totalExitedTickets) = IVaultState(vault).getExitQueueData(); + if (totalExitedTickets > positionTicket) { + revert Errors.UnclaimedAssets(); + } + + unchecked { + // cannot realistically overflow + ++i; + } + } + } + + /** + * @dev Internal function to check whether the vaults are harvested + * @param vaults The addresses of the vaults + * @return Whether the nonce has been updated + */ + function _syncRewardsNonce(address[] memory vaults) private returns (bool) { + // process first vault in the array + address vault = vaults[0]; + uint256 vaultNonce = _getSubVaultRewardsNonce(vault); + + // check whether the first vault is harvested + uint256 currentNonce = _getCurrentRewardsNonce(); + if (vaultNonce + 1 < currentNonce) { + revert Errors.NotHarvested(); + } + + // fetch current nonce + currentNonce = vaultNonce; + uint256 lastRewardsNonce = subVaultsRewardsNonce; + if (lastRewardsNonce > currentNonce) { + revert Errors.RewardsNonceIsHigher(); + } else if (lastRewardsNonce == currentNonce) { + return false; + } else { + // update last sync rewards nonce + subVaultsRewardsNonce = SafeCast.toUint128(currentNonce); + emit RewardsNonceUpdated(currentNonce); + } + + // all the vaults must be with the same rewards nonce + uint256 vaultsLength = vaults.length; + for (uint256 i = 1; i < vaultsLength;) { + vault = vaults[i]; + vaultNonce = _getSubVaultRewardsNonce(vault); + + // check whether the vault is harvested + if (vaultNonce != currentNonce) { + revert Errors.NotHarvested(); + } + + unchecked { + // cannot realistically overflow + ++i; + } + } + return true; + } + + /** + * @dev Internal function to consume ejecting sub-vault assets + * @param unprocessedAssets The amount of unprocessed assets + * @return processedAssets The amount of processed assets + */ + function _consumeEjectingSubVaultAssets(uint256 unprocessedAssets) private returns (uint256 processedAssets) { + // SLOAD to memory + address _ejectingSubVault = ejectingSubVault; + if (_ejectingSubVault == address(0)) { + return 0; + } + uint256 _ejectingSubVaultShares = ejectingSubVaultShares; + if (_ejectingSubVaultShares == 0) { + return 0; + } + + uint256 ejectingVaultAssets = IVaultState(_ejectingSubVault).convertToAssets(_ejectingSubVaultShares); + processedAssets = Math.min(unprocessedAssets, ejectingVaultAssets); + + // update state + ejectingSubVaultShares = + _ejectingSubVaultShares - IVaultState(_ejectingSubVault).convertToShares(processedAssets); + } + + /** + * @dev Internal function to set the sub-vaults curator + * @param curator The address of the sub-vaults curator + */ + function _setSubVaultsCurator(address curator) private { + if (curator == address(0)) revert Errors.ZeroAddress(); + if (curator == subVaultsCurator) revert Errors.ValueNotChanged(); + if (!ICuratorsRegistry(_curatorsRegistry).isCurator(curator)) { + revert Errors.InvalidCurator(); + } + subVaultsCurator = curator; + emit SubVaultsCuratorUpdated(curator); + } + + /** + * @dev Internal function to validate the addition of a sub-vault + * @param vault The address of the sub-vault to be added + */ + function _validateSubVault(address vault) private view { + // check whether the vault is registered in the registry + if (vault == address(0) || vault == metaVault || !IVaultsRegistry(_vaultsRegistry).vaults(vault)) { + revert Errors.InvalidVault(); + } + + // check whether the vault is not already added + if (_subVaults.contains(vault)) { + revert Errors.AlreadyAdded(); + } + + // check whether the vault is not exceeding the limit + uint256 subVaultsCount = _subVaults.length(); + if (subVaultsCount >= _maxSubVaults) { + revert Errors.CapacityExceeded(); + } + + // check whether vault is collateralized + if (!_isSubVaultCollateralized(vault)) { + revert Errors.NotCollateralized(); + } + + // check whether legacy exit queue is processed, will revert if vault doesn't have `getExitQueueData` function + (,, uint128 totalExitingTickets, uint128 totalExitingAssets,) = IVaultState(vault).getExitQueueData(); + if (totalExitingTickets != 0 || totalExitingAssets != 0) { + revert Errors.ExitRequestNotProcessed(); + } + } + + /** + * @dev Internal function to add a sub-vault + * @param vault The address of the sub-vault to add + */ + function _addSubVault(address vault) private { + // update nonce + uint256 vaultNonce = _getSubVaultRewardsNonce(vault); + uint256 lastSubVaultsRewardsNonce = subVaultsRewardsNonce; + if (_subVaults.length() == 0) { + subVaultsRewardsNonce = SafeCast.toUint128(vaultNonce); + emit RewardsNonceUpdated(vaultNonce); + } else if (vaultNonce != lastSubVaultsRewardsNonce) { + revert Errors.NotHarvested(); + } + + _subVaults.add(vault); + emit SubVaultAdded(vault); + } + + /** + * @dev Internal function to check whether the vault is a meta vault + * @param vault The address of the vault + * @return True if the vault is a meta vault, false otherwise + */ + function _isMetaVault(address vault) private view returns (bool) { + try IVaultSubVaults(vault).subVaultsRegistry() { + return true; + } catch { + return false; + } + } + + /** + * @dev Internal function to check whether the sub-vault is collateralized + * @param subVault The address of the sub-vault + * @return True if the sub-vault is collateralized, false otherwise + */ + function _isSubVaultCollateralized(address subVault) private view returns (bool) { + try IVaultSubVaults(subVault).subVaultsRegistry() returns (address registry) { + return ISubVaultsRegistry(registry).isCollateralized(); + } catch {} + + return IKeeperRewards(_keeper).isCollateralized(subVault); + } + + /** + * @dev Internal function to get the rewards nonce of a sub-vault + * @param subVault The address of the sub-vault + * @return The rewards nonce of the sub-vault + */ + function _getSubVaultRewardsNonce(address subVault) private view returns (uint256) { + try IVaultSubVaults(subVault).subVaultsRegistry() returns (address registry) { + return ISubVaultsRegistry(registry).subVaultsRewardsNonce(); + } catch {} + + (, uint256 vaultNonce) = IKeeperRewards(_keeper).rewards(subVault); + return vaultNonce; + } + + /** + * @dev Internal function to get the current rewards nonce from the Keeper contract + * @return The current rewards nonce + */ + function _getCurrentRewardsNonce() private view returns (uint256) { + return IKeeperRewards(_keeper).rewardsNonce(); + } + + /** + * @dev Processes the given redeem requests + * @param redeemer The address of the redeemer + * @param redeemRequests The redeem requests to process + * @return totalRedeemedAssets The total amount of redeemed assets + */ + function _processRedeemRequests(address redeemer, ISubVaultsCurator.ExitRequest[] memory redeemRequests) + private + returns (uint256 totalRedeemedAssets) + { + // SLOAD to memory + address _metaVault = metaVault; + + uint256 redeemRequestsLength = redeemRequests.length; + for (uint256 i = 0; i < redeemRequestsLength;) { + // calculate redeemable assets + ISubVaultsCurator.ExitRequest memory redeemRequest = redeemRequests[i]; + uint256 redeemAssets = Math.min(redeemRequest.assets, IVaultState(redeemRequest.vault).withdrawableAssets()); + if (redeemAssets == 0) { + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + + // mint osToken shares to redeemer + uint256 osTokenShares = _osTokenVaultController.convertToShares(redeemAssets); + if (osTokenShares == 0) { + unchecked { + // cannot realistically overflow + ++i; + } + continue; + } + IVaultSubVaults(_metaVault).mintSubVaultOsToken(redeemRequest.vault, redeemer, osTokenShares); + + // get shares before redemption to track actual consumption + uint256 sharesBefore = IVaultState(redeemRequest.vault).getShares(_metaVault); + + // execute redeem + redeemAssets = + IVaultSubVaults(_metaVault).redeemSubVaultOsToken(redeemRequest.vault, redeemer, osTokenShares); + + // check position is closed + if (IVaultOsToken(redeemRequest.vault).osTokenPositions(_metaVault) > 0) { + revert Errors.InvalidPosition(); + } + + uint256 redeemedShares = sharesBefore - IVaultState(redeemRequest.vault).getShares(_metaVault); + _subVaultsStates[redeemRequest.vault].stakedShares -= SafeCast.toUint128(redeemedShares); + totalRedeemedAssets += redeemAssets; + + unchecked { + // cannot realistically overflow + ++i; + } + } + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/vaults/SubVaultsRegistryFactory.sol b/contracts/vaults/SubVaultsRegistryFactory.sol new file mode 100644 index 00000000..b2b332b5 --- /dev/null +++ b/contracts/vaults/SubVaultsRegistryFactory.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {ISubVaultsRegistryFactory} from "../interfaces/ISubVaultsRegistryFactory.sol"; +import {IVaultsRegistry} from "../interfaces/IVaultsRegistry.sol"; +import {Errors} from "../libraries/Errors.sol"; + +/** + * @title SubVaultsRegistryFactory + * @author StakeWise + * @notice Factory for deploying SubVaultsRegistry contracts + */ +contract SubVaultsRegistryFactory is ISubVaultsRegistryFactory { + IVaultsRegistry internal immutable _vaultsRegistry; + + /// @inheritdoc ISubVaultsRegistryFactory + address public immutable override implementation; + + /** + * @dev Constructor + * @param _implementation The implementation address of SubVaultsRegistry + * @param vaultsRegistry The address of the VaultsRegistry contract + */ + constructor(address _implementation, IVaultsRegistry vaultsRegistry) { + implementation = _implementation; + _vaultsRegistry = vaultsRegistry; + } + + /// @inheritdoc ISubVaultsRegistryFactory + function createSubVaultsRegistry() external override returns (address) { + if (!_vaultsRegistry.vaults(msg.sender)) { + revert Errors.InvalidVault(); + } + address subVaultsRegistry = address(new ERC1967Proxy(implementation, "")); + emit SubVaultsRegistryCreated(msg.sender, subVaultsRegistry); + return subVaultsRegistry; + } +} diff --git a/contracts/vaults/base/MetaVault.sol b/contracts/vaults/base/MetaVault.sol deleted file mode 100644 index 2e4a0c39..00000000 --- a/contracts/vaults/base/MetaVault.sol +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity ^0.8.22; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {IKeeperRewards} from "../../interfaces/IKeeperRewards.sol"; -import {ISubVaultsCurator} from "../../interfaces/ISubVaultsCurator.sol"; -import {IMetaVault} from "../../interfaces/IMetaVault.sol"; -import {Errors} from "../../libraries/Errors.sol"; -import {SubVaultUtils} from "../../libraries/SubVaultUtils.sol"; -import {Multicall} from "../../base/Multicall.sol"; -import {VaultImmutables} from "../modules/VaultImmutables.sol"; -import {VaultAdmin} from "../modules/VaultAdmin.sol"; -import {VaultVersion} from "../modules/VaultVersion.sol"; -import {VaultFee} from "../modules/VaultFee.sol"; -import {VaultState, IVaultState} from "../modules/VaultState.sol"; -import {VaultEnterExit, IVaultEnterExit} from "../modules/VaultEnterExit.sol"; -import {VaultOsToken} from "../modules/VaultOsToken.sol"; -import {VaultSubVaults} from "../modules/VaultSubVaults.sol"; - -/** - * @title MetaVault - * @author StakeWise - * @notice Defines the Meta Vault that delegates stake to the sub vaults - */ -abstract contract MetaVault is - VaultImmutables, - VaultAdmin, - VaultVersion, - VaultFee, - VaultState, - VaultEnterExit, - VaultOsToken, - VaultSubVaults, - Multicall, - IMetaVault -{ - /** - * @dev Constructor - * @dev Since the immutable variable value is stored in the bytecode, - * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. - * @param args The arguments for initializing the MetaVault contract - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(MetaVaultConstructorArgs memory args) - VaultImmutables(args.keeper, args.vaultsRegistry) - VaultEnterExit(args.exitingAssetsClaimDelay) - VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) - VaultSubVaults(args.curatorsRegistry) - {} - - /// @inheritdoc IVaultState - function isStateUpdateRequired() public view override(IVaultState, VaultState, VaultSubVaults) returns (bool) { - return super.isStateUpdateRequired(); - } - - /// @inheritdoc IMetaVault - function calculateSubVaultsRedemptions(uint256 assetsToRedeem) - external - view - override - returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests) - { - return _calculateSubVaultsRedemptions(assetsToRedeem, true); - } - - /// @inheritdoc IMetaVault - function redeemSubVaultsAssets(uint256 assetsToRedeem) - external - override - nonReentrant - returns (uint256 totalRedeemedAssets) - { - // check only redeemer can call - address redeemer = _osTokenConfig.redeemer(); - if (msg.sender != redeemer) revert Errors.AccessDenied(); - - if (assetsToRedeem == 0) { - revert Errors.InvalidAssets(); - } - - // get redeem requests - ISubVaultsCurator.ExitRequest[] memory redeemRequests = _calculateSubVaultsRedemptions(assetsToRedeem, false); - if (redeemRequests.length == 0) { - return totalRedeemedAssets; - } - - // check assets before - uint256 assetsBefore = _vaultAssets(); - - // perform redemptions - totalRedeemedAssets = SubVaultUtils.processRedeemRequests( - _subVaultsStates, address(_osTokenVaultController), redeemer, redeemRequests - ); - - // check redeemed assets transferred back - if (_vaultAssets() - assetsBefore != totalRedeemedAssets) { - revert Errors.InvalidAssets(); - } - - // update sub vaults total assets - _subVaultsTotalAssets -= SafeCast.toUint128(totalRedeemedAssets); - - // emit event - emit SubVaultsAssetsRedeemed(totalRedeemedAssets); - } - - /// @inheritdoc IVaultState - function updateState(IKeeperRewards.HarvestParams calldata harvestParams) - public - override(IVaultState, VaultState, VaultSubVaults) - { - super.updateState(harvestParams); - } - - /// @inheritdoc IVaultEnterExit - function enterExitQueue(uint256 shares, address receiver) - public - virtual - override(IVaultEnterExit, VaultEnterExit, VaultOsToken) - returns (uint256 positionTicket) - { - return super.enterExitQueue(shares, receiver); - } - - /// @inheritdoc VaultImmutables - function _checkHarvested() internal view override(VaultImmutables, VaultSubVaults) { - super._checkHarvested(); - } - - /** - * @dev Calculates the required sub-vaults exit requests to fulfill the assets to redeem - * @param assetsToRedeem The amount of assets to redeem - * @param includeEjectingSubVaultShares Whether to take into account shares from the ejecting sub-vault - * @return redeemRequests The array of sub-vaults exit requests - */ - function _calculateSubVaultsRedemptions(uint256 assetsToRedeem, bool includeEjectingSubVaultShares) - private - view - returns (ISubVaultsCurator.ExitRequest[] memory redeemRequests) - { - _checkHarvested(); - - return SubVaultUtils.calculateSubVaultsRedemptions( - _subVaultsStates, - subVaultsCurator, - getSubVaults(), - assetsToRedeem, - withdrawableAssets(), - ejectingSubVault, - includeEjectingSubVaultShares ? _ejectingSubVaultShares : 0 - ); - } - - /// @inheritdoc VaultImmutables - function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { - return super._isCollateralized(); - } - - /** - * @dev Initializes the MetaVault contract - * @param admin The address of the admin of the Vault - * @param params The parameters for initializing the MetaVault contract - */ - function __MetaVault_init(address admin, MetaVaultInitParams memory params) internal onlyInitializing { - __VaultAdmin_init(admin, params.metadataIpfsHash); - __VaultSubVaults_init(params.subVaultsCurator); - // fee recipient is initially set to admin address - __VaultFee_init(admin, params.feePercent); - __VaultState_init(params.capacity); - } - - /** - * @dev This empty reserved space is put in place to allow future versions to add new - * variables without shifting down storage in the inheritance chain. - * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - */ - uint256[50] private __gap; -} diff --git a/contracts/vaults/ethereum/EthErc20MetaVault.sol b/contracts/vaults/ethereum/EthErc20MetaVault.sol new file mode 100644 index 00000000..62edec55 --- /dev/null +++ b/contracts/vaults/ethereum/EthErc20MetaVault.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IEthErc20MetaVault} from "../../interfaces/IEthErc20MetaVault.sol"; +import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; +import {IKeeperRewards} from "../../interfaces/IKeeperRewards.sol"; +import {IVaultEthStaking} from "../../interfaces/IVaultEthStaking.sol"; +import {ISubVaultsRegistry} from "../../interfaces/ISubVaultsRegistry.sol"; +import {Errors} from "../../libraries/Errors.sol"; +import {Multicall} from "../../base/Multicall.sol"; +import {ERC20Upgradeable} from "../../base/ERC20Upgradeable.sol"; +import {VaultImmutables} from "../modules/VaultImmutables.sol"; +import {VaultAdmin} from "../modules/VaultAdmin.sol"; +import {VaultVersion, IVaultVersion} from "../modules/VaultVersion.sol"; +import {VaultFee} from "../modules/VaultFee.sol"; +import {VaultState, IVaultState} from "../modules/VaultState.sol"; +import {VaultEnterExit, IVaultEnterExit} from "../modules/VaultEnterExit.sol"; +import {VaultOsToken} from "../modules/VaultOsToken.sol"; +import {VaultSubVaults} from "../modules/VaultSubVaults.sol"; +import {VaultToken} from "../modules/VaultToken.sol"; + +/** + * @title EthErc20MetaVault + * @author StakeWise + * @notice Defines the Meta Vault functionality with ERC-20 token on Ethereum + */ +contract EthErc20MetaVault is + VaultImmutables, + Initializable, + ReentrancyGuardUpgradeable, + VaultAdmin, + VaultVersion, + VaultFee, + VaultState, + VaultEnterExit, + VaultOsToken, + VaultToken, + VaultSubVaults, + Multicall, + IEthErc20MetaVault +{ + uint8 private constant _version = 6; + uint256 private constant _securityDeposit = 1e9; + + /** + * @dev Constructor + * @dev Since the immutable variable value is stored in the bytecode, + * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. + * @param args The arguments for initializing the EthErc20MetaVault contract + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(EthErc20MetaVaultConstructorArgs memory args) + VaultImmutables(args.keeper, args.vaultsRegistry) + VaultEnterExit(args.exitingAssetsClaimDelay) + VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) + VaultSubVaults(args.subVaultsRegistryFactory) + { + _disableInitializers(); + } + + /// @inheritdoc IEthErc20MetaVault + function initialize(bytes calldata params) external payable virtual override reinitializer(_version) { + // do not check for the upgrades since this is the first implementation of EthErc20MetaVault + __EthErc20MetaVault_init( + IEthMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (EthErc20MetaVaultInitParams)) + ); + } + + /// @inheritdoc IEthErc20MetaVault + function deposit(address receiver, address referrer) public payable virtual override returns (uint256 shares) { + return _deposit(receiver, msg.value, referrer); + } + + /** + * @dev Function for depositing using fallback function + */ + receive() external payable virtual { + // claim exited assets from the sub vaults should not be processed as deposits + if (ISubVaultsRegistry(subVaultsRegistry).isSubVault(msg.sender)) { + return; + } + _deposit(msg.sender, msg.value, address(0)); + } + + /// @inheritdoc IEthErc20MetaVault + function updateStateAndDeposit( + address receiver, + address referrer, + IKeeperRewards.HarvestParams calldata harvestParams + ) public payable virtual override returns (uint256 shares) { + updateState(harvestParams); + return deposit(receiver, referrer); + } + + /// @inheritdoc IEthErc20MetaVault + function depositAndMintOsToken(address receiver, uint256 osTokenShares, address referrer) + public + payable + override + returns (uint256) + { + deposit(msg.sender, referrer); + return mintOsToken(receiver, osTokenShares, referrer); + } + + /// @inheritdoc IEthErc20MetaVault + function updateStateAndDepositAndMintOsToken( + address receiver, + uint256 osTokenShares, + address referrer, + IKeeperRewards.HarvestParams calldata harvestParams + ) external payable override returns (uint256) { + updateState(harvestParams); + return depositAndMintOsToken(receiver, osTokenShares, referrer); + } + + /// @inheritdoc IEthErc20MetaVault + function donateAssets() external payable override { + if (msg.value == 0) { + revert Errors.InvalidAssets(); + } + _donatedAssets += msg.value; + emit AssetsDonated(msg.sender, msg.value); + } + + /// @inheritdoc IERC20 + function transfer(address to, uint256 amount) public virtual override(IERC20, ERC20Upgradeable) returns (bool) { + bool success = super.transfer(to, amount); + _checkOsTokenPosition(msg.sender); + return success; + } + + /// @inheritdoc IERC20 + function transferFrom(address from, address to, uint256 amount) + public + virtual + override(IERC20, ERC20Upgradeable) + returns (bool) + { + bool success = super.transferFrom(from, to, amount); + _checkOsTokenPosition(from); + return success; + } + + /// @inheritdoc IVaultEnterExit + function enterExitQueue(uint256 shares, address receiver) + public + virtual + override(IVaultEnterExit, VaultEnterExit, VaultOsToken) + returns (uint256 positionTicket) + { + positionTicket = super.enterExitQueue(shares, receiver); + // only emit Transfer if shares were queued (not directly redeemed when non-collateralized) + if (positionTicket != type(uint256).max) { + emit Transfer(msg.sender, address(this), shares); + } + } + + /// @inheritdoc IVaultVersion + function vaultId() public pure virtual override(IVaultVersion, VaultVersion) returns (bytes32) { + return keccak256("EthErc20MetaVault"); + } + + /// @inheritdoc IVaultVersion + function version() public pure virtual override(IVaultVersion, VaultVersion) returns (uint8) { + return _version; + } + + /// @inheritdoc VaultSubVaults + function _depositToVault(address vault, uint256 assets) internal override returns (uint256) { + // slither-disable-next-line arbitrary-send-eth + return IVaultEthStaking(vault).deposit{value: assets}(address(this), address(0)); + } + + /// @inheritdoc VaultState + function _vaultAssets() internal view virtual override returns (uint256) { + return address(this).balance; + } + + /// @inheritdoc VaultEnterExit + function _transferVaultAssets(address receiver, uint256 assets) internal virtual override nonReentrant { + return Address.sendValue(payable(receiver), assets); + } + + /// @inheritdoc IVaultState + function isStateUpdateRequired() + public + view + virtual + override(IVaultState, VaultState, VaultSubVaults) + returns (bool) + { + return super.isStateUpdateRequired(); + } + + /// @inheritdoc IVaultState + function updateState(IKeeperRewards.HarvestParams calldata harvestParams) + public + virtual + override(IVaultState, VaultState, VaultSubVaults) + { + super.updateState(harvestParams); + } + + /// @inheritdoc VaultState + function _updateExitQueue() internal virtual override(VaultState, VaultToken) returns (uint256 burnedShares) { + return super._updateExitQueue(); + } + + /// @inheritdoc VaultState + function _mintShares(address owner, uint256 shares) internal virtual override(VaultState, VaultToken) { + super._mintShares(owner, shares); + } + + /// @inheritdoc VaultState + function _burnShares(address owner, uint256 shares) internal virtual override(VaultState, VaultToken) { + super._burnShares(owner, shares); + } + + /// @inheritdoc VaultImmutables + function _checkHarvested() internal view virtual override(VaultImmutables, VaultSubVaults) { + super._checkHarvested(); + } + + /// @inheritdoc VaultImmutables + function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { + return super._isCollateralized(); + } + + /** + * @dev Initializes the EthErc20MetaVault contract + * @param _admin The address of the admin of the Vault + * @param params The parameters for initializing the EthErc20MetaVault contract + */ + function __EthErc20MetaVault_init(address _admin, EthErc20MetaVaultInitParams memory params) + internal + onlyInitializing + { + __ReentrancyGuard_init(); + __VaultAdmin_init(_admin, params.metadataIpfsHash); + __VaultSubVaults_init(params.subVaultsCurator); + // fee recipient is initially set to admin address + __VaultFee_init(_admin, params.feePercent); + __VaultState_init(params.capacity); + __VaultToken_init(params.name, params.symbol); + + // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 + if (msg.value < _securityDeposit) revert Errors.InvalidSecurityDeposit(); + _deposit(address(this), msg.value, address(0)); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/vaults/ethereum/EthErc20Vault.sol b/contracts/vaults/ethereum/EthErc20Vault.sol index ecf7a9ac..8dccf7a0 100644 --- a/contracts/vaults/ethereum/EthErc20Vault.sol +++ b/contracts/vaults/ethereum/EthErc20Vault.sol @@ -132,7 +132,10 @@ contract EthErc20Vault is returns (uint256 positionTicket) { positionTicket = super.enterExitQueue(shares, receiver); - emit Transfer(msg.sender, address(this), shares); + // only emit Transfer if shares were queued (not directly redeemed when non-collateralized) + if (positionTicket != type(uint256).max) { + emit Transfer(msg.sender, address(this), shares); + } } /// @inheritdoc IVaultVersion diff --git a/contracts/vaults/ethereum/EthMetaVault.sol b/contracts/vaults/ethereum/EthMetaVault.sol index e3fa8919..947c0cab 100644 --- a/contracts/vaults/ethereum/EthMetaVault.sol +++ b/contracts/vaults/ethereum/EthMetaVault.sol @@ -3,27 +3,43 @@ pragma solidity ^0.8.22; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IEthMetaVault} from "../../interfaces/IEthMetaVault.sol"; import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; import {IKeeperRewards} from "../../interfaces/IKeeperRewards.sol"; import {IVaultEthStaking} from "../../interfaces/IVaultEthStaking.sol"; +import {ISubVaultsRegistry} from "../../interfaces/ISubVaultsRegistry.sol"; import {Errors} from "../../libraries/Errors.sol"; -import {MetaVault} from "../base/MetaVault.sol"; -import {VaultEnterExit} from "../modules/VaultEnterExit.sol"; -import {VaultState} from "../modules/VaultState.sol"; -import {VaultSubVaults} from "../modules/VaultSubVaults.sol"; +import {Multicall} from "../../base/Multicall.sol"; +import {VaultImmutables} from "../modules/VaultImmutables.sol"; +import {VaultAdmin} from "../modules/VaultAdmin.sol"; import {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {VaultFee} from "../modules/VaultFee.sol"; +import {IVaultState, VaultState} from "../modules/VaultState.sol"; +import {IVaultEnterExit, VaultEnterExit} from "../modules/VaultEnterExit.sol"; +import {VaultOsToken} from "../modules/VaultOsToken.sol"; +import {VaultSubVaults} from "../modules/VaultSubVaults.sol"; /** * @title EthMetaVault * @author StakeWise * @notice Defines the Meta Vault functionality on Ethereum */ -contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { - using EnumerableSet for EnumerableSet.AddressSet; - +contract EthMetaVault is + VaultImmutables, + Initializable, + ReentrancyGuardUpgradeable, + VaultAdmin, + VaultVersion, + VaultFee, + VaultState, + VaultEnterExit, + VaultOsToken, + VaultSubVaults, + Multicall, + IEthMetaVault +{ uint8 private constant _version = 6; uint256 private constant _securityDeposit = 1e9; @@ -31,10 +47,15 @@ contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { * @dev Constructor * @dev Since the immutable variable value is stored in the bytecode, * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. - * @param args The arguments for initializing the MetaVault contract + * @param args The arguments for initializing the EthMetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(MetaVaultConstructorArgs memory args) MetaVault(args) { + constructor(EthMetaVaultConstructorArgs memory args) + VaultImmutables(args.keeper, args.vaultsRegistry) + VaultEnterExit(args.exitingAssetsClaimDelay) + VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) + VaultSubVaults(args.subVaultsRegistryFactory) + { _disableInitializers(); } @@ -42,10 +63,11 @@ contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { function initialize(bytes calldata params) external payable virtual override reinitializer(_version) { // if admin is already set, it's an upgrade from version 5 to 6 if (admin != address(0)) { + __EthMetaVault_upgrade(); return; } - __EthMetaVault_init(IEthMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (MetaVaultInitParams))); + __EthMetaVault_init(IEthMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (EthMetaVaultInitParams))); } /// @inheritdoc IEthMetaVault @@ -58,7 +80,7 @@ contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { */ receive() external payable virtual { // claim exited assets from the sub vaults should not be processed as deposits - if (_subVaults.contains(msg.sender)) { + if (ISubVaultsRegistry(subVaultsRegistry).isSubVault(msg.sender)) { return; } _deposit(msg.sender, msg.value, address(0)); @@ -117,6 +139,7 @@ contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { /// @inheritdoc VaultSubVaults function _depositToVault(address vault, uint256 assets) internal override returns (uint256) { + // slither-disable-next-line arbitrary-send-eth return IVaultEthStaking(vault).deposit{value: assets}(address(this), address(0)); } @@ -130,13 +153,65 @@ contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { return Address.sendValue(payable(receiver), assets); } + /// @inheritdoc IVaultState + function isStateUpdateRequired() + public + view + virtual + override(IVaultState, VaultState, VaultSubVaults) + returns (bool) + { + return super.isStateUpdateRequired(); + } + + /// @inheritdoc IVaultState + function updateState(IKeeperRewards.HarvestParams calldata harvestParams) + public + virtual + override(IVaultState, VaultState, VaultSubVaults) + { + super.updateState(harvestParams); + } + + /// @inheritdoc IVaultEnterExit + function enterExitQueue(uint256 shares, address receiver) + public + virtual + override(IVaultEnterExit, VaultEnterExit, VaultOsToken) + returns (uint256 positionTicket) + { + return super.enterExitQueue(shares, receiver); + } + + /// @inheritdoc VaultImmutables + function _checkHarvested() internal view virtual override(VaultImmutables, VaultSubVaults) { + super._checkHarvested(); + } + + /// @inheritdoc VaultImmutables + function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { + return super._isCollateralized(); + } + + /** + * @dev Upgrades the EthMetaVault contract + */ + function __EthMetaVault_upgrade() internal { + __VaultSubVaults_upgrade(); + } + /** * @dev Initializes the EthMetaVault contract - * @param admin The address of the admin of the Vault - * @param params The parameters for initializing the MetaVault contract + * @param _admin The address of the admin of the Vault + * @param params The parameters for initializing the EthMetaVault contract */ - function __EthMetaVault_init(address admin, MetaVaultInitParams memory params) internal onlyInitializing { - __MetaVault_init(admin, params); + function __EthMetaVault_init(address _admin, EthMetaVaultInitParams memory params) internal onlyInitializing { + __ReentrancyGuard_init(); + __VaultAdmin_init(_admin, params.metadataIpfsHash); + __VaultSubVaults_init(params.subVaultsCurator); + // fee recipient is initially set to admin address + __VaultFee_init(_admin, params.feePercent); + __VaultState_init(params.capacity); // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 if (msg.value < _securityDeposit) revert Errors.InvalidSecurityDeposit(); diff --git a/contracts/vaults/ethereum/EthMetaVaultFactory.sol b/contracts/vaults/ethereum/EthMetaVaultFactory.sol index 7a1f401d..f1d23ff0 100644 --- a/contracts/vaults/ethereum/EthMetaVaultFactory.sol +++ b/contracts/vaults/ethereum/EthMetaVaultFactory.sol @@ -6,7 +6,6 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; import {IEthMetaVault} from "../../interfaces/IEthMetaVault.sol"; import {IVaultsRegistry} from "../../interfaces/IVaultsRegistry.sol"; -import {Errors} from "../../libraries/Errors.sol"; /** * @title EthMetaVaultFactory @@ -37,6 +36,9 @@ contract EthMetaVaultFactory is IEthMetaVaultFactory { // create vault vault = address(new ERC1967Proxy(implementation, "")); + // add vault to the registry + _vaultsRegistry.addVault(vault); + // set admin so that it can be initialized in the Vault vaultAdmin = msg.sender; @@ -46,9 +48,6 @@ contract EthMetaVaultFactory is IEthMetaVaultFactory { // cleanup admin delete vaultAdmin; - // add vault to the registry - _vaultsRegistry.addVault(vault); - // emit event emit MetaVaultCreated(msg.sender, msg.sender, vault, params); } diff --git a/contracts/vaults/gnosis/GnoPrivMetaVault.sol b/contracts/vaults/ethereum/EthPrivErc20MetaVault.sol similarity index 50% rename from contracts/vaults/gnosis/GnoPrivMetaVault.sol rename to contracts/vaults/ethereum/EthPrivErc20MetaVault.sol index 21674251..62423567 100644 --- a/contracts/vaults/gnosis/GnoPrivMetaVault.sol +++ b/contracts/vaults/ethereum/EthPrivErc20MetaVault.sol @@ -3,59 +3,72 @@ pragma solidity ^0.8.22; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {IGnoMetaVaultFactory} from "../../interfaces/IGnoMetaVaultFactory.sol"; -import {IGnoPrivMetaVault} from "../../interfaces/IGnoPrivMetaVault.sol"; +import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; +import {IEthPrivErc20MetaVault} from "../../interfaces/IEthPrivErc20MetaVault.sol"; +import {ISubVaultsRegistry} from "../../interfaces/ISubVaultsRegistry.sol"; +import {ERC20Upgradeable} from "../../base/ERC20Upgradeable.sol"; import {IVaultOsToken, VaultOsToken} from "../modules/VaultOsToken.sol"; -import {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {IVaultVersion} from "../modules/VaultVersion.sol"; import {VaultWhitelist} from "../modules/VaultWhitelist.sol"; -import {GnoMetaVault, IGnoMetaVault} from "./GnoMetaVault.sol"; +import {EthErc20MetaVault, IEthErc20MetaVault} from "./EthErc20MetaVault.sol"; /** - * @title GnoPrivMetaVault + * @title EthPrivErc20MetaVault * @author StakeWise - * @notice Defines the Meta Vault functionality with whitelist on Gnosis + * @notice Defines the Meta Vault functionality with whitelist and ERC-20 token on Ethereum */ -contract GnoPrivMetaVault is Initializable, GnoMetaVault, VaultWhitelist, IGnoPrivMetaVault { +contract EthPrivErc20MetaVault is Initializable, EthErc20MetaVault, VaultWhitelist, IEthPrivErc20MetaVault { // slither-disable-next-line shadowing-state - uint8 private constant _version = 4; + uint8 private constant _version = 6; /** * @dev Constructor * @dev Since the immutable variable value is stored in the bytecode, * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. - * @param gnoToken The address of the GNO token contract - * @param args The arguments for initializing the GnoMetaVault contract + * @param args The arguments for initializing the EthErc20MetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address gnoToken, MetaVaultConstructorArgs memory args) GnoMetaVault(gnoToken, args) { + constructor(EthErc20MetaVaultConstructorArgs memory args) EthErc20MetaVault(args) { _disableInitializers(); } - /// @inheritdoc IGnoMetaVault + /// @inheritdoc IEthErc20MetaVault function initialize(bytes calldata params) external + payable virtual - override(IGnoMetaVault, GnoMetaVault) + override(IEthErc20MetaVault, EthErc20MetaVault) reinitializer(_version) { - // do not check for the upgrades since this is the first implementation of GnoPrivMetaVault + // do not check for the upgrades since this is the first implementation of EthPrivErc20MetaVault // initialize deployed vault - address _admin = IGnoMetaVaultFactory(msg.sender).vaultAdmin(); - __GnoMetaVault_init(_admin, abi.decode(params, (MetaVaultInitParams))); + address _admin = IEthMetaVaultFactory(msg.sender).vaultAdmin(); + __EthErc20MetaVault_init(_admin, abi.decode(params, (EthErc20MetaVaultInitParams))); // whitelister is initially set to admin address __VaultWhitelist_init(_admin); } - /// @inheritdoc IGnoMetaVault - function deposit(uint256 assets, address receiver, address referrer) + /// @inheritdoc IEthErc20MetaVault + function deposit(address receiver, address referrer) public + payable virtual - override(IGnoMetaVault, GnoMetaVault) + override(IEthErc20MetaVault, EthErc20MetaVault) returns (uint256 shares) { _checkWhitelist(msg.sender); _checkWhitelist(receiver); - return super.deposit(assets, receiver, referrer); + return super.deposit(receiver, referrer); + } + + /// @inheritdoc EthErc20MetaVault + receive() external payable virtual override { + // claim exited assets from the sub vaults should not be processed as deposits + if (ISubVaultsRegistry(subVaultsRegistry).isSubVault(msg.sender)) { + return; + } + _checkWhitelist(msg.sender); + _deposit(msg.sender, msg.value, address(0)); } /// @inheritdoc IVaultOsToken @@ -70,15 +83,22 @@ contract GnoPrivMetaVault is Initializable, GnoMetaVault, VaultWhitelist, IGnoPr } /// @inheritdoc IVaultVersion - function vaultId() public pure virtual override(IVaultVersion, GnoMetaVault) returns (bytes32) { - return keccak256("GnoPrivMetaVault"); + function vaultId() public pure virtual override(IVaultVersion, EthErc20MetaVault) returns (bytes32) { + return keccak256("EthPrivErc20MetaVault"); } /// @inheritdoc IVaultVersion - function version() public pure virtual override(IVaultVersion, GnoMetaVault) returns (uint8) { + function version() public pure virtual override(IVaultVersion, EthErc20MetaVault) returns (uint8) { return _version; } + /// @inheritdoc ERC20Upgradeable + function _transfer(address from, address to, uint256 amount) internal virtual override { + _checkWhitelist(from); + _checkWhitelist(to); + super._transfer(from, to, amount); + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/contracts/vaults/ethereum/EthPrivMetaVault.sol b/contracts/vaults/ethereum/EthPrivMetaVault.sol index d8f2bfb7..f9c3a77f 100644 --- a/contracts/vaults/ethereum/EthPrivMetaVault.sol +++ b/contracts/vaults/ethereum/EthPrivMetaVault.sol @@ -3,9 +3,9 @@ pragma solidity ^0.8.22; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; import {IEthPrivMetaVault} from "../../interfaces/IEthPrivMetaVault.sol"; +import {ISubVaultsRegistry} from "../../interfaces/ISubVaultsRegistry.sol"; import {IVaultOsToken, VaultOsToken} from "../modules/VaultOsToken.sol"; import {IVaultVersion} from "../modules/VaultVersion.sol"; import {VaultWhitelist} from "../modules/VaultWhitelist.sol"; @@ -17,8 +17,6 @@ import {EthMetaVault, IEthMetaVault} from "./EthMetaVault.sol"; * @notice Defines the Meta Vault functionality with whitelist on Ethereum */ contract EthPrivMetaVault is Initializable, EthMetaVault, VaultWhitelist, IEthPrivMetaVault { - using EnumerableSet for EnumerableSet.AddressSet; - // slither-disable-next-line shadowing-state uint8 private constant _version = 6; @@ -29,7 +27,7 @@ contract EthPrivMetaVault is Initializable, EthMetaVault, VaultWhitelist, IEthPr * @param args The arguments for initializing the EthMetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(MetaVaultConstructorArgs memory args) EthMetaVault(args) { + constructor(EthMetaVaultConstructorArgs memory args) EthMetaVault(args) { _disableInitializers(); } @@ -44,7 +42,7 @@ contract EthPrivMetaVault is Initializable, EthMetaVault, VaultWhitelist, IEthPr // do not check for the upgrades since this is the first implementation of EthPrivMetaVault // initialize deployed vault address _admin = IEthMetaVaultFactory(msg.sender).vaultAdmin(); - __EthMetaVault_init(_admin, abi.decode(params, (MetaVaultInitParams))); + __EthMetaVault_init(_admin, abi.decode(params, (EthMetaVaultInitParams))); // whitelister is initially set to admin address __VaultWhitelist_init(_admin); } @@ -65,7 +63,7 @@ contract EthPrivMetaVault is Initializable, EthMetaVault, VaultWhitelist, IEthPr /// @inheritdoc EthMetaVault receive() external payable virtual override { // claim exited assets from the sub vaults should not be processed as deposits - if (_subVaults.contains(msg.sender)) { + if (ISubVaultsRegistry(subVaultsRegistry).isSubVault(msg.sender)) { return; } _checkWhitelist(msg.sender); diff --git a/contracts/vaults/gnosis/GnoMetaVault.sol b/contracts/vaults/gnosis/GnoMetaVault.sol index 34e27d1b..91d0cabf 100644 --- a/contracts/vaults/gnosis/GnoMetaVault.sol +++ b/contracts/vaults/gnosis/GnoMetaVault.sol @@ -5,22 +5,41 @@ pragma solidity ^0.8.22; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {IGnoMetaVault} from "../../interfaces/IGnoMetaVault.sol"; import {IGnoMetaVaultFactory} from "../../interfaces/IGnoMetaVaultFactory.sol"; +import {IKeeperRewards} from "../../interfaces/IKeeperRewards.sol"; import {IVaultGnoStaking} from "../../interfaces/IVaultGnoStaking.sol"; import {Errors} from "../../libraries/Errors.sol"; -import {MetaVault} from "../base/MetaVault.sol"; -import {VaultEnterExit} from "../modules/VaultEnterExit.sol"; -import {VaultState} from "../modules/VaultState.sol"; -import {IVaultSubVaults, VaultSubVaults} from "../modules/VaultSubVaults.sol"; +import {Multicall} from "../../base/Multicall.sol"; +import {VaultImmutables} from "../modules/VaultImmutables.sol"; +import {VaultAdmin} from "../modules/VaultAdmin.sol"; import {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {VaultFee} from "../modules/VaultFee.sol"; +import {IVaultState, VaultState} from "../modules/VaultState.sol"; +import {IVaultEnterExit, VaultEnterExit} from "../modules/VaultEnterExit.sol"; +import {VaultOsToken} from "../modules/VaultOsToken.sol"; +import {IVaultSubVaults, VaultSubVaults} from "../modules/VaultSubVaults.sol"; /** * @title GnoMetaVault * @author StakeWise * @notice Defines the Meta Vault functionality on Gnosis */ -contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { +contract GnoMetaVault is + VaultImmutables, + Initializable, + ReentrancyGuardUpgradeable, + VaultAdmin, + VaultVersion, + VaultFee, + VaultState, + VaultEnterExit, + VaultOsToken, + VaultSubVaults, + Multicall, + IGnoMetaVault +{ uint8 private constant _version = 4; uint256 private constant _securityDeposit = 1e9; @@ -31,10 +50,15 @@ contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { * @dev Since the immutable variable value is stored in the bytecode, * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. * @param gnoToken The address of the GNO token contract - * @param args The arguments for initializing the MetaVault contract + * @param args The arguments for initializing the GnoMetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address gnoToken, MetaVaultConstructorArgs memory args) MetaVault(args) { + constructor(address gnoToken, GnoMetaVaultConstructorArgs memory args) + VaultImmutables(args.keeper, args.vaultsRegistry) + VaultEnterExit(args.exitingAssetsClaimDelay) + VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) + VaultSubVaults(args.subVaultsRegistryFactory) + { _gnoToken = IERC20(gnoToken); _disableInitializers(); } @@ -43,10 +67,11 @@ contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { function initialize(bytes calldata params) external virtual override reinitializer(_version) { // if admin is already set, it's an upgrade from version 3 to 4 if (admin != address(0)) { + __GnoMetaVault_upgrade(); return; } - __GnoMetaVault_init(IGnoMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (MetaVaultInitParams))); + __GnoMetaVault_init(IGnoMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (GnoMetaVaultInitParams))); } /// @inheritdoc IGnoMetaVault @@ -61,20 +86,6 @@ contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { shares = _deposit(receiver, assets, referrer); } - /// @inheritdoc IVaultSubVaults - function addSubVault(address vault) public virtual override(IVaultSubVaults, VaultSubVaults) { - super.addSubVault(vault); - // approve transferring GNO to sub-vault - _gnoToken.approve(vault, type(uint256).max); - } - - /// @inheritdoc IVaultSubVaults - function ejectSubVault(address vault) public virtual override(IVaultSubVaults, VaultSubVaults) { - super.ejectSubVault(vault); - // revoke transferring GNO to sub-vault - _gnoToken.approve(vault, 0); - } - /// @inheritdoc IGnoMetaVault function donateAssets(uint256 amount) external override nonReentrant { if (amount == 0) { @@ -98,6 +109,7 @@ contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { /// @inheritdoc VaultSubVaults function _depositToVault(address vault, uint256 assets) internal override returns (uint256) { + _gnoToken.approve(vault, assets); return IVaultGnoStaking(vault).deposit(assets, address(this), address(0)); } @@ -111,13 +123,65 @@ contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { SafeERC20.safeTransfer(_gnoToken, receiver, assets); } + /// @inheritdoc IVaultState + function isStateUpdateRequired() + public + view + virtual + override(IVaultState, VaultState, VaultSubVaults) + returns (bool) + { + return super.isStateUpdateRequired(); + } + + /// @inheritdoc IVaultState + function updateState(IKeeperRewards.HarvestParams calldata harvestParams) + public + virtual + override(IVaultState, VaultState, VaultSubVaults) + { + super.updateState(harvestParams); + } + + /// @inheritdoc IVaultEnterExit + function enterExitQueue(uint256 shares, address receiver) + public + virtual + override(IVaultEnterExit, VaultEnterExit, VaultOsToken) + returns (uint256 positionTicket) + { + return super.enterExitQueue(shares, receiver); + } + + /// @inheritdoc VaultImmutables + function _checkHarvested() internal view virtual override(VaultImmutables, VaultSubVaults) { + super._checkHarvested(); + } + + /// @inheritdoc VaultImmutables + function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { + return super._isCollateralized(); + } + + /** + * @dev Upgrades the GnoMetaVault contract + */ + function __GnoMetaVault_upgrade() internal { + __VaultSubVaults_upgrade(); + } + /** * @dev Initializes the GnoMetaVault contract - * @param admin The address of the admin of the Vault - * @param params The parameters for initializing the MetaVault contract + * @param _admin The address of the admin of the Vault + * @param params The parameters for initializing the GnoMetaVault contract */ - function __GnoMetaVault_init(address admin, MetaVaultInitParams memory params) internal onlyInitializing { - __MetaVault_init(admin, params); + function __GnoMetaVault_init(address _admin, GnoMetaVaultInitParams memory params) internal onlyInitializing { + __ReentrancyGuard_init(); + __VaultAdmin_init(_admin, params.metadataIpfsHash); + __VaultSubVaults_init(params.subVaultsCurator); + // fee recipient is initially set to admin address + __VaultFee_init(_admin, params.feePercent); + __VaultState_init(params.capacity); _deposit(address(this), _securityDeposit, address(0)); // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 diff --git a/contracts/vaults/gnosis/GnoMetaVaultFactory.sol b/contracts/vaults/gnosis/GnoMetaVaultFactory.sol index 534c923a..ca30476e 100644 --- a/contracts/vaults/gnosis/GnoMetaVaultFactory.sol +++ b/contracts/vaults/gnosis/GnoMetaVaultFactory.sol @@ -8,7 +8,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IGnoMetaVaultFactory} from "../../interfaces/IGnoMetaVaultFactory.sol"; import {IGnoMetaVault} from "../../interfaces/IGnoMetaVault.sol"; import {IVaultsRegistry} from "../../interfaces/IVaultsRegistry.sol"; -import {Errors} from "../../libraries/Errors.sol"; /** * @title GnoMetaVaultFactory @@ -49,6 +48,9 @@ contract GnoMetaVaultFactory is IGnoMetaVaultFactory { // create vault vault = address(new ERC1967Proxy(implementation, "")); + // add vault to the registry + _vaultsRegistry.addVault(vault); + // approve GNO token for the vault security deposit _gnoToken.approve(vault, _securityDeposit); @@ -61,9 +63,6 @@ contract GnoMetaVaultFactory is IGnoMetaVaultFactory { // cleanup admin delete vaultAdmin; - // add vault to the registry - _vaultsRegistry.addVault(vault); - // emit event emit MetaVaultCreated(msg.sender, msg.sender, vault, params); } diff --git a/contracts/vaults/modules/VaultOsToken.sol b/contracts/vaults/modules/VaultOsToken.sol index af80f443..3328092d 100644 --- a/contracts/vaults/modules/VaultOsToken.sol +++ b/contracts/vaults/modules/VaultOsToken.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.22; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IVaultOsToken} from "../../interfaces/IVaultOsToken.sol"; import {IOsTokenVaultController} from "../../interfaces/IOsTokenVaultController.sol"; import {IOsTokenConfig} from "../../interfaces/IOsTokenConfig.sol"; -import {IVaultOsToken} from "../../interfaces/IVaultOsToken.sol"; import {IOsTokenVaultEscrow} from "../../interfaces/IOsTokenVaultEscrow.sol"; import {Errors} from "../../libraries/Errors.sol"; import {VaultImmutables} from "./VaultImmutables.sol"; @@ -23,10 +23,10 @@ abstract contract VaultOsToken is VaultImmutables, VaultState, VaultEnterExit, I uint256 private constant _maxPercent = 1e18; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IOsTokenVaultController internal immutable _osTokenVaultController; + IOsTokenVaultController private immutable _osTokenVaultController; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IOsTokenConfig internal immutable _osTokenConfig; + IOsTokenConfig private immutable _osTokenConfig; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IOsTokenVaultEscrow private immutable _osTokenVaultEscrow; diff --git a/contracts/vaults/modules/VaultSubVaults.sol b/contracts/vaults/modules/VaultSubVaults.sol index f916e43d..839949fa 100644 --- a/contracts/vaults/modules/VaultSubVaults.sol +++ b/contracts/vaults/modules/VaultSubVaults.sol @@ -2,25 +2,17 @@ pragma solidity ^0.8.22; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {IVaultsRegistry} from "../../interfaces/IVaultsRegistry.sol"; import {IKeeperRewards} from "../../interfaces/IKeeperRewards.sol"; -import {IVaultEnterExit} from "../../interfaces/IVaultEnterExit.sol"; -import {ISubVaultsCurator} from "../../interfaces/ISubVaultsCurator.sol"; import {IVaultSubVaults} from "../../interfaces/IVaultSubVaults.sol"; -import {ICuratorsRegistry} from "../../interfaces/ICuratorsRegistry.sol"; -import {IVaultVersion} from "../../interfaces/IVaultVersion.sol"; -import {ExitQueue} from "../../libraries/ExitQueue.sol"; +import {IVaultEnterExit} from "../../interfaces/IVaultEnterExit.sol"; +import {IVaultOsToken} from "../../interfaces/IVaultOsToken.sol"; +import {ISubVaultsRegistry} from "../../interfaces/ISubVaultsRegistry.sol"; +import {IOsTokenRedeemer} from "../../interfaces/IOsTokenRedeemer.sol"; +import {ISubVaultsRegistryFactory} from "../../interfaces/ISubVaultsRegistryFactory.sol"; import {Errors} from "../../libraries/Errors.sol"; -import {SubVaultUtils} from "../../libraries/SubVaultUtils.sol"; -import {SubVaultExits} from "../../libraries/SubVaultExits.sol"; -import {VaultAdmin} from "./VaultAdmin.sol"; import {VaultImmutables} from "./VaultImmutables.sol"; import {VaultState, IVaultState} from "./VaultState.sol"; @@ -29,270 +21,120 @@ import {VaultState, IVaultState} from "./VaultState.sol"; * @author StakeWise * @notice Defines the functionality for managing the Vault sub-vaults */ -abstract contract VaultSubVaults is - VaultImmutables, - Initializable, - ReentrancyGuardUpgradeable, - VaultAdmin, - VaultState, - IVaultSubVaults -{ +abstract contract VaultSubVaults is VaultImmutables, Initializable, VaultState, IVaultSubVaults { using EnumerableSet for EnumerableSet.AddressSet; using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _curatorsRegistry; + address private immutable _subVaultsRegistryFactory; - /// @inheritdoc IVaultSubVaults - address public override subVaultsCurator; + /// @dev Deprecated: moved to SubVaultsRegistry + address private __deprecated__subVaultsCurator; - /// @inheritdoc IVaultSubVaults - address public override ejectingSubVault; + /// @dev Deprecated: moved to SubVaultsRegistry + address private __deprecated__ejectingSubVault; - EnumerableSet.AddressSet internal _subVaults; - mapping(address vault => DoubleEndedQueue.Bytes32Deque) private _subVaultsExits; - mapping(address vault => SubVaultState state) internal _subVaultsStates; + /// @dev Deprecated: moved to SubVaultsRegistry + EnumerableSet.AddressSet private __deprecated__subVaults; - /// @inheritdoc IVaultSubVaults - uint128 public override subVaultsRewardsNonce; - uint128 internal _subVaultsTotalAssets; + /// @dev Deprecated: moved to SubVaultsRegistry + mapping(address vault => DoubleEndedQueue.Bytes32Deque) private __deprecated__subVaultsExits; + + /// @dev Deprecated: moved to SubVaultsRegistry + mapping(address vault => ISubVaultsRegistry.SubVaultState state) private __deprecated__subVaultsStates; - uint256 private _totalProcessedExitQueueTickets; - uint256 internal _ejectingSubVaultShares; + /// @dev Deprecated: moved to SubVaultsRegistry + uint128 private __deprecated__subVaultsRewardsNonce; + + /// @dev Deprecated: moved to SubVaultsRegistry + uint128 private __deprecated__subVaultsTotalAssets; + + /// @dev Deprecated: moved to SubVaultsRegistry + uint256 private __deprecated__totalProcessedExitQueueTickets; + + /// @dev Deprecated: moved to SubVaultsRegistry + uint256 private __deprecated__ejectingSubVaultShares; /// @inheritdoc IVaultSubVaults - address public override pendingMetaSubVault; + address public override subVaultsRegistry; /** * @dev Constructor * @dev Since the immutable variable value is stored in the bytecode, * its value would be shared among all proxies pointing to a given contract instead of each proxy’s storage. - * @param curatorsRegistry The address of the CuratorsRegistry contract + * @param subVaultsRegistryFactory The address of the factory used to deploy SubVaultsRegistry contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address curatorsRegistry) { - _curatorsRegistry = curatorsRegistry; - } - - /// @inheritdoc IVaultSubVaults - function subVaultsStates(address vault) external view override returns (SubVaultState memory) { - return _subVaultsStates[vault]; + constructor(address subVaultsRegistryFactory) { + _subVaultsRegistryFactory = subVaultsRegistryFactory; } /// @inheritdoc IVaultSubVaults - function getSubVaults() public view override returns (address[] memory) { - return _subVaults.values(); + function depositToSubVault(address vault, uint256 assets) external override returns (uint256) { + _checkSubVaultsRegistry(); + return _depositToVault(vault, assets); } /// @inheritdoc IVaultSubVaults - function setSubVaultsCurator(address curator) external override { - _checkAdmin(); - _setSubVaultsCurator(curator); + function enterSubVaultExitQueue(address vault, uint256 shares) external override returns (uint256 positionTicket) { + _checkSubVaultsRegistry(); + return IVaultEnterExit(vault).enterExitQueue(shares, address(this)); } /// @inheritdoc IVaultSubVaults - function addSubVault(address vault) public virtual override { - _checkAdmin(); - - // check new sub-vault validity - SubVaultUtils.validateSubVault(_subVaults, _vaultsRegistry, _keeper, vault); - - if (_isMetaVault(vault)) { - // meta vault must be approved before being added as a sub vault - if (pendingMetaSubVault != address(0)) { - revert Errors.AlreadyAdded(); - } - pendingMetaSubVault = vault; - emit MetaSubVaultProposed(msg.sender, vault); - } else { - _addSubVault(vault); - } - } - - /// @inheritdoc IVaultSubVaults - function acceptMetaSubVault(address metaSubVault) external virtual override { - // only the VaultsRegistry owner can accept a meta vault addition as a sub vault - if (msg.sender != Ownable(_vaultsRegistry).owner()) { - revert Errors.AccessDenied(); - } - - if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { - revert Errors.InvalidVault(); - } - - // check sub-vault validity - SubVaultUtils.validateSubVault(_subVaults, _vaultsRegistry, _keeper, metaSubVault); - - // update state - delete pendingMetaSubVault; - _addSubVault(metaSubVault); + function claimSubVaultExitedAssets(address vault, uint256 positionTicket, uint256 timestamp, uint256 exitQueueIndex) + external + override + { + _checkSubVaultsRegistry(); + IVaultEnterExit(vault).claimExitedAssets(positionTicket, timestamp, exitQueueIndex); } /// @inheritdoc IVaultSubVaults - function rejectMetaSubVault(address metaSubVault) external virtual override { - // only the VaultsRegistry owner or admin can reject a meta vault addition as a sub vault - if (msg.sender != Ownable(_vaultsRegistry).owner() && msg.sender != admin) { - revert Errors.AccessDenied(); - } - - if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { - revert Errors.InvalidVault(); - } - - // update state - delete pendingMetaSubVault; - - // emit event - emit MetaSubVaultRejected(msg.sender, metaSubVault); + function mintSubVaultOsToken(address vault, address receiver, uint256 osTokenShares) external override { + _checkSubVaultsRegistry(); + IVaultOsToken(vault).mintOsToken(receiver, osTokenShares, address(0)); } /// @inheritdoc IVaultSubVaults - function ejectSubVault(address vault) public virtual override { - _checkAdmin(); - - (bool ejected, uint128 ejectingShares) = - SubVaultUtils.ejectSubVault(_subVaults, _subVaultsStates, _subVaultsExits, ejectingSubVault, vault); - - if (ejected) { - emit SubVaultEjected(msg.sender, vault); - } else { - ejectingSubVault = vault; - _ejectingSubVaultShares = ejectingShares; - emit SubVaultEjecting(msg.sender, vault); - } + function redeemSubVaultOsToken(address vault, address redeemer, uint256 osTokenShares) + external + override + returns (uint256 assets) + { + _checkSubVaultsRegistry(); + return IOsTokenRedeemer(redeemer).redeemSubVaultOsToken(vault, osTokenShares); } /// @inheritdoc IVaultState - function isStateUpdateRequired() public view virtual override returns (bool) { - // SLOAD to memory - uint256 currentNonce = _getCurrentRewardsNonce(); - unchecked { - // cannot realistically overflow - return subVaultsRewardsNonce + 1 < currentNonce; - } - } - - /// @inheritdoc IVaultSubVaults - function canUpdateState() external view override returns (bool) { - uint256 nonce = subVaultsRewardsNonce; - return nonce != 0 && nonce < _getCurrentRewardsNonce(); + function isStateUpdateRequired() public view virtual override(IVaultState, VaultState) returns (bool) { + return ISubVaultsRegistry(subVaultsRegistry).isStateUpdateRequired(); } - /// @inheritdoc IVaultSubVaults - function isCollateralized() external view override returns (bool) { - return _subVaults.length() > 0; - } - - /// @inheritdoc IVaultSubVaults - function depositToSubVaults() external override nonReentrant { - _checkHarvested(); - - address[] memory vaults = getSubVaults(); - uint256 vaultsLength = vaults.length; - if (vaultsLength == 0) revert Errors.EmptySubVaults(); - - // deposit accumulated assets to sub vaults - uint256 availableAssets = withdrawableAssets(); - if (availableAssets == 0) { - revert Errors.InvalidAssets(); - } - ISubVaultsCurator.Deposit[] memory deposits = - ISubVaultsCurator(subVaultsCurator).getDeposits(availableAssets, vaults, ejectingSubVault); - - // process deposits - uint256 depositsLength = deposits.length; - // SLOAD to memory - uint256 subVaultsTotalAssets = _subVaultsTotalAssets; - for (uint256 i = 0; i < depositsLength;) { - ISubVaultsCurator.Deposit memory depositData = deposits[i]; - if (depositData.assets == 0) { - // skip empty deposits - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - - // reverts if there are more deposits than available assets - availableAssets -= depositData.assets; - - // update state - uint128 vaultShares = SafeCast.toUint128(_depositToVault(depositData.vault, depositData.assets)); - _subVaultsStates[depositData.vault].stakedShares += vaultShares; - subVaultsTotalAssets += depositData.assets; - unchecked { - // cannot realistically overflow - ++i; - } - } - // update last sync sub vaults assets - _subVaultsTotalAssets = SafeCast.toUint128(subVaultsTotalAssets); - } - - /// @inheritdoc IVaultSubVaults - function claimSubVaultsExitedAssets(SubVaultExitRequest[] calldata exitRequests) external override { + /// @inheritdoc IVaultState + function updateState(IKeeperRewards.HarvestParams calldata) public virtual override(IVaultState, VaultState) { // SLOAD to memory - address _ejectingSubVault = ejectingSubVault; - uint256 totalExitedAssets = - SubVaultUtils.claimSubVaultsExitedAssets(_subVaultsStates, _subVaultsExits, exitRequests); - - if (_ejectingSubVault != address(0)) { - // check whether ejecting vault can be cleaned up - SubVaultState memory subVaultState = _subVaultsStates[_ejectingSubVault]; - if (subVaultState.queuedShares == 0) { - // clean up ejecting vault - delete ejectingSubVault; - delete _ejectingSubVaultShares; - _subVaultsExits[_ejectingSubVault].clear(); - _subVaults.remove(_ejectingSubVault); - emit SubVaultEjected(msg.sender, _ejectingSubVault); - } - } - - // update sub vaults total assets - _subVaultsTotalAssets -= SafeCast.toUint128(totalExitedAssets); - } + ISubVaultsRegistry _subVaultsRegistry = ISubVaultsRegistry(subVaultsRegistry); + (int256 totalAssetsDelta, bool harvested) = _subVaultsRegistry.harvestSubVaultsAssets(); - /// @inheritdoc IVaultState - function updateState(IKeeperRewards.HarvestParams calldata) public virtual override { - // fetch all the vaults - address[] memory vaults = getSubVaults(); - uint256 vaultsLength = vaults.length; - if (vaultsLength == 0) revert Errors.EmptySubVaults(); - - // sync rewards nonce - bool isHarvested = _syncRewardsNonce(vaults); - if (!isHarvested) { + // process total assets delta only if harvested + if (!harvested) { return; } - // check claims - _checkSubVaultsExitClaims(vaults); - - // calculate new total assets and save balances in each sub vault - uint256[] memory balances; - uint256 newSubVaultsTotalAssets; - (balances, newSubVaultsTotalAssets) = SubVaultUtils.getSubVaultsBalances(_subVaultsStates, vaults, true); - - // store new sub vaults total assets delta - int256 totalAssetsDelta = SafeCast.toInt256(newSubVaultsTotalAssets) - SafeCast.toInt256(_subVaultsTotalAssets); - // SLOAD to memory uint256 donatedAssets = _donatedAssets; if (donatedAssets > 0) { + // add donated assets to total assets delta totalAssetsDelta += int256(donatedAssets); _donatedAssets = 0; } - _subVaultsTotalAssets = SafeCast.toUint128(newSubVaultsTotalAssets); - emit SubVaultsHarvested(totalAssetsDelta); - _processTotalAssetsDelta(totalAssetsDelta); _updateExitQueue(); - _enterSubVaultsExitQueue(vaults, balances); + _subVaultsRegistry.enterSubVaultsExitQueue(); } /// @inheritdoc VaultState @@ -306,216 +148,6 @@ abstract contract VaultSubVaults is return (0, false); } - /** - * @dev Internal function to add a sub-vault - * @param vault The address of the sub-vault to add - */ - function _addSubVault(address vault) private { - // update nonce - uint256 vaultNonce = _getSubVaultRewardsNonce(vault); - uint256 lastSubVaultsRewardsNonce = subVaultsRewardsNonce; - if (_subVaults.length() == 0) { - subVaultsRewardsNonce = SafeCast.toUint128(vaultNonce); - emit RewardsNonceUpdated(vaultNonce); - } else if (vaultNonce != lastSubVaultsRewardsNonce) { - revert Errors.NotHarvested(); - } - - _subVaults.add(vault); - emit SubVaultAdded(msg.sender, vault); - } - - /** - * @dev Internal function to enter the exit queue for sub vaults - * @param vaults The addresses of the sub vaults - * @param balances The balances of the sub vaults - */ - function _enterSubVaultsExitQueue(address[] memory vaults, uint256[] memory balances) private nonReentrant { - // SLOAD to memory - uint256 totalExitedTickets = ExitQueue.getLatestTotalTickets(_exitQueue); - uint256 totalProcessedTickets = Math.max(_totalProcessedExitQueueTickets, totalExitedTickets); - - // calculate unprocessed exit queue tickets - uint256 unprocessedTickets = _queuedShares - (totalProcessedTickets - totalExitedTickets); - if (unprocessedTickets == 0) { - // nothing to process - return; - } - - // update state - _totalProcessedExitQueueTickets = totalProcessedTickets + unprocessedTickets; - - // check whether ejecting vault has exiting assets - uint256 unprocessedAssets = convertToAssets(unprocessedTickets); - if (unprocessedAssets == 0) { - // nothing to process - return; - } - - unprocessedAssets -= _consumeEjectingSubVaultAssets(unprocessedAssets); - if (unprocessedAssets == 0) { - return; - } - - // fetch exit requests from the curator - ISubVaultsCurator.ExitRequest[] memory exits = - ISubVaultsCurator(subVaultsCurator).getExitRequests(unprocessedAssets, vaults, balances, ejectingSubVault); - - // process exits - uint256 processedAssets; - uint256 exitsLength = exits.length; - for (uint256 i = 0; i < exitsLength;) { - // submit exit request to the vault - ISubVaultsCurator.ExitRequest memory exitRequest = exits[i]; - if (exitRequest.assets == 0) { - // skip empty exit requests - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - SubVaultState memory vaultState = _subVaultsStates[exitRequest.vault]; - uint256 vaultShares = IVaultState(exitRequest.vault).convertToShares(exitRequest.assets); - if (vaultShares == 0) { - // skip exit requests with zero shares - processedAssets += exitRequest.assets; - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - uint256 positionTicket = IVaultEnterExit(exitRequest.vault).enterExitQueue(vaultShares, address(this)); - - // save exit request - SubVaultExits.pushSubVaultExit( - _subVaultsExits, - exitRequest.vault, - SafeCast.toUint160(positionTicket), - SafeCast.toUint96(vaultShares), - false - ); - - // update state - uint128 vaultShares128 = SafeCast.toUint128(vaultShares); - vaultState.queuedShares += vaultShares128; - vaultState.stakedShares -= vaultShares128; - - _subVaultsStates[exitRequest.vault] = vaultState; - processedAssets += exitRequest.assets; - - unchecked { - // cannot realistically overflow - ++i; - } - } - if (processedAssets > unprocessedAssets) { - revert Errors.InvalidAssets(); - } - } - - /** - * @dev Internal function to check whether the sub vaults have claimed processed exit queue tickets - * @param vaults The addresses of the sub vaults - */ - function _checkSubVaultsExitClaims(address[] memory vaults) private view { - uint256 vaultsLength = vaults.length; - for (uint256 i = 0; i < vaultsLength;) { - address vault = vaults[i]; - (uint256 positionTicket, uint256 exitShares) = SubVaultExits.peekSubVaultExit(_subVaultsExits, vault); - if (positionTicket == 0 && exitShares == 0) { - // no queue positions - unchecked { - // cannot realistically overflow - ++i; - } - continue; - } - (,,,, uint256 totalExitedTickets) = IVaultState(vault).getExitQueueData(); - if (totalExitedTickets > positionTicket) { - revert Errors.UnclaimedAssets(); - } - - unchecked { - // cannot realistically overflow - ++i; - } - } - } - - /** - * @dev Internal function to check whether the vaults are harvested - * @param vaults The addresses of the vaults - * @return Whether the nonce has been updated - */ - function _syncRewardsNonce(address[] memory vaults) private returns (bool) { - // process first vault in the array - address vault = vaults[0]; - uint256 vaultNonce = _getSubVaultRewardsNonce(vault); - - // check whether the first vault is harvested - uint256 currentNonce = _getCurrentRewardsNonce(); - if (vaultNonce + 1 < currentNonce) { - revert Errors.NotHarvested(); - } - - // fetch current nonce - currentNonce = vaultNonce; - uint256 lastRewardsNonce = subVaultsRewardsNonce; - if (lastRewardsNonce > currentNonce) { - revert Errors.RewardsNonceIsHigher(); - } else if (lastRewardsNonce == currentNonce) { - return false; - } else { - // update last sync rewards nonce - subVaultsRewardsNonce = SafeCast.toUint128(currentNonce); - emit RewardsNonceUpdated(currentNonce); - } - - // all the vaults must be with the same rewards nonce - uint256 vaultsLength = vaults.length; - for (uint256 i = 1; i < vaultsLength;) { - vault = vaults[i]; - vaultNonce = _getSubVaultRewardsNonce(vault); - - // check whether the vault is harvested - if (vaultNonce != currentNonce) { - revert Errors.NotHarvested(); - } - - unchecked { - // cannot realistically overflow - ++i; - } - } - return true; - } - - /** - * @dev Internal function to consume ejecting sub-vault assets - * @param unprocessedAssets The amount of unprocessed assets - * @return processedAssets The amount of processed assets - */ - function _consumeEjectingSubVaultAssets(uint256 unprocessedAssets) private returns (uint256 processedAssets) { - // SLOAD to memory - address _ejectingSubVault = ejectingSubVault; - if (_ejectingSubVault == address(0)) { - return 0; - } - uint256 ejectingSubVaultShares = _ejectingSubVaultShares; - if (ejectingSubVaultShares == 0) { - return 0; - } - - uint256 ejectingVaultAssets = IVaultState(_ejectingSubVault).convertToAssets(ejectingSubVaultShares); - processedAssets = Math.min(unprocessedAssets, ejectingVaultAssets); - - // update state - _ejectingSubVaultShares = - ejectingSubVaultShares - IVaultState(_ejectingSubVault).convertToShares(processedAssets); - } - /// @inheritdoc VaultImmutables function _checkHarvested() internal view virtual override { if (isStateUpdateRequired()) { @@ -525,74 +157,100 @@ abstract contract VaultSubVaults is /// @inheritdoc VaultImmutables function _isCollateralized() internal view virtual override returns (bool) { - return _subVaults.length() > 0; + return ISubVaultsRegistry(subVaultsRegistry).isCollateralized(); } /** - * @dev Internal function to get the rewards nonce of a sub-vault - * @param subVault The address of the sub-vault - * @return The rewards nonce of the sub-vault + * @dev Internal function to deposit assets to the sub-vault + * @param vault The address of the vault + * @param assets The amount of assets to deposit + * @return The amount of vault shares received */ - function _getSubVaultRewardsNonce(address subVault) private view returns (uint256) { - try IVaultSubVaults(subVault).subVaultsRewardsNonce() returns (uint128 nonce) { - return nonce; - } catch {} - - (, uint256 vaultNonce) = IKeeperRewards(_keeper).rewards(subVault); - return vaultNonce; - } + function _depositToVault(address vault, uint256 assets) internal virtual returns (uint256); /** - * @dev Internal function to get the current rewards nonce from the Keeper contract - * @return The current rewards nonce + * @dev Internal function to check if the caller is the SubVaultsRegistry */ - function _getCurrentRewardsNonce() private view returns (uint256) { - return IKeeperRewards(_keeper).rewardsNonce(); + function _checkSubVaultsRegistry() private view { + if (msg.sender != subVaultsRegistry) revert Errors.AccessDenied(); } /** - * @dev Internal function to set the sub-vaults curator - * @param curator The address of the sub-vaults curator + * @dev Initializes the VaultSubVaults contract + * @param curator The address of initial sub-vaults curator */ - function _setSubVaultsCurator(address curator) private { - if (curator == address(0)) revert Errors.ZeroAddress(); - if (curator == subVaultsCurator) revert Errors.ValueNotChanged(); - if (!ICuratorsRegistry(_curatorsRegistry).isCurator(curator)) { - revert Errors.InvalidCurator(); - } - subVaultsCurator = curator; - emit SubVaultsCuratorUpdated(msg.sender, curator); + function __VaultSubVaults_init(address curator) internal onlyInitializing { + address _subVaultsRegistry = ISubVaultsRegistryFactory(_subVaultsRegistryFactory).createSubVaultsRegistry(); + subVaultsRegistry = _subVaultsRegistry; + ISubVaultsRegistry(_subVaultsRegistry).initialize(address(this), curator); } /** - * @dev Internal function to check whether the vault is a meta vault - * @param vault The address of the vault - * @return True if the vault is a meta vault, false otherwise + * @dev Upgrades the VaultSubVaults contract by migrating deprecated state to SubVaultsRegistry */ - function _isMetaVault(address vault) private view returns (bool) { - try IVaultSubVaults(vault).getSubVaults() { - return true; - } catch { - return false; + function __VaultSubVaults_upgrade() internal onlyInitializing { + // get deprecated sub-vaults array + address[] memory subVaults = __deprecated__subVaults.values(); + uint256 subVaultsLength = subVaults.length; + + // prepare migration data arrays + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](subVaultsLength); + bytes32[][] memory exits = new bytes32[][](subVaultsLength); + + for (uint256 i = 0; i < subVaultsLength;) { + address vault = subVaults[i]; + + // migrate state + ISubVaultsRegistry.SubVaultState memory state = __deprecated__subVaultsStates[vault]; + states[i] = + ISubVaultsRegistry.SubVaultState({stakedShares: state.stakedShares, queuedShares: state.queuedShares}); + delete __deprecated__subVaultsStates[vault]; + + // migrate exits + DoubleEndedQueue.Bytes32Deque storage deque = __deprecated__subVaultsExits[vault]; + uint256 exitsLength = deque.length(); + exits[i] = new bytes32[](exitsLength); + for (uint256 j = 0; j < exitsLength;) { + exits[i][j] = deque.popFront(); + unchecked { + ++j; + } + } + + unchecked { + ++i; + } + + // remove vault from deprecated set + __deprecated__subVaults.remove(vault); } - } - /** - * @dev Internal function to deposit assets to the vault - * @param vault The address of the vault - * @param assets The amount of assets to deposit - * @return The amount of vault shares received - */ - function _depositToVault(address vault, uint256 assets) internal virtual returns (uint256); + // create SubVaultsRegistry + subVaultsRegistry = ISubVaultsRegistryFactory(_subVaultsRegistryFactory).createSubVaultsRegistry(); + + // call migrate on SubVaultsRegistry + ISubVaultsRegistry(subVaultsRegistry) + .migrate( + ISubVaultsRegistry.MigrationData({ + curator: __deprecated__subVaultsCurator, + ejectingSubVault: __deprecated__ejectingSubVault, + ejectingSubVaultShares: __deprecated__ejectingSubVaultShares, + subVaultsRewardsNonce: __deprecated__subVaultsRewardsNonce, + subVaultsTotalAssets: __deprecated__subVaultsTotalAssets, + totalProcessedExitQueueTickets: __deprecated__totalProcessedExitQueueTickets, + subVaults: subVaults, + subVaultsStates: states, + subVaultsExits: exits + }) + ); - /** - * @dev Initializes the VaultSubVaults contract - * @param curator The address of initial sub-vaults curator - */ - function __VaultSubVaults_init(address curator) internal onlyInitializing { - __ReentrancyGuard_init(); - _setSubVaultsCurator(curator); - subVaultsRewardsNonce = SafeCast.toUint128(_getCurrentRewardsNonce()); + // clean up deprecated storage + delete __deprecated__subVaultsCurator; + delete __deprecated__ejectingSubVault; + delete __deprecated__ejectingSubVaultShares; + delete __deprecated__subVaultsRewardsNonce; + delete __deprecated__subVaultsTotalAssets; + delete __deprecated__totalProcessedExitQueueTickets; } /** diff --git a/script/Network.sol b/script/Network.sol index fa1087a4..f9c8ff56 100644 --- a/script/Network.sol +++ b/script/Network.sol @@ -182,9 +182,12 @@ abstract contract Network is Script { vm.writeJson(output, getUpgradesFilePath()); } - function generateAddressesJson(Factory[] memory newFactories, address validatorsChecker, address osTokenRedeemer) - internal - { + function generateAddressesJson( + Factory[] memory newFactories, + address validatorsChecker, + address osTokenRedeemer, + address subVaultsRegistryFactory + ) internal { Deployment memory deployment = getDeploymentData(); string memory json = "addresses"; @@ -224,6 +227,7 @@ abstract contract Network is Script { } vm.serializeAddress(json, "OsTokenRedeemer", osTokenRedeemer); + vm.serializeAddress(json, "SubVaultsRegistryFactory", subVaultsRegistryFactory); string memory output = vm.serializeAddress(json, "ValidatorsChecker", validatorsChecker); string memory path = string.concat("./deployments/", getNetworkName(), "-new.json"); vm.writeJson(output, path); diff --git a/script/UpgradeEthNetwork.s.sol b/script/UpgradeEthNetwork.s.sol index 4bb6066f..98618ecf 100644 --- a/script/UpgradeEthNetwork.s.sol +++ b/script/UpgradeEthNetwork.s.sol @@ -3,13 +3,18 @@ pragma solidity ^0.8.22; import {console} from "forge-std/console.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; +import {IEthErc20MetaVault} from "../contracts/interfaces/IEthErc20MetaVault.sol"; +import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; import {EthOsTokenRedeemer} from "../contracts/tokens/EthOsTokenRedeemer.sol"; import {EthValidatorsChecker} from "../contracts/validators/EthValidatorsChecker.sol"; +import {SubVaultsRegistry} from "../contracts/vaults/SubVaultsRegistry.sol"; +import {SubVaultsRegistryFactory} from "../contracts/vaults/SubVaultsRegistryFactory.sol"; +import {EthErc20MetaVault} from "../contracts/vaults/ethereum/EthErc20MetaVault.sol"; import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; import {EthMetaVaultFactory} from "../contracts/vaults/ethereum/EthMetaVaultFactory.sol"; +import {EthPrivErc20MetaVault} from "../contracts/vaults/ethereum/EthPrivErc20MetaVault.sol"; import {EthPrivMetaVault} from "../contracts/vaults/ethereum/EthPrivMetaVault.sol"; import {Network} from "./Network.sol"; @@ -20,6 +25,7 @@ contract UpgradeEthNetwork is Network { address public validatorsChecker; address public osTokenRedeemer; + address public subVaultsRegistryFactory; address[] public vaultImpls; Factory[] public vaultFactories; @@ -58,18 +64,33 @@ contract UpgradeEthNetwork is Network { ) ); + // Deploy SubVaultsRegistryFactory + address subVaultsRegistryImpl = address( + new SubVaultsRegistry( + deployment.curatorsRegistry, + deployment.vaultsRegistry, + deployment.keeper, + deployment.osTokenVaultController, + deployment.osTokenConfig + ) + ); + subVaultsRegistryFactory = + address(new SubVaultsRegistryFactory(subVaultsRegistryImpl, IVaultsRegistry(deployment.vaultsRegistry))); + _deployImplementations(); _deployFactories(); vm.stopBroadcast(); generateGovernorTxJson(vaultImpls, vaultFactories, osTokenRedeemer); generateUpgradesJson(vaultImpls); - generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer); + generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer, subVaultsRegistryFactory); } function _deployImplementations() internal { // constructors for implementations - IMetaVault.MetaVaultConstructorArgs memory metaVaultArgs = _getMetaVaultConstructorArgs(); + IEthMetaVault.EthMetaVaultConstructorArgs memory metaVaultArgs = _getEthMetaVaultConstructorArgs(); + IEthErc20MetaVault.EthErc20MetaVaultConstructorArgs memory erc20MetaVaultArgs = + _getEthErc20MetaVaultConstructorArgs(); // deploy meta vaults metaVaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; @@ -78,8 +99,17 @@ contract UpgradeEthNetwork is Network { metaVaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; EthPrivMetaVault ethPrivMetaVault = new EthPrivMetaVault(metaVaultArgs); + // deploy ERC20 meta vaults + erc20MetaVaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; + EthErc20MetaVault ethErc20MetaVault = new EthErc20MetaVault(erc20MetaVaultArgs); + + erc20MetaVaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; + EthPrivErc20MetaVault ethPrivErc20MetaVault = new EthPrivErc20MetaVault(erc20MetaVaultArgs); + vaultImpls.push(address(ethMetaVault)); vaultImpls.push(address(ethPrivMetaVault)); + vaultImpls.push(address(ethErc20MetaVault)); + vaultImpls.push(address(ethPrivErc20MetaVault)); } function _deployFactories() internal { @@ -93,19 +123,39 @@ contract UpgradeEthNetwork is Network { vaultFactories.push(Factory({name: "MetaVaultFactory", factory: factory})); } else if (vaultId == keccak256("EthPrivMetaVault")) { vaultFactories.push(Factory({name: "PrivMetaVaultFactory", factory: factory})); + } else if (vaultId == keccak256("EthErc20MetaVault")) { + vaultFactories.push(Factory({name: "Erc20MetaVaultFactory", factory: factory})); + } else if (vaultId == keccak256("EthPrivErc20MetaVault")) { + vaultFactories.push(Factory({name: "PrivErc20MetaVaultFactory", factory: factory})); } } } - function _getMetaVaultConstructorArgs() internal returns (IMetaVault.MetaVaultConstructorArgs memory) { + function _getEthMetaVaultConstructorArgs() internal returns (IEthMetaVault.EthMetaVaultConstructorArgs memory) { + Deployment memory deployment = getDeploymentData(); + return IEthMetaVault.EthMetaVaultConstructorArgs({ + keeper: deployment.keeper, + vaultsRegistry: deployment.vaultsRegistry, + osTokenVaultController: deployment.osTokenVaultController, + osTokenConfig: deployment.osTokenConfig, + osTokenVaultEscrow: deployment.osTokenVaultEscrow, + subVaultsRegistryFactory: subVaultsRegistryFactory, + exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY + }); + } + + function _getEthErc20MetaVaultConstructorArgs() + internal + returns (IEthErc20MetaVault.EthErc20MetaVaultConstructorArgs memory) + { Deployment memory deployment = getDeploymentData(); - return IMetaVault.MetaVaultConstructorArgs({ + return IEthErc20MetaVault.EthErc20MetaVaultConstructorArgs({ keeper: deployment.keeper, vaultsRegistry: deployment.vaultsRegistry, osTokenVaultController: deployment.osTokenVaultController, osTokenConfig: deployment.osTokenConfig, osTokenVaultEscrow: deployment.osTokenVaultEscrow, - curatorsRegistry: deployment.curatorsRegistry, + subVaultsRegistryFactory: subVaultsRegistryFactory, exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY }); } diff --git a/script/UpgradeGnoNetwork.s.sol b/script/UpgradeGnoNetwork.s.sol index 05691c1b..480d161b 100644 --- a/script/UpgradeGnoNetwork.s.sol +++ b/script/UpgradeGnoNetwork.s.sol @@ -3,14 +3,15 @@ pragma solidity ^0.8.22; import {console} from "forge-std/console.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; +import {IGnoMetaVault} from "../contracts/interfaces/IGnoMetaVault.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; import {GnoOsTokenRedeemer} from "../contracts/tokens/GnoOsTokenRedeemer.sol"; import {GnoValidatorsChecker} from "../contracts/validators/GnoValidatorsChecker.sol"; +import {SubVaultsRegistry} from "../contracts/vaults/SubVaultsRegistry.sol"; +import {SubVaultsRegistryFactory} from "../contracts/vaults/SubVaultsRegistryFactory.sol"; import {GnoMetaVault} from "../contracts/vaults/gnosis/GnoMetaVault.sol"; import {GnoMetaVaultFactory} from "../contracts/vaults/gnosis/GnoMetaVaultFactory.sol"; -import {GnoPrivMetaVault} from "../contracts/vaults/gnosis/GnoPrivMetaVault.sol"; import {Network} from "./Network.sol"; contract UpgradeGnoNetwork is Network { @@ -21,6 +22,7 @@ contract UpgradeGnoNetwork is Network { address public validatorsChecker; address public osTokenRedeemer; + address public subVaultsRegistryFactory; address[] public vaultImpls; Factory[] public vaultFactories; @@ -62,28 +64,37 @@ contract UpgradeGnoNetwork is Network { ) ); + // Deploy SubVaultsRegistryFactory + address subVaultsRegistryImpl = address( + new SubVaultsRegistry( + deployment.curatorsRegistry, + deployment.vaultsRegistry, + deployment.keeper, + deployment.osTokenVaultController, + deployment.osTokenConfig + ) + ); + subVaultsRegistryFactory = + address(new SubVaultsRegistryFactory(subVaultsRegistryImpl, IVaultsRegistry(deployment.vaultsRegistry))); + _deployImplementations(); _deployFactories(); vm.stopBroadcast(); generateGovernorTxJson(vaultImpls, vaultFactories, osTokenRedeemer); generateUpgradesJson(vaultImpls); - generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer); + generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer, subVaultsRegistryFactory); } function _deployImplementations() internal { // constructors for implementations - IMetaVault.MetaVaultConstructorArgs memory metaVaultArgs = _getMetaVaultConstructorArgs(); + IGnoMetaVault.GnoMetaVaultConstructorArgs memory metaVaultArgs = _getGnoMetaVaultConstructorArgs(); // deploy meta vaults metaVaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; GnoMetaVault gnoMetaVault = new GnoMetaVault(gnoToken, metaVaultArgs); - metaVaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; - GnoPrivMetaVault gnoPrivMetaVault = new GnoPrivMetaVault(gnoToken, metaVaultArgs); - vaultImpls.push(address(gnoMetaVault)); - vaultImpls.push(address(gnoPrivMetaVault)); } function _deployFactories() internal { @@ -96,21 +107,19 @@ contract UpgradeGnoNetwork is Network { address(new GnoMetaVaultFactory(vaultImpl, IVaultsRegistry(deployment.vaultsRegistry), gnoToken)); if (vaultId == keccak256("GnoMetaVault")) { vaultFactories.push(Factory({name: "MetaVaultFactory", factory: factory})); - } else if (vaultId == keccak256("GnoPrivMetaVault")) { - vaultFactories.push(Factory({name: "PrivMetaVaultFactory", factory: factory})); } } } - function _getMetaVaultConstructorArgs() internal returns (IMetaVault.MetaVaultConstructorArgs memory) { + function _getGnoMetaVaultConstructorArgs() internal returns (IGnoMetaVault.GnoMetaVaultConstructorArgs memory) { Deployment memory deployment = getDeploymentData(); - return IMetaVault.MetaVaultConstructorArgs({ + return IGnoMetaVault.GnoMetaVaultConstructorArgs({ keeper: deployment.keeper, vaultsRegistry: deployment.vaultsRegistry, osTokenVaultController: deployment.osTokenVaultController, osTokenConfig: deployment.osTokenConfig, osTokenVaultEscrow: deployment.osTokenVaultEscrow, - curatorsRegistry: deployment.curatorsRegistry, + subVaultsRegistryFactory: subVaultsRegistryFactory, exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY }); } diff --git a/snapshots/DepositDataRegistryTest.json b/snapshots/DepositDataRegistryTest.json index 4c42af58..ff9443b8 100644 --- a/snapshots/DepositDataRegistryTest.json +++ b/snapshots/DepositDataRegistryTest.json @@ -1,9 +1,9 @@ { - "DepositDataRegistryTest_test_registerValidator_succeedsWith0x01Validator": "273443", - "DepositDataRegistryTest_test_registerValidator_succeedsWith0x02Validator": "296337", - "DepositDataRegistryTest_test_registerValidators_successWith0x01Validators": "314933", - "DepositDataRegistryTest_test_registerValidators_successWith0x02Validators": "339002", - "DepositDataRegistryTest_test_setDepositDataManager_succeeds": "68682", - "DepositDataRegistryTest_test_setDepositDataRoot_succeeds": "65115", - "DepositDataRegistryTest_test_updateVaultState_succeeds": "128278" + "DepositDataRegistryTest_test_registerValidator_succeedsWith0x01Validator": "273455", + "DepositDataRegistryTest_test_registerValidator_succeedsWith0x02Validator": "296361", + "DepositDataRegistryTest_test_registerValidators_successWith0x01Validators": "314945", + "DepositDataRegistryTest_test_registerValidators_successWith0x02Validators": "339018", + "DepositDataRegistryTest_test_setDepositDataManager_succeeds": "68694", + "DepositDataRegistryTest_test_setDepositDataRoot_succeeds": "65127", + "DepositDataRegistryTest_test_updateVaultState_succeeds": "128302" } \ No newline at end of file diff --git a/snapshots/EthBlocklistErc20VaultTest.json b/snapshots/EthBlocklistErc20VaultTest.json index 940d521b..c001f261 100644 --- a/snapshots/EthBlocklistErc20VaultTest.json +++ b/snapshots/EthBlocklistErc20VaultTest.json @@ -1,8 +1,8 @@ { - "EthBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "89912", + "EthBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "89972", "EthBlocklistErc20VaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "84302", "EthBlocklistErc20VaultTest_test_canMintOsTokenAsNonBlockedUser": "161501", - "EthBlocklistErc20VaultTest_test_deploysCorrectly": "625440", + "EthBlocklistErc20VaultTest_test_deploysCorrectly": "625605", "EthBlocklistErc20VaultTest_test_transfer": "61693", - "EthBlocklistErc20VaultTest_test_upgradesCorrectly": "119538" + "EthBlocklistErc20VaultTest_test_upgradesCorrectly": "119646" } \ No newline at end of file diff --git a/snapshots/EthBlocklistVaultTest.json b/snapshots/EthBlocklistVaultTest.json index 7c46536f..cc0b1508 100644 --- a/snapshots/EthBlocklistVaultTest.json +++ b/snapshots/EthBlocklistVaultTest.json @@ -2,6 +2,6 @@ "EthBlocklistVaultTest_test_canDepositAsNonBlockedUser": "87959", "EthBlocklistVaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "82360", "EthBlocklistVaultTest_test_canMintOsTokenAsNonBlockedUser": "161528", - "EthBlocklistVaultTest_test_deploysCorrectly": "550241", - "EthBlocklistVaultTest_test_upgradesCorrectly": "118869" + "EthBlocklistVaultTest_test_deploysCorrectly": "550406", + "EthBlocklistVaultTest_test_upgradesCorrectly": "118977" } \ No newline at end of file diff --git a/snapshots/EthErc20MetaVaultTest.json b/snapshots/EthErc20MetaVaultTest.json new file mode 100644 index 00000000..41692c1e --- /dev/null +++ b/snapshots/EthErc20MetaVaultTest.json @@ -0,0 +1,11 @@ +{ + "EthErc20MetaVaultTest_test_deposit": "93642", + "EthErc20MetaVaultTest_test_depositAndMintOsToken": "194438", + "EthErc20MetaVaultTest_test_depositViaFallback": "95642", + "EthErc20MetaVaultTest_test_enterExitQueue": "106106", + "EthErc20MetaVaultTest_test_enterExitQueue_nonCollateralized": "106106", + "EthErc20MetaVaultTest_test_transfer": "64426", + "EthErc20MetaVaultTest_test_transferFrom": "61161", + "EthErc20MetaVaultTest_test_updateStateAndDeposit": "212459", + "EthErc20MetaVaultTest_test_updateStateAndDepositAndMintOsToken": "218872" +} \ No newline at end of file diff --git a/snapshots/EthErc20VaultTest.json b/snapshots/EthErc20VaultTest.json index d27828c7..64b5af28 100644 --- a/snapshots/EthErc20VaultTest.json +++ b/snapshots/EthErc20VaultTest.json @@ -1,15 +1,15 @@ { "EthErc20VaultTest_test_canTransferFromSharesWithHighLtv": "90077", "EthErc20VaultTest_test_cannotTransferFromSharesWithLowLtv": "96608", - "EthErc20VaultTest_test_deploysCorrectly": "455018", + "EthErc20VaultTest_test_deploysCorrectly": "455183", "EthErc20VaultTest_test_depositAndMintOsToken": "203747", "EthErc20VaultTest_test_depositViaReceiveFallback_emitsTransfer": "77951", "EthErc20VaultTest_test_deposit_emitsTransfer": "81413", - "EthErc20VaultTest_test_enterExitQueue_emitsTransfer": "89780", - "EthErc20VaultTest_test_redeem_emitsEvent": "58285", - "EthErc20VaultTest_test_updateExitQueue_emitsTransfer": "177065", - "EthErc20VaultTest_test_updateStateAndDepositAndMintOsToken": "230047", - "EthErc20VaultTest_test_upgradesCorrectly": "119424", + "EthErc20VaultTest_test_enterExitQueue_emitsTransfer": "89801", + "EthErc20VaultTest_test_redeem_emitsEvent": "56464", + "EthErc20VaultTest_test_updateExitQueue_emitsTransfer": "177077", + "EthErc20VaultTest_test_updateStateAndDepositAndMintOsToken": "230035", + "EthErc20VaultTest_test_upgradesCorrectly": "119532", "EthErc20VaultTest_test_withdrawValidator_unknown": "55951", "EthErc20VaultTest_test_withdrawValidator_validatorsManager": "74092" } \ No newline at end of file diff --git a/snapshots/EthGenesisVaultTest.json b/snapshots/EthGenesisVaultTest.json index a8226ddb..0acc7ce3 100644 --- a/snapshots/EthGenesisVaultTest.json +++ b/snapshots/EthGenesisVaultTest.json @@ -1,7 +1,9 @@ { "EthGenesisVaultTest_test_claimsPoolEscrowAssets": "78175", + "EthGenesisVaultTest_test_deposit": "81489", "EthGenesisVaultTest_test_fallback_acceptsEtherFromPoolEscrow": "35807", "EthGenesisVaultTest_test_fallback_acceptsEtherFromUser": "79346", "EthGenesisVaultTest_test_migrate_works": "204956", + "EthGenesisVaultTest_test_transferVaultAssets_pullsFromEscrow": "74422", "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "819842" } \ No newline at end of file diff --git a/snapshots/EthMetaVaultTest.json b/snapshots/EthMetaVaultTest.json index 520a8996..29de17ae 100644 --- a/snapshots/EthMetaVaultTest.json +++ b/snapshots/EthMetaVaultTest.json @@ -1,17 +1,17 @@ { - "EthMetaVaultTest_test_calculateSubVaultsRedemptions_exactWithdrawableAssets": "47214", - "EthMetaVaultTest_test_calculateSubVaultsRedemptions_insufficientWithdrawableAssets": "96978", - "EthMetaVaultTest_test_calculateSubVaultsRedemptions_success": "97007", - "EthMetaVaultTest_test_deposit": "85504", - "EthMetaVaultTest_test_depositAndMintOsToken": "179869", - "EthMetaVaultTest_test_depositViaFallback": "82122", - "EthMetaVaultTest_test_isStateUpdateRequired_true": "136011", - "EthMetaVaultTest_test_redeemSubVaultsAssets_noRedeemRequests": "69102", - "EthMetaVaultTest_test_redeemSubVaultsAssets_noRoundingErrors": "549703", - "EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsExceedSubVaultsWithdrawableAssets": "548661", - "EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsLessThanSubVaultsWithdrawableAssets": "549679", - "EthMetaVaultTest_test_redeemSubVaultsAssets_success": "549679", - "EthMetaVaultTest_test_updateStateAndDeposit": "197478", - "EthMetaVaultTest_test_updateStateAndDepositAndMintOsToken": "200840", - "EthMetaVaultTest_test_userClaimExitedAssets": "54213" + "EthMetaVaultTest_test_calculateSubVaultsRedemptions_exactWithdrawableAssets": "38857", + "EthMetaVaultTest_test_calculateSubVaultsRedemptions_insufficientWithdrawableAssets": "102615", + "EthMetaVaultTest_test_calculateSubVaultsRedemptions_success": "102644", + "EthMetaVaultTest_test_deposit": "91733", + "EthMetaVaultTest_test_depositAndMintOsToken": "192462", + "EthMetaVaultTest_test_depositViaFallback": "93711", + "EthMetaVaultTest_test_isStateUpdateRequired_true": "135911", + "EthMetaVaultTest_test_redeemSubVaultsAssets_noRedeemRequests": "64845", + "EthMetaVaultTest_test_redeemSubVaultsAssets_noRoundingErrors": "560328", + "EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsExceedSubVaultsWithdrawableAssets": "559286", + "EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsLessThanSubVaultsWithdrawableAssets": "560304", + "EthMetaVaultTest_test_redeemSubVaultsAssets_success": "560304", + "EthMetaVaultTest_test_updateStateAndDeposit": "210461", + "EthMetaVaultTest_test_updateStateAndDepositAndMintOsToken": "216899", + "EthMetaVaultTest_test_userClaimExitedAssets": "54232" } \ No newline at end of file diff --git a/snapshots/EthOsTokenRedeemerTest.json b/snapshots/EthOsTokenRedeemerTest.json index 03035fdf..846b99d2 100644 --- a/snapshots/EthOsTokenRedeemerTest.json +++ b/snapshots/EthOsTokenRedeemerTest.json @@ -3,14 +3,15 @@ "EthOsTokenRedeemerTest_test_permitOsToken_success": "82575", "EthOsTokenRedeemerTest_test_processExitQueue_nothingToProcess": "33998", "EthOsTokenRedeemerTest_test_processExitQueue_success": "101715", - "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_multiplePositions": "297443", - "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "201436", + "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_multiplePositions": "297412", + "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "201405", "EthOsTokenRedeemerTest_test_setPositionsManager": "35834", - "EthOsTokenRedeemerTest_test_setRedeemablePositions_success": "100561", + "EthOsTokenRedeemerTest_test_setRedeemablePositions_success": "100649", "EthOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "101264", + "EthOsTokenRedeemerTest_test_updateVaultState_success": "129442", "test_claimExitedAssets_fullWithdrawal": "53564", "test_claimExitedAssets_noPosition": "30222", "test_claimExitedAssets_partialWithdrawal": "75901", - "test_redeemSubVaultsAssets_concurrentMultipleSubVaults": "675026", - "test_redeemSubVaultsAssets_largeAmountAcrossAllSubVaults": "816491" + "test_redeemSubVaultsAssets_concurrentMultipleSubVaults": "683579", + "test_redeemSubVaultsAssets_largeAmountAcrossAllSubVaults": "823140" } \ No newline at end of file diff --git a/snapshots/EthPrivErc20MetaVaultTest.json b/snapshots/EthPrivErc20MetaVaultTest.json new file mode 100644 index 00000000..71e7dc49 --- /dev/null +++ b/snapshots/EthPrivErc20MetaVaultTest.json @@ -0,0 +1,9 @@ +{ + "EthPrivErc20MetaVaultTest_test_depositViaFallback_whitelistedUser": "97878", + "EthPrivErc20MetaVaultTest_test_deposit_whitelistedUser": "98144", + "EthPrivErc20MetaVaultTest_test_mintOsToken_whitelistedUser": "172589", + "EthPrivErc20MetaVaultTest_test_setWhitelister": "41109", + "EthPrivErc20MetaVaultTest_test_transferFrom_bothWhitelisted": "65611", + "EthPrivErc20MetaVaultTest_test_transfer_bothWhitelisted": "64486", + "EthPrivErc20MetaVaultTest_test_updateWhitelist": "58956" +} \ No newline at end of file diff --git a/snapshots/EthPrivErc20VaultTest.json b/snapshots/EthPrivErc20VaultTest.json index 74df5a12..42768958 100644 --- a/snapshots/EthPrivErc20VaultTest.json +++ b/snapshots/EthPrivErc20VaultTest.json @@ -3,7 +3,7 @@ "EthPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "81395", "EthPrivErc20VaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "77693", "EthPrivErc20VaultTest_test_canMintOsTokenAsWhitelistedUser": "161601", - "EthPrivErc20VaultTest_test_deploysCorrectly": "625462", + "EthPrivErc20VaultTest_test_deploysCorrectly": "625627", "EthPrivErc20VaultTest_test_transfer": "59709", - "EthPrivErc20VaultTest_test_upgradesCorrectly": "119559" + "EthPrivErc20VaultTest_test_upgradesCorrectly": "119667" } \ No newline at end of file diff --git a/snapshots/EthPrivMetaVaultTest.json b/snapshots/EthPrivMetaVaultTest.json index dd76de74..76174150 100644 --- a/snapshots/EthPrivMetaVaultTest.json +++ b/snapshots/EthPrivMetaVaultTest.json @@ -1,11 +1,11 @@ { - "EthPrivMetaVaultTest_test_canDepositAndMintOsTokenAsWhitelistedUser": "182769", - "EthPrivMetaVaultTest_test_canDepositAsWhitelistedUser": "83505", - "EthPrivMetaVaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "79859", - "EthPrivMetaVaultTest_test_canMintOsTokenAsWhitelistedUser": "161264", - "EthPrivMetaVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "112889", - "EthPrivMetaVaultTest_test_depositToSubVaultsWorksWithWhitelistedUser": "295390", - "EthPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval": "84654", - "EthPrivMetaVaultTest_test_setWhitelister": "36586", - "EthPrivMetaVaultTest_test_updateWhitelist": "54499" + "EthPrivMetaVaultTest_test_canDepositAndMintOsTokenAsWhitelistedUser": "195340", + "EthPrivMetaVaultTest_test_canDepositAsWhitelistedUser": "91702", + "EthPrivMetaVaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "91436", + "EthPrivMetaVaultTest_test_canMintOsTokenAsWhitelistedUser": "172660", + "EthPrivMetaVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "126550", + "EthPrivMetaVaultTest_test_depositToSubVaultsWorksWithWhitelistedUser": "331231", + "EthPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval": "94916", + "EthPrivMetaVaultTest_test_setWhitelister": "36608", + "EthPrivMetaVaultTest_test_updateWhitelist": "54543" } \ No newline at end of file diff --git a/snapshots/EthPrivVaultTest.json b/snapshots/EthPrivVaultTest.json index e860566b..c1b0b717 100644 --- a/snapshots/EthPrivVaultTest.json +++ b/snapshots/EthPrivVaultTest.json @@ -1,11 +1,11 @@ { - "EthPrivVaultTest_test_canDepositAsWhitelistedUser": "79431", + "EthPrivVaultTest_test_canDepositAsWhitelistedUser": "79491", "EthPrivVaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "75857", "EthPrivVaultTest_test_canMintOsTokenAsWhitelistedUser": "161628", - "EthPrivVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "140687", - "EthPrivVaultTest_test_deploysCorrectly": "550259", + "EthPrivVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "140675", + "EthPrivVaultTest_test_deploysCorrectly": "550424", "EthPrivVaultTest_test_depositAndMintOsTokenAsWhitelistedUser": "200570", "EthPrivVaultTest_test_setWhitelister": "36608", "EthPrivVaultTest_test_updateWhitelist": "54543", - "EthPrivVaultTest_test_upgradesCorrectly": "118985" + "EthPrivVaultTest_test_upgradesCorrectly": "119093" } \ No newline at end of file diff --git a/snapshots/EthRewardSplitterTest.json b/snapshots/EthRewardSplitterTest.json index 6d3d62db..2f9e5fab 100644 --- a/snapshots/EthRewardSplitterTest.json +++ b/snapshots/EthRewardSplitterTest.json @@ -1,14 +1,14 @@ { - "EthRewardSplitter_claimExitedAssetsOnBehalf": "767084", - "EthRewardSplitter_claimVaultTokens": "771719", + "EthRewardSplitter_claimExitedAssetsOnBehalf": "767108", + "EthRewardSplitter_claimVaultTokens": "771743", "EthRewardSplitter_decreaseShares": "90133", - "EthRewardSplitter_enterExitQueue": "171850", - "EthRewardSplitter_enterExitQueueMaxWithdrawal": "131378", - "EthRewardSplitter_enterExitQueueOnBehalf": "964738", + "EthRewardSplitter_enterExitQueue": "171871", + "EthRewardSplitter_enterExitQueueMaxWithdrawal": "131399", + "EthRewardSplitter_enterExitQueueOnBehalf": "964783", "EthRewardSplitter_increaseShares": "73128", "EthRewardSplitter_receiveEth": "31194", "EthRewardSplitter_syncRewards": "77682", "EthRewardSplitter_syncRewardsDetailed": "77682", "EthRewardSplitter_test_setClaimer": "66269", - "EthRewardSplitter_updateVaultState": "132613" + "EthRewardSplitter_updateVaultState": "132625" } \ No newline at end of file diff --git a/snapshots/EthVaultTest.json b/snapshots/EthVaultTest.json index 395ae7c5..60c3c731 100644 --- a/snapshots/EthVaultTest.json +++ b/snapshots/EthVaultTest.json @@ -1,10 +1,10 @@ { - "EthVaultTest_test_deploysCorrectly": "379895", + "EthVaultTest_test_deploysCorrectly": "380060", "EthVaultTest_test_depositAndMintOsToken": "202060", "EthVaultTest_test_exitQueue_works": "99131", "EthVaultTest_test_fallbackDeposit": "77627", - "EthVaultTest_test_updateStateAndDepositAndMintOsToken": "228182", - "EthVaultTest_test_upgradesCorrectly": "118828", + "EthVaultTest_test_updateStateAndDepositAndMintOsToken": "228170", + "EthVaultTest_test_upgradesCorrectly": "118936", "EthVaultTest_test_withdrawValidator_unknown": "55973", "EthVaultTest_test_withdrawValidator_validatorsManager": "74114" } \ No newline at end of file diff --git a/snapshots/GnoBlocklistErc20VaultTest.json b/snapshots/GnoBlocklistErc20VaultTest.json index 2f80b1dd..0788f0c7 100644 --- a/snapshots/GnoBlocklistErc20VaultTest.json +++ b/snapshots/GnoBlocklistErc20VaultTest.json @@ -1,7 +1,7 @@ { - "GnoBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "160108", + "GnoBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "160076", "GnoBlocklistErc20VaultTest_test_canMintOsTokenAsNonBlockedUser": "161610", - "GnoBlocklistErc20VaultTest_test_deploysCorrectly": "953762", + "GnoBlocklistErc20VaultTest_test_deploysCorrectly": "953673", "GnoBlocklistErc20VaultTest_test_transfer": "61693", - "GnoBlocklistErc20VaultTest_test_upgradesCorrectly": "339872" + "GnoBlocklistErc20VaultTest_test_upgradesCorrectly": "339804" } \ No newline at end of file diff --git a/snapshots/GnoBlocklistVaultTest.json b/snapshots/GnoBlocklistVaultTest.json index 27856b9d..f65dab28 100644 --- a/snapshots/GnoBlocklistVaultTest.json +++ b/snapshots/GnoBlocklistVaultTest.json @@ -1,6 +1,6 @@ { - "GnoBlocklistVaultTest_test_canDepositAsNonBlockedUser": "158124", + "GnoBlocklistVaultTest_test_canDepositAsNonBlockedUser": "158092", "GnoBlocklistVaultTest_test_canMintOsTokenAsNonBlockedUser": "161570", - "GnoBlocklistVaultTest_test_deploysCorrectly": "702387", - "GnoBlocklistVaultTest_test_upgradesCorrectly": "339168" + "GnoBlocklistVaultTest_test_deploysCorrectly": "702298", + "GnoBlocklistVaultTest_test_upgradesCorrectly": "339099" } \ No newline at end of file diff --git a/snapshots/GnoErc20VaultTest.json b/snapshots/GnoErc20VaultTest.json index 7581c144..3aeff5ff 100644 --- a/snapshots/GnoErc20VaultTest.json +++ b/snapshots/GnoErc20VaultTest.json @@ -1,11 +1,11 @@ { "GnoErc20VaultTest_test_canTransferFromSharesWithHighLtv": "90100", "GnoErc20VaultTest_test_cannotTransferFromSharesWithLowLtv": "96637", - "GnoErc20VaultTest_test_deploysCorrectly": "753330", + "GnoErc20VaultTest_test_deploysCorrectly": "753241", "GnoErc20VaultTest_test_deposit_emitsTransfer": "100732", "GnoErc20VaultTest_test_enterExitQueue_emitsTransfer": "89780", "GnoErc20VaultTest_test_redeem_emitsEvent": "77069", - "GnoErc20VaultTest_test_upgradesCorrectly": "339858", + "GnoErc20VaultTest_test_upgradesCorrectly": "339790", "VaultGnoErc20VaultTest_test_withdrawValidator_unknown": "55973", "VaultGnoErc20VaultTest_test_withdrawValidator_validatorsManager": "74114" } \ No newline at end of file diff --git a/snapshots/GnoGenesisVaultTest.json b/snapshots/GnoGenesisVaultTest.json index 3ac51c48..21ceb90b 100644 --- a/snapshots/GnoGenesisVaultTest.json +++ b/snapshots/GnoGenesisVaultTest.json @@ -1,5 +1,4 @@ { - "GnoGenesisVaultTest_test_migrate_works": "206716", - "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "879736", - "GnoGenesisVaultTest_test_upgradesCorrectly": "5830554" + "GnoGenesisVaultTest_test_migrate_works": "207235", + "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "883606" } \ No newline at end of file diff --git a/snapshots/GnoMetaVaultTest.json b/snapshots/GnoMetaVaultTest.json index ffe00ffa..a71f1ec2 100644 --- a/snapshots/GnoMetaVaultTest.json +++ b/snapshots/GnoMetaVaultTest.json @@ -1,9 +1,9 @@ { - "GnoMetaVaultTest_test_addSubVault": "161623", - "GnoMetaVaultTest_test_claimExitedAssets": "67835", - "GnoMetaVaultTest_test_deposit": "103300", - "GnoMetaVaultTest_test_depositToSubVaults": "329543", - "GnoMetaVaultTest_test_ejectSubVault": "218113", - "GnoMetaVaultTest_test_enterExitQueue": "89168", - "GnoMetaVaultTest_test_updateState": "158452" + "GnoMetaVaultTest_test_addSubVault": "132774", + "GnoMetaVaultTest_test_claimExitedAssets": "67905", + "GnoMetaVaultTest_test_deposit": "109532", + "GnoMetaVaultTest_test_depositToSubVaults": "376819", + "GnoMetaVaultTest_test_ejectSubVault": "216850", + "GnoMetaVaultTest_test_enterExitQueue": "99334", + "GnoMetaVaultTest_test_updateState": "171949" } \ No newline at end of file diff --git a/snapshots/GnoOsTokenRedeemerTest.json b/snapshots/GnoOsTokenRedeemerTest.json index 1bb452bb..6dd9af61 100644 --- a/snapshots/GnoOsTokenRedeemerTest.json +++ b/snapshots/GnoOsTokenRedeemerTest.json @@ -1,6 +1,6 @@ { - "GnoOsTokenRedeemerTest_test_claimExitedAssets_fullWithdrawal": "80495", - "GnoOsTokenRedeemerTest_test_permitGnoToken_success": "91926", - "GnoOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "252803", - "GnoOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "137991" + "GnoOsTokenRedeemerTest_test_claimExitedAssets_fullWithdrawal": "80517", + "GnoOsTokenRedeemerTest_test_permitGnoToken_success": "91931", + "GnoOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "252760", + "GnoOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "138013" } \ No newline at end of file diff --git a/snapshots/GnoOsTokenVaultEscrowTest.json b/snapshots/GnoOsTokenVaultEscrowTest.json index 7e35444f..717cbdf9 100644 --- a/snapshots/GnoOsTokenVaultEscrowTest.json +++ b/snapshots/GnoOsTokenVaultEscrowTest.json @@ -1,5 +1,5 @@ { - "GnoOsTokenVaultEscrowTest_test_transferAssets_claim": "140340", - "GnoOsTokenVaultEscrowTest_test_transferAssets_process": "893932", + "GnoOsTokenVaultEscrowTest_test_transferAssets_claim": "140328", + "GnoOsTokenVaultEscrowTest_test_transferAssets_process": "893896", "GnoOsTokenVaultEscrowTest_test_transferAssets_transfer": "163157" } \ No newline at end of file diff --git a/snapshots/GnoPrivErc20VaultTest.json b/snapshots/GnoPrivErc20VaultTest.json index 2a1d7fa1..fbe91dbd 100644 --- a/snapshots/GnoPrivErc20VaultTest.json +++ b/snapshots/GnoPrivErc20VaultTest.json @@ -1,7 +1,7 @@ { - "GnoPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "156037", + "GnoPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "156005", "GnoPrivErc20VaultTest_test_canMintOsTokenAsWhitelistedUser": "161543", - "GnoPrivErc20VaultTest_test_deploysCorrectly": "953806", + "GnoPrivErc20VaultTest_test_deploysCorrectly": "953717", "GnoPrivErc20VaultTest_test_transfer": "59709", - "GnoPrivErc20VaultTest_test_upgradesCorrectly": "340008" + "GnoPrivErc20VaultTest_test_upgradesCorrectly": "339939" } \ No newline at end of file diff --git a/snapshots/GnoPrivMetaVaultTest.json b/snapshots/GnoPrivMetaVaultTest.json deleted file mode 100644 index 433487f0..00000000 --- a/snapshots/GnoPrivMetaVaultTest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "GnoPrivMetaVaultTest_test_canDepositAsWhitelistedUser": "103337", - "GnoPrivMetaVaultTest_test_canMintOsTokenAsWhitelistedUser": "161126", - "GnoPrivMetaVaultTest_test_depositToSubVaultsWorksWithWhitelistedUser": "329543", - "GnoPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval": "84646", - "GnoPrivMetaVaultTest_test_setWhitelister": "36586", - "GnoPrivMetaVaultTest_test_updateWhitelist": "54477" -} \ No newline at end of file diff --git a/snapshots/GnoPrivVaultTest.json b/snapshots/GnoPrivVaultTest.json index ff248b50..842e3313 100644 --- a/snapshots/GnoPrivVaultTest.json +++ b/snapshots/GnoPrivVaultTest.json @@ -1,8 +1,8 @@ { - "GnoPrivVaultTest_test_canDepositAsWhitelistedUser": "154140", + "GnoPrivVaultTest_test_canDepositAsWhitelistedUser": "154108", "GnoPrivVaultTest_test_canMintOsTokenAsWhitelistedUser": "161480", - "GnoPrivVaultTest_test_deploysCorrectly": "702431", + "GnoPrivVaultTest_test_deploysCorrectly": "702342", "GnoPrivVaultTest_test_setWhitelister": "36608", "GnoPrivVaultTest_test_updateWhitelist": "54521", - "GnoPrivVaultTest_test_upgradesCorrectly": "339304" + "GnoPrivVaultTest_test_upgradesCorrectly": "339236" } \ No newline at end of file diff --git a/snapshots/GnoRewardSplitterTest.json b/snapshots/GnoRewardSplitterTest.json index 28733f70..45c6272c 100644 --- a/snapshots/GnoRewardSplitterTest.json +++ b/snapshots/GnoRewardSplitterTest.json @@ -1,7 +1,7 @@ { "GnoRewardSplitter_claimExitedAssets": "94809", - "GnoRewardSplitter_claimExitedAssetsOnBehalf": "824394", - "GnoRewardSplitter_claimVaultTokens": "855626", + "GnoRewardSplitter_claimExitedAssetsOnBehalf": "824382", + "GnoRewardSplitter_claimVaultTokens": "855614", "GnoRewardSplitter_decreaseShares": "101041", "GnoRewardSplitter_enterExitQueue": "186729", "GnoRewardSplitter_enterExitQueueMaxWithdrawal": "131400", diff --git a/snapshots/GnoVaultExitQueueTest.json b/snapshots/GnoVaultExitQueueTest.json index d054d0a7..9d2a785f 100644 --- a/snapshots/GnoVaultExitQueueTest.json +++ b/snapshots/GnoVaultExitQueueTest.json @@ -1,7 +1,7 @@ { - "GnoVaultExitQueueTest_test_ExitingAssetsPenalized_event": "194845", - "GnoVaultExitQueueTest_test_claim_position1_after_upgrade": "6653963", - "GnoVaultExitQueueTest_test_claim_position2_before_upgrade": "125766", + "GnoVaultExitQueueTest_test_ExitingAssetsPenalized_event": "194833", + "GnoVaultExitQueueTest_test_claim_position1_after_upgrade": "6653936", + "GnoVaultExitQueueTest_test_claim_position2_before_upgrade": "125760", "GnoVaultExitQueueTest_test_claim_position3_after_upgrade": "165242", "GnoVaultExitQueueTest_test_claim_position4_after_upgrade": "160429" } \ No newline at end of file diff --git a/snapshots/GnoVaultTest.json b/snapshots/GnoVaultTest.json index 0ddb8366..2d140e4c 100644 --- a/snapshots/GnoVaultTest.json +++ b/snapshots/GnoVaultTest.json @@ -1,7 +1,7 @@ { - "GnoVaultTest_test_deploysCorrectly": "678345", + "GnoVaultTest_test_deploysCorrectly": "678256", "GnoVaultTest_test_exitQueue_works": "99131", - "GnoVaultTest_test_upgradesCorrectly": "339206", + "GnoVaultTest_test_upgradesCorrectly": "339137", "GnoVaultTest_test_withdrawValidator_unknown": "55950", "GnoVaultTest_test_withdrawValidator_validatorsManager": "74091" } \ No newline at end of file diff --git a/snapshots/KeeperRewardsTest.json b/snapshots/KeeperRewardsTest.json index 78e34a09..1b3fa950 100644 --- a/snapshots/KeeperRewardsTest.json +++ b/snapshots/KeeperRewardsTest.json @@ -1,14 +1,14 @@ { "KeeperRewardsTest_test_canHarvest": "12938", "KeeperRewardsTest_test_harvest": "130843", - "KeeperRewardsTest_test_harvestWithPenalties": "95048", + "KeeperRewardsTest_test_harvestWithPenalties": "95036", "KeeperRewardsTest_test_harvest_alreadyHarvested": "50472", "KeeperRewardsTest_test_harvest_invalidProof": "49041", "KeeperRewardsTest_test_harvest_invalidRewardsRoot": "49601", "KeeperRewardsTest_test_harvest_nonVault": "35559", "KeeperRewardsTest_test_isCollateralized": "10123", "KeeperRewardsTest_test_isHarvestRequired": "12620", - "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_1": "135313", + "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_1": "135325", "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_2": "606725", "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_3": "606771", "KeeperRewardsTest_test_setRewardsMinOracles": "37748", diff --git a/snapshots/KeeperValidatorsTest.json b/snapshots/KeeperValidatorsTest.json index 30f8465b..73ac4ec2 100644 --- a/snapshots/KeeperValidatorsTest.json +++ b/snapshots/KeeperValidatorsTest.json @@ -1,15 +1,15 @@ { "KeeperValidatorsTest_test_approveValidators_accessDenied": "139546", - "KeeperValidatorsTest_test_approveValidators_invalidDeadline": "31828", - "KeeperValidatorsTest_test_approveValidators_invalidRegistry": "134445", - "KeeperValidatorsTest_test_approveValidators_success": "179025", + "KeeperValidatorsTest_test_approveValidators_invalidDeadline": "31852", + "KeeperValidatorsTest_test_approveValidators_invalidRegistry": "134469", + "KeeperValidatorsTest_test_approveValidators_success": "179049", "KeeperValidatorsTest_test_setValidatorsMinOracles_success": "37599", "KeeperValidatorsTest_test_setValidatorsMinOracles_tooHigh": "29460", "KeeperValidatorsTest_test_setValidatorsMinOracles_unauthorized": "30245", "KeeperValidatorsTest_test_setValidatorsMinOracles_zero": "29882", - "KeeperValidatorsTest_test_updateExitSignatures_duplicateUpdate": "45623", - "KeeperValidatorsTest_test_updateExitSignatures_expiredDeadline": "36838", + "KeeperValidatorsTest_test_updateExitSignatures_duplicateUpdate": "45635", + "KeeperValidatorsTest_test_updateExitSignatures_expiredDeadline": "36850", "KeeperValidatorsTest_test_updateExitSignatures_invalidVault": "31986", - "KeeperValidatorsTest_test_updateExitSignatures_notCollateralized": "34301", - "KeeperValidatorsTest_test_updateExitSignatures_success": "70498" + "KeeperValidatorsTest_test_updateExitSignatures_notCollateralized": "34313", + "KeeperValidatorsTest_test_updateExitSignatures_success": "70510" } \ No newline at end of file diff --git a/snapshots/VaultAdminTest.json b/snapshots/VaultAdminTest.json index 27e434c9..c773373f 100644 --- a/snapshots/VaultAdminTest.json +++ b/snapshots/VaultAdminTest.json @@ -1,7 +1,7 @@ { "VaultAdminTest_test_checkAdmin_withOtherFunctions_admin": "48301", "VaultAdminTest_test_checkAdmin_withOtherFunctions_nonAdmin": "34853", - "VaultAdminTest_test_initialization": "378686", + "VaultAdminTest_test_initialization": "378851", "VaultAdminTest_test_setAdmin_byAdmin": "39247", "VaultAdminTest_test_setAdmin_byNonAdmin": "36921", "VaultAdminTest_test_setAdmin_toSameValue": "35135", diff --git a/snapshots/VaultEnterExitTest.json b/snapshots/VaultEnterExitTest.json index e2c63547..c9fe9453 100644 --- a/snapshots/VaultEnterExitTest.json +++ b/snapshots/VaultEnterExitTest.json @@ -1,6 +1,6 @@ { "VaultEnterExitTest_test_calculateExitedAssets_invalidPosition": "18600", - "VaultEnterExitTest_test_claimExitedAssets": "752879", + "VaultEnterExitTest_test_claimExitedAssets": "752843", "VaultEnterExitTest_test_claimExitedAssets_insufficientDelay": "43924", "VaultEnterExitTest_test_claimExitedAssets_invalidCheckpoint": "36507", "VaultEnterExitTest_test_deposit_exceedingCapacity": "47943", diff --git a/snapshots/VaultEthStakingTest.json b/snapshots/VaultEthStakingTest.json index cf1c5e76..6a8e939a 100644 --- a/snapshots/VaultEthStakingTest.json +++ b/snapshots/VaultEthStakingTest.json @@ -13,6 +13,6 @@ "VaultEthStakingTest_test_transferVaultAssets": "54251", "VaultEthStakingTest_test_updateStateAndDeposit": "166673", "VaultEthStakingTest_test_updateStateAndDepositAndMintOsToken": "254280", - "VaultEthStakingTest_test_validatorMinMaxEffectiveBalance": "199908", + "VaultEthStakingTest_test_validatorMinMaxEffectiveBalance": "199884", "VaultEthStakingTest_test_withdrawValidator_fullFlow": "74114" } \ No newline at end of file diff --git a/snapshots/VaultFeeTest.json b/snapshots/VaultFeeTest.json index 399199f7..a8466b29 100644 --- a/snapshots/VaultFeeTest.json +++ b/snapshots/VaultFeeTest.json @@ -1,5 +1,5 @@ { - "VaultFeeTest_test_feeCollection": "121919", + "VaultFeeTest_test_feeCollection": "121931", "VaultFeeTest_test_feePercent_changeAffectsFutureRewards": "87733", "VaultFeeTest_test_setFeePercent_aboveMaximum": "40380", "VaultFeeTest_test_setFeePercent_initialZeroToOne": "44812", diff --git a/snapshots/VaultGnoStakingTest.json b/snapshots/VaultGnoStakingTest.json index 5f5bc848..981ed3fc 100644 --- a/snapshots/VaultGnoStakingTest.json +++ b/snapshots/VaultGnoStakingTest.json @@ -1,14 +1,14 @@ { "VaultGnoStakingCoverageTest_test_processTotalAssetsDelta_smallXdaiBalance": "175401", - "VaultGnoStakingCoverageTest_test_registerValidator_topUp_invalid": "57128", - "VaultGnoStakingCoverageTest_test_registerValidator_topUp_valid": "182150", + "VaultGnoStakingCoverageTest_test_registerValidator_topUp_invalid": "57152", + "VaultGnoStakingCoverageTest_test_registerValidator_topUp_valid": "182162", "VaultGnoStakingTest_test_deposit": "101137", "VaultGnoStakingTest_test_processTotalAssetsDelta": "303158", "VaultGnoStakingTest_test_pullWithdrawals": "92921", "VaultGnoStakingTest_test_receive_xDai": "213139", "VaultGnoStakingTest_test_transferVaultAssets": "89952", - "VaultGnoStakingTest_test_vaultGnoStaking_init": "678313", + "VaultGnoStakingTest_test_vaultGnoStaking_init": "678256", "VaultGnoStakingTest_test_withdrawValidator_fullFlow": "74091", "test_registerValidators_succeeds_0x01": "286528", - "test_registerValidators_succeeds_0x02": "630112" + "test_registerValidators_succeeds_0x02": "630128" } \ No newline at end of file diff --git a/snapshots/VaultOsTokenTest.json b/snapshots/VaultOsTokenTest.json index 44d8888e..b8b8b584 100644 --- a/snapshots/VaultOsTokenTest.json +++ b/snapshots/VaultOsTokenTest.json @@ -37,12 +37,12 @@ "VaultOsTokenTest_test_redeemVsLiquidate": "134071", "VaultOsTokenTest_test_test_liquidateOsToken_notHarvested": "43548", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_basic": "167657", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_claim": "107765", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_claim": "107777", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_maxAmount": "167657", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_moreThanOwned": "44819", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_noPosition": "43502", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_notHarvested": "41342", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_partialTransfer": "173220", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_process": "736184", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_process": "736148", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_zeroShares": "47545" } \ No newline at end of file diff --git a/snapshots/VaultSubVaultsTest.json b/snapshots/VaultSubVaultsTest.json index 47f4c6bb..2d0f6c20 100644 --- a/snapshots/VaultSubVaultsTest.json +++ b/snapshots/VaultSubVaultsTest.json @@ -1,24 +1,24 @@ { - "VaultSubVaultsTest_test_acceptMetaSubVault_success": "128444", - "VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_proposesMetaVault": "92907", - "VaultSubVaultsTest_test_addSubVault_success": "130867", - "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_ejectionConsumesShares": "417792", - "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets": "120643", - "VaultSubVaultsTest_test_depositToSubVaults_maxVaults": "3874692", - "VaultSubVaultsTest_test_depositToSubVaults_multipleSubVaults": "295050", - "VaultSubVaultsTest_test_depositToSubVaults_singleSubVault": "142644", - "VaultSubVaultsTest_test_depositToSubVaults_withMetaVaultSubVault": "371161", - "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_emptySubVault": "57635", - "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_withShares": "206794", - "VaultSubVaultsTest_test_rejectMetaSubVault_byAdmin_success": "42237", - "VaultSubVaultsTest_test_rejectMetaSubVault_byOwner_success": "50033", - "VaultSubVaultsTest_test_setSubVaultsCurator_success": "51010", - "VaultSubVaultsTest_test_updateState_enterExitQueueMaxVaults": "7041487", - "VaultSubVaultsTest_test_updateState_withMetaVaultSubVault_success": "169251", - "test_addSubVault_firstSubVault": "145160", - "test_depositToSubVaults_ejectingSubVault": "153972", - "test_ejectSubVault_emptySubVault": "66011", - "test_ejectSubVault_subVaultWithShares": "205950", - "test_updateState_newTotalAssets": "178910", - "test_updateState_unprocessedSubVaultExit": "204479" + "VaultSubVaultsTest_test_acceptMetaSubVault_success": "137996", + "VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_proposesMetaVault": "105714", + "VaultSubVaultsTest_test_addSubVault_success": "136946", + "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_ejectionConsumesShares": "442007", + "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets": "135816", + "VaultSubVaultsTest_test_depositToSubVaults_maxVaults": "3985383", + "VaultSubVaultsTest_test_depositToSubVaults_multipleSubVaults": "330869", + "VaultSubVaultsTest_test_depositToSubVaults_singleSubVault": "175277", + "VaultSubVaultsTest_test_depositToSubVaults_withMetaVaultSubVault": "416303", + "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_emptySubVault": "63232", + "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_withShares": "226072", + "VaultSubVaultsTest_test_rejectMetaSubVault_byAdmin_success": "54123", + "VaultSubVaultsTest_test_rejectMetaSubVault_byOwner_success": "49587", + "VaultSubVaultsTest_test_setSubVaultsCurator_success": "60428", + "VaultSubVaultsTest_test_updateState_enterExitQueueMaxVaults": "7233735", + "VaultSubVaultsTest_test_updateState_withMetaVaultSubVault_success": "190504", + "test_addSubVault_firstSubVault": "151735", + "test_depositToSubVaults_ejectingSubVault": "171099", + "test_ejectSubVault_emptySubVault": "71608", + "test_ejectSubVault_subVaultWithShares": "219558", + "test_updateState_newTotalAssets": "195596", + "test_updateState_unprocessedSubVaultExit": "216338" } \ No newline at end of file diff --git a/snapshots/VaultTokenTest.json b/snapshots/VaultTokenTest.json index e738ddbc..88ce61be 100644 --- a/snapshots/VaultTokenTest.json +++ b/snapshots/VaultTokenTest.json @@ -2,12 +2,12 @@ "VaultTokenTest_test_approve": "58846", "VaultTokenTest_test_approveZeroAddress": "33703", "VaultTokenTest_test_depositEmitsTransferEvent": "83493", - "VaultTokenTest_test_enterExitQueueEmitsTransferEvent": "97066", + "VaultTokenTest_test_enterExitQueueEmitsTransferEvent": "97087", "VaultTokenTest_test_invalidTokenMetaNameTooLong": "478037", "VaultTokenTest_test_invalidTokenMetaSymbolTooLong": "311105", "VaultTokenTest_test_permit": "87202", - "VaultTokenTest_test_permitExpiredDeadline": "35305", - "VaultTokenTest_test_permitInvalidSigner": "63558", + "VaultTokenTest_test_permitExpiredDeadline": "35317", + "VaultTokenTest_test_permitInvalidSigner": "63570", "VaultTokenTest_test_permitZeroAddressSpender": "34863", "VaultTokenTest_test_transfer": "66441", "VaultTokenTest_test_transferFrom": "67701", @@ -18,5 +18,5 @@ "VaultTokenTest_test_transferToZeroAddress": "33734", "VaultTokenTest_test_transferWithOsTokenPosition": "96584", "VaultTokenTest_test_unlimitedAllowance": "69361", - "VaultTokenTest_test_updateExitQueueBurnsShares": "146991" + "VaultTokenTest_test_updateExitQueueBurnsShares": "146979" } \ No newline at end of file diff --git a/snapshots/VaultValidatorsTest.json b/snapshots/VaultValidatorsTest.json index 3db88da5..cb8f6fc4 100644 --- a/snapshots/VaultValidatorsTest.json +++ b/snapshots/VaultValidatorsTest.json @@ -4,33 +4,33 @@ "VaultValidatorsTest_test_consolidateValidators_invalidSignature": "72120", "VaultValidatorsTest_test_consolidateValidators_invalidValidatorsEmpty": "55608", "VaultValidatorsTest_test_consolidateValidators_invalidValidatorsLength": "57330", - "VaultValidatorsTest_test_consolidateValidators_multipleValidators": "99394", + "VaultValidatorsTest_test_consolidateValidators_multipleValidators": "99370", "VaultValidatorsTest_test_consolidateValidators_notManager": "57250", "VaultValidatorsTest_test_consolidateValidators_untrackedDestination": "78708", "VaultValidatorsTest_test_consolidateValidators_withOracleSignatures": "115804", "VaultValidatorsTest_test_consolidateValidators_withSignature": "111705", "VaultValidatorsTest_test_fundValidators_byManager": "105234", "VaultValidatorsTest_test_fundValidators_insufficientAssets": "103060", - "VaultValidatorsTest_test_fundValidators_invalidSignature": "63689", + "VaultValidatorsTest_test_fundValidators_invalidSignature": "63701", "VaultValidatorsTest_test_fundValidators_invalidValidators": "45735", - "VaultValidatorsTest_test_fundValidators_multipleValidators": "144799", + "VaultValidatorsTest_test_fundValidators_multipleValidators": "144775", "VaultValidatorsTest_test_fundValidators_nonExistingValidator": "57140", "VaultValidatorsTest_test_fundValidators_notHarvested": "44394", "VaultValidatorsTest_test_fundValidators_notManager": "48806", - "VaultValidatorsTest_test_fundValidators_v1Validators": "52712", + "VaultValidatorsTest_test_fundValidators_v1Validators": "52700", "VaultValidatorsTest_test_fundValidators_withSignature": "137699", - "VaultValidatorsTest_test_registerValidators_byManager": "267691", - "VaultValidatorsTest_test_registerValidators_insufficientAssets": "41930", - "VaultValidatorsTest_test_registerValidators_invalidSignature": "206436", + "VaultValidatorsTest_test_registerValidators_byManager": "267715", + "VaultValidatorsTest_test_registerValidators_insufficientAssets": "41954", + "VaultValidatorsTest_test_registerValidators_invalidSignature": "206460", "VaultValidatorsTest_test_registerValidators_invalidValidatorLength": "194366", - "VaultValidatorsTest_test_registerValidators_invalidValidators": "188419", + "VaultValidatorsTest_test_registerValidators_invalidValidators": "188431", "VaultValidatorsTest_test_registerValidators_multipleValidators": "304623", - "VaultValidatorsTest_test_registerValidators_nonceIncrement": "148369", + "VaultValidatorsTest_test_registerValidators_nonceIncrement": "148393", "VaultValidatorsTest_test_registerValidators_notHarvested": "170713", - "VaultValidatorsTest_test_registerValidators_notManager": "193585", + "VaultValidatorsTest_test_registerValidators_notManager": "193609", "VaultValidatorsTest_test_registerValidators_v1Validators": "244804", - "VaultValidatorsTest_test_registerValidators_v2Validators": "267691", - "VaultValidatorsTest_test_registerValidators_withSignature": "300155", + "VaultValidatorsTest_test_registerValidators_v2Validators": "267715", + "VaultValidatorsTest_test_registerValidators_withSignature": "300179", "VaultValidatorsTest_test_setValidatorsManager_valueNotChanged": "36929", "VaultValidatorsTest_test_withdrawValidators_byManager": "74102", "VaultValidatorsTest_test_withdrawValidators_feeHandling": "81103", diff --git a/snapshots/VaultVersionTest.json b/snapshots/VaultVersionTest.json index b0ac9537..e5efabfe 100644 --- a/snapshots/VaultVersionTest.json +++ b/snapshots/VaultVersionTest.json @@ -1,12 +1,12 @@ { "VaultVersionTest_test_reinitializeFails": "30923", "VaultVersionTest_test_upgradeMultipleSteps": "61033", - "VaultVersionTest_test_upgradeNonAdminFails": "38872", - "VaultVersionTest_test_upgradeToDifferentVaultIdFails": "42352", - "VaultVersionTest_test_upgradeToNextVersion": "83681", + "VaultVersionTest_test_upgradeNonAdminFails": "38860", + "VaultVersionTest_test_upgradeToDifferentVaultIdFails": "42364", + "VaultVersionTest_test_upgradeToNextVersion": "83669", "VaultVersionTest_test_upgradeToSameVersionFails": "37105", "VaultVersionTest_test_upgradeToSkipVersionFails": "43074", "VaultVersionTest_test_upgradeToUnapprovedImplementationFails": "46371", "VaultVersionTest_test_upgradeToZeroAddressFails": "36640", - "VaultVersionTest_test_upgradeWithInvalidCallDataFails": "60730" + "VaultVersionTest_test_upgradeWithInvalidCallDataFails": "60718" } \ No newline at end of file diff --git a/test/EthErc20MetaVault.t.sol b/test/EthErc20MetaVault.t.sol new file mode 100644 index 00000000..c2be7f86 --- /dev/null +++ b/test/EthErc20MetaVault.t.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IEthErc20MetaVault} from "../contracts/interfaces/IEthErc20MetaVault.sol"; +import {IVaultState} from "../contracts/interfaces/IVaultState.sol"; +import {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; +import {IVaultOsToken} from "../contracts/interfaces/IVaultOsToken.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; +import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; +import {Errors} from "../contracts/libraries/Errors.sol"; +import {EthErc20MetaVault} from "../contracts/vaults/ethereum/EthErc20MetaVault.sol"; +import {EthHelpers} from "./helpers/EthHelpers.sol"; + +contract EthErc20MetaVaultTest is Test, EthHelpers { + ForkContracts public contracts; + EthErc20MetaVault public metaVault; + ISubVaultsRegistry public registry; + + address public admin; + address public sender; + address public receiver; + address public referrer; + + // Sub vaults + address[] public subVaults; + + function setUp() public { + // Activate Ethereum fork and get the contracts + contracts = _activateEthereumFork(); + + // Set up test accounts + admin = makeAddr("Admin"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + referrer = makeAddr("Referrer"); + + // Deal ETH to accounts + vm.deal(admin, 100 ether); + vm.deal(sender, 100 ether); + + // Deploy meta vault using helper + bytes memory initParams = abi.encode( + IEthErc20MetaVault.EthErc20MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + name: "SW Meta ETH Vault", + symbol: "swMetaETH", + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + address vaultAddr = _createVault(VaultType.EthErc20MetaVault, admin, initParams, false); + metaVault = EthErc20MetaVault(payable(vaultAddr)); + + // Get registry reference + registry = _getSubVaultsRegistry(address(metaVault)); + + // Deploy and add sub vaults + for (uint256 i = 0; i < 3; i++) { + address subVault = _createEthSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + subVaults.push(subVault); + + vm.prank(admin); + registry.addSubVault(subVault); + } + } + + function _updateMetaVaultState() internal { + uint64 newNonce = contracts.keeper.rewardsNonce() + 1; + _setKeeperRewardsNonce(newNonce); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], newNonce); + } + + metaVault.updateState(_getEmptyHarvestParams()); + } + + function test_deployment() public view { + assertEq(metaVault.vaultId(), keccak256("EthErc20MetaVault"), "Incorrect vault ID"); + assertEq(metaVault.version(), 6, "Incorrect version"); + assertEq(metaVault.admin(), admin, "Incorrect admin"); + assertEq(registry.subVaultsCurator(), _balancedCurator, "Incorrect curator"); + assertEq(metaVault.capacity(), type(uint256).max, "Incorrect capacity"); + assertEq(metaVault.feePercent(), 0, "Incorrect fee percent"); + assertEq(metaVault.name(), "SW Meta ETH Vault", "Incorrect name"); + assertEq(metaVault.symbol(), "swMetaETH", "Incorrect symbol"); + } + + function test_cannotInitializeTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + metaVault.initialize("0x"); + } + + function test_deposit() public { + uint256 totalAssetsBefore = metaVault.totalAssets(); + uint256 depositAmount = 10 ether; + uint256 expectedShares = metaVault.convertToShares(depositAmount); + + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, receiver, depositAmount, expectedShares, referrer); + + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_deposit"); + uint256 shares = metaVault.deposit{value: depositAmount}(receiver, referrer); + _stopSnapshotGas(); + + // Verify shares were minted to the receiver + assertApproxEqAbs(shares, expectedShares, 1, "Incorrect shares minted"); + assertApproxEqAbs(metaVault.balanceOf(receiver), expectedShares, 1, "Receiver did not receive shares"); + + // Verify total assets and shares + assertApproxEqAbs(metaVault.totalAssets(), totalAssetsBefore + depositAmount, 1, "Incorrect total assets"); + } + + function test_depositViaFallback() public { + vm.deal(address(this), 100 ether); + uint256 depositAmount = 5 ether; + uint256 expectedShares = metaVault.convertToShares(depositAmount); + + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(address(this), address(this), depositAmount, expectedShares, address(0)); + + _startSnapshotGas("EthErc20MetaVaultTest_test_depositViaFallback"); + Address.sendValue(payable(address(metaVault)), depositAmount); + _stopSnapshotGas(); + + // Verify shares were minted to the sender + assertApproxEqAbs(metaVault.balanceOf(address(this)), expectedShares, 1, "Sender did not receive shares"); + } + + function test_depositViaFallback_fromSubVault() public { + // First deposit to meta vault and sub vaults + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Get balance before + uint256 balanceBefore = metaVault.totalSupply(); + + // Simulate sub vault sending ETH (e.g., during claim) + // This should NOT create a deposit + vm.deal(subVaults[0], 1 ether); + vm.prank(subVaults[0]); + Address.sendValue(payable(address(metaVault)), 1 ether); + + // Total supply should not increase (no deposit was made) + assertEq(metaVault.totalSupply(), balanceBefore, "Total supply should not increase when sub vault sends ETH"); + } + + function test_updateStateAndDeposit() public { + // First deposit to meta vault and sub vaults to establish initial state + uint256 initialDeposit = 5 ether; + vm.prank(sender); + metaVault.deposit{value: initialDeposit}(sender, address(0)); + registry.depositToSubVaults(); + + // Set up a new deposit + uint256 depositAmount = 10 ether; + + // Update nonces for sub vaults to prepare for state update + uint64 newNonce = contracts.keeper.rewardsNonce() + 1; + _setKeeperRewardsNonce(newNonce); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], newNonce); + } + + // Remember state before the update + uint256 receiverSharesBefore = metaVault.balanceOf(receiver); + + // Create harvest params + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Call updateStateAndDeposit + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_updateStateAndDeposit"); + uint256 shares = metaVault.updateStateAndDeposit{value: depositAmount}(receiver, referrer, harvestParams); + _stopSnapshotGas(); + + // Verify deposit was processed + uint256 receiverSharesAfter = metaVault.balanceOf(receiver); + assertApproxEqAbs(receiverSharesAfter, receiverSharesBefore + shares, 1, "Receiver did not receive shares"); + } + + function test_depositAndMintOsToken() public { + // First collateralize the meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Mint osTokens + uint256 osTokenShares = depositAmount / 2; + + // Expect Deposited and OsTokenMinted events + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, sender, depositAmount, 0, referrer); + vm.expectEmit(true, false, false, false); + emit IVaultOsToken.OsTokenMinted(sender, sender, 0, osTokenShares, referrer); + + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_depositAndMintOsToken"); + uint256 mintedAssets = metaVault.depositAndMintOsToken{value: depositAmount}(sender, osTokenShares, referrer); + _stopSnapshotGas(); + + // Verify sender received osTokens + uint128 senderOsTokenShares = metaVault.osTokenPositions(sender); + assertEq(senderOsTokenShares, osTokenShares, "Incorrect osToken shares"); + assertGt(mintedAssets, 0, "No osToken assets minted"); + } + + function test_updateStateAndDepositAndMintOsToken() public { + // First collateralize the meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Set up harvest params + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Mint osTokens with state update + uint256 osTokenShares = depositAmount / 2; + + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_updateStateAndDepositAndMintOsToken"); + uint256 mintedAssets = metaVault.updateStateAndDepositAndMintOsToken{value: depositAmount}( + sender, osTokenShares, referrer, harvestParams + ); + _stopSnapshotGas(); + + // Verify sender received osTokens + uint128 senderOsTokenShares = metaVault.osTokenPositions(sender); + assertEq(senderOsTokenShares, osTokenShares, "Incorrect osToken shares"); + assertGt(mintedAssets, 0, "No osToken assets minted"); + } + + function test_donateAssets() public { + // First collateralize the meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + _updateMetaVaultState(); + + uint256 donationAmount = 1 ether; + + // Get vault state before donation + uint256 vaultBalanceBefore = address(metaVault).balance; + uint256 totalAssetsBefore = metaVault.totalAssets(); + + vm.startPrank(sender); + + // Check event emission + vm.expectEmit(true, true, false, true); + emit IVaultState.AssetsDonated(sender, donationAmount); + + // Make donation + metaVault.donateAssets{value: donationAmount}(); + vm.stopPrank(); + + // Verify donation was received + assertEq(address(metaVault).balance, vaultBalanceBefore + donationAmount, "Meta vault ETH balance increased"); + assertEq(metaVault.totalAssets(), totalAssetsBefore, "Meta vault total assets didn't increase"); + + // Process donation by updating state + _updateMetaVaultState(); + + assertEq(metaVault.totalAssets(), totalAssetsBefore + donationAmount, "Meta vault total assets increased"); + } + + function test_donateAssets_zeroValue() public { + vm.prank(sender); + vm.expectRevert(Errors.InvalidAssets.selector); + metaVault.donateAssets{value: 0}(); + } + + function test_transfer() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 senderBalanceBefore = metaVault.balanceOf(sender); + uint256 transferAmount = 1 ether; + + // Expect Transfer event + vm.expectEmit(true, true, false, true); + emit IERC20.Transfer(sender, receiver, transferAmount); + + // Transfer shares + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_transfer"); + bool success = metaVault.transfer(receiver, transferAmount); + _stopSnapshotGas(); + + assertTrue(success, "Transfer should succeed"); + assertEq(metaVault.balanceOf(sender), senderBalanceBefore - transferAmount, "Sender balance incorrect"); + assertEq(metaVault.balanceOf(receiver), transferAmount, "Receiver balance incorrect"); + } + + function test_transferFrom() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 senderBalanceBefore = metaVault.balanceOf(sender); + uint256 transferAmount = 1 ether; + + // Approve spender + address spender = makeAddr("Spender"); + vm.prank(sender); + metaVault.approve(spender, transferAmount); + + // Expect Transfer event + vm.expectEmit(true, true, false, true); + emit IERC20.Transfer(sender, receiver, transferAmount); + + // Transfer from + vm.prank(spender); + _startSnapshotGas("EthErc20MetaVaultTest_test_transferFrom"); + bool success = metaVault.transferFrom(sender, receiver, transferAmount); + _stopSnapshotGas(); + + assertTrue(success, "TransferFrom should succeed"); + assertEq(metaVault.balanceOf(sender), senderBalanceBefore - transferAmount, "Sender balance incorrect"); + assertEq(metaVault.balanceOf(receiver), transferAmount, "Receiver balance incorrect"); + } + + function test_transfer_checksOsTokenPosition() public { + // First collateralize the meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Mint osTokens for sender to create a position + uint256 osTokenShares = depositAmount / 2; + vm.prank(sender); + metaVault.mintOsToken(sender, osTokenShares, referrer); + + // Try to transfer almost all shares - should fail due to osToken position + uint256 largeTransfer = metaVault.balanceOf(sender) - 1; + vm.prank(sender); + vm.expectRevert(Errors.LowLtv.selector); + metaVault.transfer(receiver, largeTransfer); + } + + function test_enterExitQueue() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + uint256 senderBalanceBefore = metaVault.balanceOf(sender); + uint256 exitAmount = 1 ether; + + // Expect ExitQueueEntered event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.ExitQueueEntered(sender, sender, 0, exitAmount); + + // Enter exit queue + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_enterExitQueue"); + uint256 positionTicket = metaVault.enterExitQueue(exitAmount, sender); + _stopSnapshotGas(); + + // Position ticket should be valid (meta vault uses exit queue for collateralized vaults) + assertNotEq(positionTicket, type(uint256).max, "Should have queued exit"); + + // Balance should decrease + assertEq(metaVault.balanceOf(sender), senderBalanceBefore - exitAmount, "Balance should decrease"); + } + + function test_enterExitQueue_nonCollateralized() public { + // Deposit to get shares (vault is not collateralized since no sub vaults have deposits) + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 senderBalanceBefore = metaVault.balanceOf(sender); + uint256 exitAmount = 1 ether; + + // Enter exit queue - meta vault always uses exit queue, even when non-collateralized + vm.prank(sender); + _startSnapshotGas("EthErc20MetaVaultTest_test_enterExitQueue_nonCollateralized"); + uint256 positionTicket = metaVault.enterExitQueue(exitAmount, sender); + _stopSnapshotGas(); + + // Meta vault uses exit queue position (position ticket is 0 based) + assertLt(positionTicket, type(uint256).max, "Should have created exit queue position"); + + // Balance should decrease + assertEq(metaVault.balanceOf(sender), senderBalanceBefore - exitAmount, "Token balance should decrease"); + } + + function test_erc20Metadata() public view { + assertEq(metaVault.name(), "SW Meta ETH Vault", "Incorrect name"); + assertEq(metaVault.symbol(), "swMetaETH", "Incorrect symbol"); + assertEq(metaVault.decimals(), 18, "Incorrect decimals"); + } + + function test_approve() public { + address spender = makeAddr("Spender"); + uint256 amount = 100 ether; + + // Expect Approval event + vm.expectEmit(true, true, false, true); + emit IERC20.Approval(sender, spender, amount); + + vm.prank(sender); + bool success = metaVault.approve(spender, amount); + + assertTrue(success, "Approve should succeed"); + assertEq(metaVault.allowance(sender, spender), amount, "Allowance not set correctly"); + } + + function test_totalSupply() public { + uint256 totalSupplyBefore = metaVault.totalSupply(); + + uint256 depositAmount = 10 ether; + vm.prank(sender); + uint256 shares = metaVault.deposit{value: depositAmount}(sender, referrer); + + assertEq(metaVault.totalSupply(), totalSupplyBefore + shares, "Total supply not updated correctly"); + } + + function test_isStateUpdateRequired() public { + // First deposit to meta vault and establish the initial state + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Verify initial state - should not require update + assertFalse(registry.isStateUpdateRequired(), "Should not require state update initially"); + + // Get current nonce + uint64 initialNonce = contracts.keeper.rewardsNonce(); + + // Increase keeper nonce by 2 - now should require update + _setKeeperRewardsNonce(initialNonce + 2); + assertTrue(registry.isStateUpdateRequired(), "Should require state update when nonce is 2 higher"); + } +} diff --git a/test/EthGenesisVault.t.sol b/test/EthGenesisVault.t.sol index 4c7dd730..2f778eac 100644 --- a/test/EthGenesisVault.t.sol +++ b/test/EthGenesisVault.t.sol @@ -281,4 +281,116 @@ contract EthGenesisVaultTest is Test, EthHelpers { vm.prank(user); vault.claimExitedAssets(positionTicket, timestamp, uint256(exitQueueIndex)); } + + function test_vaultIdAndVersion() public { + address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); + EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); + assertEq(vault.vaultId(), keccak256("EthGenesisVault"), "Incorrect vault ID"); + assertEq(vault.version(), 5, "Incorrect version"); + } + + function test_deposit() public { + address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); + EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); + + uint256 depositAmount = 10 ether; + uint256 sharesBefore = vault.getShares(user); + uint256 totalAssetsBefore = vault.totalAssets(); + + _startSnapshotGas("EthGenesisVaultTest_test_deposit"); + _depositToVault(vaultAddr, depositAmount, user, user); + _stopSnapshotGas(); + + assertGt(vault.getShares(user), sharesBefore, "Shares should increase"); + assertEq(vault.totalAssets(), totalAssetsBefore + depositAmount, "Total assets should increase"); + } + + function test_migrate_mintsOsTokenShares() public { + address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); + EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); + + // Ensure vault is harvested and collateralized + _collateralizeEthVault(address(vault)); + + // Mock the pool escrow owner to be the vault + vm.mockCall(poolEscrow, abi.encodeWithSelector(IEthPoolEscrow.owner.selector), abi.encode(address(vault))); + + // Ensure osToken position is empty + assertEq(vault.osTokenPositions(user), 0, "OsToken position should be empty"); + + // Perform migration + uint256 migrateAmount = 10 ether; + vm.prank(rewardEthToken); + vault.migrate(user, migrateAmount); + + // Verify osToken position was created + uint256 osTokenShares = vault.osTokenPositions(user); + assertGt(osTokenShares, 0, "OsToken position should be created"); + } + + function test_migrate_existingOsTokenPosition() public { + address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); + EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); + + // Ensure vault is harvested and collateralized + _collateralizeEthVault(address(vault)); + + // First deposit and mint osToken to create an existing position + _depositToVault(vaultAddr, 10 ether, user, user); + uint256 osTokenAmount = 1 ether; + vm.prank(user); + vault.mintOsToken(user, osTokenAmount, address(0)); + + uint256 osTokenPositionBefore = vault.osTokenPositions(user); + assertEq(osTokenPositionBefore, osTokenAmount, "OsToken position should exist"); + + // Mock the pool escrow owner to be the vault + vm.mockCall(poolEscrow, abi.encodeWithSelector(IEthPoolEscrow.owner.selector), abi.encode(address(vault))); + + // Perform migration + uint256 migrateAmount = 10 ether; + vm.prank(rewardEthToken); + vault.migrate(user, migrateAmount); + + // Verify osToken position increased + uint256 osTokenPositionAfter = vault.osTokenPositions(user); + assertGt(osTokenPositionAfter, osTokenPositionBefore, "OsToken position should increase after migration"); + } + + function test_transferVaultAssets_pullsFromEscrow() public { + address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); + EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); + + // Deposit and enter exit queue + uint256 depositAmount = 10 ether; + _depositToVault(vaultAddr, depositAmount, user, user); + + uint256 shares = vault.getShares(user); + vm.prank(user); + uint256 timestamp = vm.getBlockTimestamp(); + uint256 positionTicket = vault.enterExitQueue(shares, user); + + // Process exit queue + IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(address(vault), 0, 0); + vault.updateState(harvestParams); + + // Wait for claim delay + vm.warp(timestamp + _exitingAssetsClaimDelay + 1); + + // Move all vault balance to pool escrow (simulating assets in escrow) + uint256 vaultBalance = address(vault).balance; + vm.deal(poolEscrow, poolEscrow.balance + vaultBalance); + vm.deal(vaultAddr, 0); + + // Claim should still work by pulling from pool escrow + int256 exitQueueIndex = vault.getExitQueueIndex(positionTicket); + uint256 userBalanceBefore = user.balance; + + vm.prank(user); + _startSnapshotGas("EthGenesisVaultTest_test_transferVaultAssets_pullsFromEscrow"); + vault.claimExitedAssets(positionTicket, timestamp, uint256(exitQueueIndex)); + _stopSnapshotGas(); + + assertGt(user.balance, userBalanceBefore, "User should receive assets"); + } } diff --git a/test/EthMetaVault.t.sol b/test/EthMetaVault.t.sol index 772e5279..2a62d881 100644 --- a/test/EthMetaVault.t.sol +++ b/test/EthMetaVault.t.sol @@ -6,9 +6,9 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; import {IEthVault} from "../contracts/interfaces/IEthVault.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; import {IVaultState} from "../contracts/interfaces/IVaultState.sol"; import {IVaultSubVaults} from "../contracts/interfaces/IVaultSubVaults.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; import {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; import {IVaultOsToken} from "../contracts/interfaces/IVaultOsToken.sol"; import {ISubVaultsCurator} from "../contracts/interfaces/ISubVaultsCurator.sol"; @@ -21,6 +21,19 @@ import {EthHelpers} from "./helpers/EthHelpers.sol"; import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; import {EthOsTokenRedeemer} from "../contracts/tokens/EthOsTokenRedeemer.sol"; +/// @dev Legacy interface for V5 meta vault that had sub-vault functions directly on it +interface ILegacyMetaVaultV5 { + struct SubVaultState { + uint128 stakedShares; + uint128 queuedShares; + } + + function subVaultsCurator() external view returns (address); + function subVaultsRewardsNonce() external view returns (uint128); + function getSubVaults() external view returns (address[] memory); + function subVaultsStates(address vault) external view returns (SubVaultState memory); +} + contract EthMetaVaultTest is Test, EthHelpers { bytes32 private constant exitQueueEnteredTopic = keccak256("ExitQueueEntered(address,address,uint256,uint256)"); address private constant FORK_META_VAULT = 0x34284C27A2304132aF751b0dEc5bBa2CF98eD039; @@ -47,6 +60,7 @@ contract EthMetaVaultTest is Test, EthHelpers { ForkContracts public contracts; EthMetaVault public metaVault; + ISubVaultsRegistry public registry; address public admin; address public sender; @@ -59,7 +73,11 @@ contract EthMetaVaultTest is Test, EthHelpers { // Pre-upgrade state for fork vault upgrade test PreUpgradeState public preUpgradeState; address[] public preUpgradeSubVaults; - mapping(address => IVaultSubVaults.SubVaultState) public preUpgradeSubVaultStates; + mapping(address => ISubVaultsRegistry.SubVaultState) public preUpgradeSubVaultStates; + + function _getRegistry(address vault) internal view returns (ISubVaultsRegistry) { + return ISubVaultsRegistry(EthMetaVault(payable(vault)).subVaultsRegistry()); + } function setUp() public { // Activate Ethereum fork and get the contracts @@ -82,7 +100,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: type(uint256).max, feePercent: 0, @@ -91,8 +109,11 @@ contract EthMetaVaultTest is Test, EthHelpers { ); metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + // Get registry reference + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Get existing sub vaults (if any) - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < currentSubVaults.length; i++) { subVaults.push(currentSubVaults[i]); } @@ -104,7 +125,7 @@ contract EthMetaVaultTest is Test, EthHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } } @@ -112,21 +133,27 @@ contract EthMetaVaultTest is Test, EthHelpers { EthMetaVault vault = EthMetaVault(payable(FORK_META_VAULT)); require(vault.version() == 5, "Fork vault is not version 5"); + // Use legacy interface for V5 vault functions that are now on the registry + ILegacyMetaVaultV5 legacyVault = ILegacyMetaVaultV5(FORK_META_VAULT); + preUpgradeState.admin = vault.admin(); preUpgradeState.feeRecipient = vault.feeRecipient(); preUpgradeState.feePercent = vault.feePercent(); preUpgradeState.totalShares = vault.totalShares(); preUpgradeState.totalAssets = vault.totalAssets(); preUpgradeState.capacity = vault.capacity(); - preUpgradeState.curator = vault.subVaultsCurator(); - preUpgradeState.rewardsNonce = vault.subVaultsRewardsNonce(); + preUpgradeState.curator = legacyVault.subVaultsCurator(); + preUpgradeState.rewardsNonce = legacyVault.subVaultsRewardsNonce(); (preUpgradeState.queuedShares, preUpgradeState.unclaimedAssets,,, preUpgradeState.totalTickets) = vault.getExitQueueData(); - address[] memory vaultSubVaults = vault.getSubVaults(); + address[] memory vaultSubVaults = legacyVault.getSubVaults(); for (uint256 i = 0; i < vaultSubVaults.length; i++) { preUpgradeSubVaults.push(vaultSubVaults[i]); - preUpgradeSubVaultStates[vaultSubVaults[i]] = vault.subVaultsStates(vaultSubVaults[i]); + ILegacyMetaVaultV5.SubVaultState memory legacyState = legacyVault.subVaultsStates(vaultSubVaults[i]); + preUpgradeSubVaultStates[vaultSubVaults[i]] = ISubVaultsRegistry.SubVaultState({ + stakedShares: legacyState.stakedShares, queuedShares: legacyState.queuedShares + }); } } @@ -159,13 +186,13 @@ contract EthMetaVaultTest is Test, EthHelpers { assertEq(metaVault.vaultId(), keccak256("EthMetaVault"), "Incorrect vault ID"); assertEq(metaVault.version(), 6, "Incorrect version"); assertEq(metaVault.admin(), admin, "Incorrect admin"); - assertEq(metaVault.subVaultsCurator(), _balancedCurator, "Incorrect curator"); + assertEq(registry.subVaultsCurator(), _balancedCurator, "Incorrect curator"); assertEq(metaVault.capacity(), type(uint256).max, "Incorrect capacity"); assertEq(metaVault.feePercent(), 0, "Incorrect fee percent"); assertEq(metaVault.feeRecipient(), admin, "Incorrect fee recipient"); // Verify sub vaults - address[] memory storedSubVaults = metaVault.getSubVaults(); + address[] memory storedSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < subVaults.length; i++) { assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); } @@ -177,6 +204,10 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; uint256 expectedShares = metaVault.convertToShares(depositAmount); + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, receiver, depositAmount, expectedShares, referrer); + vm.prank(sender); _startSnapshotGas("EthMetaVaultTest_test_deposit"); uint256 shares = metaVault.deposit{value: depositAmount}(receiver, referrer); @@ -196,6 +227,10 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 5 ether; uint256 expectedShares = metaVault.convertToShares(depositAmount); + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(address(this), address(this), depositAmount, expectedShares, address(0)); + _startSnapshotGas("EthMetaVaultTest_test_depositViaFallback"); Address.sendValue(payable(address(metaVault)), depositAmount); _stopSnapshotGas(); @@ -209,7 +244,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 initialDeposit = 5 ether; vm.prank(sender); metaVault.deposit{value: initialDeposit}(sender, address(0)); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Set up a new deposit uint256 depositAmount = 10 ether; @@ -263,11 +298,17 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Mint osTokens uint256 osTokenShares = depositAmount / 2; // 50% of deposit + // Expect Deposited and OsTokenMinted events + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, sender, depositAmount, 0, referrer); + vm.expectEmit(true, false, false, false); + emit IVaultOsToken.OsTokenMinted(sender, sender, 0, osTokenShares, referrer); + vm.prank(sender); _startSnapshotGas("EthMetaVaultTest_test_depositAndMintOsToken"); uint256 mintedAssets = metaVault.depositAndMintOsToken{value: depositAmount}(sender, osTokenShares, referrer); @@ -284,7 +325,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Set up harvest params IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); @@ -312,19 +353,19 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Verify initial state - should not require update - assertFalse(metaVault.isStateUpdateRequired(), "Should not require state update initially"); + assertFalse(registry.isStateUpdateRequired(), "Should not require state update initially"); // Get current nonce uint64 initialNonce = contracts.keeper.rewardsNonce(); // Increase keeper nonce by 1 - still should not require update _setKeeperRewardsNonce(initialNonce + 1); - assertFalse(metaVault.isStateUpdateRequired(), "Should not require state update when nonce is only 1 higher"); + assertFalse(registry.isStateUpdateRequired(), "Should not require state update when nonce is only 1 higher"); // Increase keeper nonce by 1 more - now should require update _startSnapshotGas("EthMetaVaultTest_test_isStateUpdateRequired_true"); _setKeeperRewardsNonce(initialNonce + 2); - bool required = metaVault.isStateUpdateRequired(); + bool required = registry.isStateUpdateRequired(); _stopSnapshotGas(); assertTrue(required, "Should require state update when nonce is 2 higher"); @@ -338,12 +379,12 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.updateState(_getEmptyHarvestParams()); // Verify no update required after state update - assertFalse(metaVault.isStateUpdateRequired(), "Should not require state update after updating"); + assertFalse(registry.isStateUpdateRequired(), "Should not require state update after updating"); // Test with empty sub vaults // Create a new meta vault without sub vaults bytes memory emptyInitParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: 1000 ether, feePercent: 1000, @@ -368,7 +409,7 @@ contract EthMetaVaultTest is Test, EthHelpers { _setKeeperRewardsNonce(initialNonce + 2); // Verify behavior - assertFalse(metaVault.isStateUpdateRequired(), "Should not require update when keeper nonce is lower"); + assertFalse(registry.isStateUpdateRequired(), "Should not require update when keeper nonce is lower"); } function test_userClaimExitedAssets() public { @@ -378,7 +419,7 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Deposit to sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Enter exit queue with all shares uint256 senderShares = metaVault.getShares(sender); @@ -414,10 +455,10 @@ contract EthMetaVaultTest is Test, EthHelpers { } // Prepare exit requests for claiming from sub vaults to meta vault - IVaultSubVaults.SubVaultExitRequest[] memory exitRequests = - new IVaultSubVaults.SubVaultExitRequest[](extractedExits.length); + ISubVaultsRegistry.SubVaultExitRequest[] memory exitRequests = + new ISubVaultsRegistry.SubVaultExitRequest[](extractedExits.length); for (uint256 i = 0; i < extractedExits.length; i++) { - exitRequests[i] = IVaultSubVaults.SubVaultExitRequest({ + exitRequests[i] = ISubVaultsRegistry.SubVaultExitRequest({ vault: extractedExits[i].vault, exitQueueIndex: uint256( IVaultEnterExit(extractedExits[i].vault).getExitQueueIndex(extractedExits[i].positionTicket) @@ -430,7 +471,7 @@ contract EthMetaVaultTest is Test, EthHelpers { vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); // Claim exited assets from sub vaults to meta vault - metaVault.claimSubVaultsExitedAssets(exitRequests); + registry.claimSubVaultsExitedAssets(exitRequests); // Update nonces for sub vaults to process exit queue newNonce = contracts.keeper.rewardsNonce() + 1; @@ -478,7 +519,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -514,7 +555,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Trying to donate 0 ETH should revert vm.prank(sender); @@ -539,20 +580,23 @@ contract EthMetaVaultTest is Test, EthHelpers { assertEq(vault.feePercent(), preUpgradeState.feePercent, "Fee percent should be preserved"); // Verify vault state preserved + ISubVaultsRegistry vaultRegistry = _getRegistry(address(vault)); assertEq(vault.totalShares(), preUpgradeState.totalShares, "Total shares should be preserved"); assertEq(vault.totalAssets(), preUpgradeState.totalAssets, "Total assets should be preserved"); assertEq(vault.capacity(), preUpgradeState.capacity, "Capacity should be preserved"); - assertEq(vault.subVaultsCurator(), preUpgradeState.curator, "Curator should be preserved"); - assertEq(vault.subVaultsRewardsNonce(), preUpgradeState.rewardsNonce, "Rewards nonce should be preserved"); + assertEq(vaultRegistry.subVaultsCurator(), preUpgradeState.curator, "Curator should be preserved"); + assertEq( + vaultRegistry.subVaultsRewardsNonce(), preUpgradeState.rewardsNonce, "Rewards nonce should be preserved" + ); // Verify sub vaults state preserved (original sub vaults should still be present) - address[] memory postSubVaults = vault.getSubVaults(); + address[] memory postSubVaults = vaultRegistry.getSubVaults(); assertGe(postSubVaults.length, preUpgradeSubVaults.length, "Should have at least original sub vaults"); for (uint256 i = 0; i < preUpgradeSubVaults.length; i++) { // Original sub vaults should be at the beginning of the list assertEq(postSubVaults[i], preUpgradeSubVaults[i], "Sub vault address should be preserved"); - IVaultSubVaults.SubVaultState memory postState = vault.subVaultsStates(preUpgradeSubVaults[i]); - IVaultSubVaults.SubVaultState memory preState = preUpgradeSubVaultStates[preUpgradeSubVaults[i]]; + ISubVaultsRegistry.SubVaultState memory postState = vaultRegistry.subVaultsStates(preUpgradeSubVaults[i]); + ISubVaultsRegistry.SubVaultState memory preState = preUpgradeSubVaultStates[preUpgradeSubVaults[i]]; assertEq(postState.stakedShares, preState.stakedShares, "Staked shares should be preserved"); assertEq(postState.queuedShares, preState.queuedShares, "Queued shares should be preserved"); } @@ -569,18 +613,18 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Increase keeper nonce by 2 to trigger NotHarvested uint64 initialNonce = contracts.keeper.rewardsNonce(); _setKeeperRewardsNonce(initialNonce + 2); // Verify state update is required - assertTrue(metaVault.isStateUpdateRequired(), "State update should be required"); + assertTrue(registry.isStateUpdateRequired(), "State update should be required"); // Try to call calculateSubVaultsRedemptions - should revert with NotHarvested vm.expectRevert(Errors.NotHarvested.selector); - metaVault.calculateSubVaultsRedemptions(1 ether); + registry.calculateSubVaultsRedemptions(1 ether); } function test_calculateSubVaultsRedemptions_withMetaSubVault() public { @@ -588,11 +632,11 @@ contract EthMetaVaultTest is Test, EthHelpers { _updateMetaVaultState(); // Get the current nonce that main meta vault is at - uint128 currentNonce = metaVault.subVaultsRewardsNonce(); + uint128 currentNonce = registry.subVaultsRewardsNonce(); // Create another meta vault to use as a sub vault bytes memory metaInitParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: type(uint256).max, feePercent: 0, @@ -609,8 +653,9 @@ contract EthMetaVaultTest is Test, EthHelpers { // Set the nested sub vault's nonce to match what the sub meta vault expects _setVaultRewardsNonce(nestedSubVault, uint64(currentNonce)); + ISubVaultsRegistry subMetaVaultRegistry = _getRegistry(address(subMetaVault)); vm.prank(admin); - subMetaVault.addSubVault(nestedSubVault); + subMetaVaultRegistry.addSubVault(nestedSubVault); // Deposit to sub meta vault vm.deal(admin, 50 ether); @@ -632,16 +677,16 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.updateState(_getEmptyHarvestParams()); // Deposit to sub vaults - subMetaVault.depositToSubVaults(); + _getRegistry(address(subMetaVault)).depositToSubVaults(); // Propose adding meta vault as sub vault (requires approval) vm.prank(admin); - metaVault.addSubVault(address(subMetaVault)); + registry.addSubVault(address(subMetaVault)); // Accept the meta sub vault (requires VaultsRegistry owner) address registryOwner = contracts.vaultsRegistry.owner(); vm.prank(registryOwner); - metaVault.acceptMetaSubVault(address(subMetaVault)); + registry.acceptMetaSubVault(address(subMetaVault)); // Deposit to main meta vault vm.prank(sender); @@ -658,7 +703,7 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.updateState(_getEmptyHarvestParams()); // Deposit main meta vault assets to sub vaults to make withdrawable assets 0 - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Set meta vault balance to cover unclaimed assets only (withdrawable = 0) (, uint256 unclaimedAssets,,,) = metaVault.getExitQueueData(); @@ -684,7 +729,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Test calculateSubVaultsRedemptions with meta sub vault present uint256 assetsToRedeem = 5 ether; - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(assetsToRedeem); // Should return exit requests since withdrawable assets are 0 assertGt(requests.length, 0, "Should return exit requests when no withdrawable assets"); @@ -697,7 +742,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // If the request is for the sub meta vault, verify it can calculate its own redemptions if (requests[i].vault == address(subMetaVault)) { ISubVaultsCurator.ExitRequest[] memory subMetaRequests = - subMetaVault.calculateSubVaultsRedemptions(requests[i].assets); + _getRegistry(address(subMetaVault)).calculateSubVaultsRedemptions(requests[i].assets); // Sub meta vault should also return exit requests from its nested sub vault assertGt(subMetaRequests.length, 0, "Sub meta vault should return exit requests"); @@ -723,7 +768,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -731,10 +776,10 @@ contract EthMetaVaultTest is Test, EthHelpers { // Start ejecting one of the sub vaults address vaultToEject = subVaults[0]; vm.prank(admin); - metaVault.ejectSubVault(vaultToEject); + registry.ejectSubVault(vaultToEject); // Verify ejecting sub vault is set - assertEq(metaVault.ejectingSubVault(), vaultToEject, "Ejecting sub vault should be set"); + assertEq(registry.ejectingSubVault(), vaultToEject, "Ejecting sub vault should be set"); // Update meta vault state after ejection _updateMetaVaultState(); @@ -743,7 +788,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 withdrawableAssets = metaVault.withdrawableAssets(); // Get ejecting sub vault assets - these are counted as available in calculateSubVaultsRedemptions - IVaultSubVaults.SubVaultState memory ejectingState = metaVault.subVaultsStates(vaultToEject); + ISubVaultsRegistry.SubVaultState memory ejectingState = registry.subVaultsStates(vaultToEject); uint256 ejectingAssets = 0; if (ejectingState.queuedShares > 0) { ejectingAssets = IVaultState(vaultToEject).convertToAssets(ejectingState.queuedShares); @@ -751,7 +796,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Request redemption for more than withdrawable + ejecting assets to force requests from other sub vaults uint256 assetsToRedeem = withdrawableAssets + ejectingAssets + 5 ether; - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(assetsToRedeem); // Should return exit requests since we're requesting more than available assertGt(requests.length, 0, "Should return exit requests"); @@ -784,7 +829,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -796,7 +841,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Request more assets than withdrawable uint256 assetsToRedeem = 10 ether; _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_insufficientWithdrawableAssets"); - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(assetsToRedeem); _stopSnapshotGas(); // Should return exit requests from sub vaults @@ -827,7 +872,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Request exactly the withdrawable assets _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_exactWithdrawableAssets"); - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(withdrawableAssets); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(withdrawableAssets); _stopSnapshotGas(); // Should return empty array since withdrawable assets exactly match @@ -839,7 +884,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 50 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -851,7 +896,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 assetsToRedeem = withdrawableAssets + 20 ether; _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_success"); - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(assetsToRedeem); _stopSnapshotGas(); // Should return exit requests from sub vaults @@ -891,7 +936,7 @@ contract EthMetaVaultTest is Test, EthHelpers { _updateMetaVaultState(); // Request zero assets - should return empty array - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(0); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(0); assertEq(requests.length, 0, "Should return empty requests for zero assets"); } @@ -906,7 +951,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Request less than withdrawable uint256 assetsToRedeem = 5 ether; - ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(assetsToRedeem); // Should return empty array since withdrawable covers it assertEq(requests.length, 0, "Should return empty requests when less than withdrawable"); @@ -916,11 +961,11 @@ contract EthMetaVaultTest is Test, EthHelpers { // Only the redeemer can call redeemSubVaultsAssets vm.prank(sender); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.redeemSubVaultsAssets(1 ether); + registry.redeemSubVaultsAssets(1 ether); vm.prank(admin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.redeemSubVaultsAssets(1 ether); + registry.redeemSubVaultsAssets(1 ether); } function test_redeemSubVaultsAssets_zeroAssets() public { @@ -930,7 +975,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Try to redeem zero assets vm.prank(redeemer); vm.expectRevert(Errors.InvalidAssets.selector); - metaVault.redeemSubVaultsAssets(0); + registry.redeemSubVaultsAssets(0); } function test_redeemSubVaultsAssets_noRedeemRequests() public { @@ -956,7 +1001,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Should return 0 since no redeem requests needed (withdrawable covers it) vm.prank(redeemer); _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_noRedeemRequests"); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(depositAmount); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(depositAmount); _stopSnapshotGas(); // Verify meta vault balance unchanged (no redemptions occurred) @@ -983,7 +1028,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -1015,7 +1060,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsExceedSubVaultsWithdrawableAssets"); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); _stopSnapshotGas(); // Verify redemption occurred - total redeemed should equal total sub vault withdrawable @@ -1046,7 +1091,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -1078,7 +1123,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsLessThanSubVaultsWithdrawableAssets"); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); _stopSnapshotGas(); // Verify redemption occurred and matches requested amount (not more) @@ -1123,7 +1168,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -1154,7 +1199,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_success"); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); _stopSnapshotGas(); // Verify redemption occurred @@ -1186,7 +1231,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update meta vault state _updateMetaVaultState(); @@ -1208,7 +1253,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_noRoundingErrors"); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); _stopSnapshotGas(); // Verify no rounding errors - redeemed amount should match requested exactly or be very close @@ -1235,7 +1280,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Verify sub vault states were properly updated (no extra shares remaining) uint256 totalSubVaultAssets = 0; for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory state = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory state = registry.subVaultsStates(subVaults[i]); if (state.stakedShares > 0) { totalSubVaultAssets += IVaultState(subVaults[i]).convertToAssets(state.stakedShares); } @@ -1256,7 +1301,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Increase keeper nonce by 2 to trigger NotHarvested uint64 initialNonce = contracts.keeper.rewardsNonce(); @@ -1275,13 +1320,13 @@ contract EthMetaVaultTest is Test, EthHelpers { contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); // Verify state update is required - assertTrue(metaVault.isStateUpdateRequired(), "State update should be required"); + assertTrue(registry.isStateUpdateRequired(), "State update should be required"); // Try to redeem - should revert with NotHarvested address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); vm.expectRevert(Errors.NotHarvested.selector); - metaVault.redeemSubVaultsAssets(1 ether); + registry.redeemSubVaultsAssets(1 ether); } function test_redeemSubVaultsAssets_emitsEvent() public { @@ -1301,7 +1346,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1316,7 +1361,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); vm.recordLogs(); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); // Find and verify the SubVaultsAssetsRedeemed event Vm.Log[] memory logs = vm.getRecordedLogs(); @@ -1350,14 +1395,14 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record staked shares before uint256[] memory stakedSharesBefore = new uint256[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory state = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory state = registry.subVaultsStates(subVaults[i]); stakedSharesBefore[i] = state.stakedShares; } @@ -1371,12 +1416,12 @@ contract EthMetaVaultTest is Test, EthHelpers { // Perform redemption address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - metaVault.redeemSubVaultsAssets(assetsToRedeem); + registry.redeemSubVaultsAssets(assetsToRedeem); // Verify staked shares decreased for at least one sub vault bool anySharesReduced = false; for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory stateAfter = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory stateAfter = registry.subVaultsStates(subVaults[i]); if (stateAfter.stakedShares < stakedSharesBefore[i]) { anySharesReduced = true; break; @@ -1402,20 +1447,20 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Start ejecting one sub vault (use last one since it's always a newly created vault) address vaultToEject = subVaults[subVaults.length - 1]; vm.prank(admin); - metaVault.ejectSubVault(vaultToEject); + registry.ejectSubVault(vaultToEject); _updateMetaVaultState(); // Get the ejecting vault's queued shares value - these are counted toward redemption // but can't be redeemed immediately (they're in exit queue) - IVaultSubVaults.SubVaultState memory ejectingState = metaVault.subVaultsStates(vaultToEject); + ISubVaultsRegistry.SubVaultState memory ejectingState = registry.subVaultsStates(vaultToEject); uint256 ejectingVaultAssets = IVaultState(vaultToEject).convertToAssets(ejectingState.queuedShares); // Give remaining sub vaults (all except the ejecting one) withdrawable assets @@ -1429,15 +1474,15 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 assetsToRedeem = ejectingVaultAssets + 5 ether; // Record ejecting vault's state before - IVaultSubVaults.SubVaultState memory ejectingStateBefore = metaVault.subVaultsStates(vaultToEject); + ISubVaultsRegistry.SubVaultState memory ejectingStateBefore = registry.subVaultsStates(vaultToEject); // Perform redemption address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(assetsToRedeem); // Verify ejecting vault's state unchanged (ejecting shares are not modified by redemption) - IVaultSubVaults.SubVaultState memory ejectingStateAfter = metaVault.subVaultsStates(vaultToEject); + ISubVaultsRegistry.SubVaultState memory ejectingStateAfter = registry.subVaultsStates(vaultToEject); assertEq( ejectingStateAfter.stakedShares, ejectingStateBefore.stakedShares, @@ -1473,7 +1518,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1491,7 +1536,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Try to redeem - should return 0 since no sub vaults have withdrawable assets address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(5 ether); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(5 ether); assertEq(totalRedeemed, 0, "Should redeem 0 when all sub vaults have 0 withdrawable"); } @@ -1513,7 +1558,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1531,7 +1576,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Perform first redemption vm.prank(redeemer); - uint256 redeemed1 = metaVault.redeemSubVaultsAssets(3 ether); + uint256 redeemed1 = registry.redeemSubVaultsAssets(3 ether); totalRedeemed += redeemed1; // Note: After first redemption, meta vault now has withdrawable assets (redeemed1) @@ -1541,13 +1586,13 @@ contract EthMetaVaultTest is Test, EthHelpers { // Second redemption - request more than current withdrawable uint256 metaVaultWithdrawable = metaVault.withdrawableAssets(); vm.prank(redeemer); - uint256 redeemed2 = metaVault.redeemSubVaultsAssets(metaVaultWithdrawable + 4 ether); + uint256 redeemed2 = registry.redeemSubVaultsAssets(metaVaultWithdrawable + 4 ether); totalRedeemed += redeemed2; // Third redemption metaVaultWithdrawable = metaVault.withdrawableAssets(); vm.prank(redeemer); - uint256 redeemed3 = metaVault.redeemSubVaultsAssets(metaVaultWithdrawable + 2 ether); + uint256 redeemed3 = registry.redeemSubVaultsAssets(metaVaultWithdrawable + 2 ether); totalRedeemed += redeemed3; // Verify total redeemed is sum of all redemptions @@ -1581,7 +1626,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1595,7 +1640,7 @@ contract EthMetaVaultTest is Test, EthHelpers { address redeemer = contracts.osTokenConfig.redeemer(); uint256 smallAmount = 10; vm.prank(redeemer); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(smallAmount); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(smallAmount); // Verify redemption succeeded with minimal rounding assertApproxEqAbs(totalRedeemed, smallAmount, 5, "Small redemption should work with minimal rounding"); @@ -1620,7 +1665,7 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Deposit only part to sub vaults by manipulating the balance - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1640,7 +1685,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Redeem more than meta vault's withdrawable but less than total address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(8 ether); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(8 ether); // Should only redeem from sub vaults the amount exceeding meta vault's withdrawable (3 ether) // Allow slightly larger delta for fork state rounding differences @@ -1664,12 +1709,12 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record staked shares before for first sub vault - IVaultSubVaults.SubVaultState memory stateBefore = metaVault.subVaultsStates(subVaults[0]); + ISubVaultsRegistry.SubVaultState memory stateBefore = registry.subVaultsStates(subVaults[0]); uint256 stakedAssetsBefore = IVaultState(subVaults[0]).convertToAssets(stateBefore.stakedShares); // Set all sub vaults to have 0 withdrawable except the first one @@ -1696,7 +1741,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // but only the first vault can actually provide assets address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(5 ether); + uint256 totalRedeemed = registry.redeemSubVaultsAssets(5 ether); // The actual redemption is limited by what the curator requests from the first vault // The curator uses balanced distribution, so it may request less from each vault @@ -1704,7 +1749,7 @@ contract EthMetaVaultTest is Test, EthHelpers { assertGt(totalRedeemed, 0, "Should redeem some assets from the first sub vault"); // Verify first sub vault's staked shares decreased - IVaultSubVaults.SubVaultState memory stateAfter = metaVault.subVaultsStates(subVaults[0]); + ISubVaultsRegistry.SubVaultState memory stateAfter = registry.subVaultsStates(subVaults[0]); uint256 stakedAssetsAfter = IVaultState(subVaults[0]).convertToAssets(stateAfter.stakedShares); assertLt(stakedAssetsAfter, stakedAssetsBefore, "First sub vault staked assets should decrease"); @@ -1734,7 +1779,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 30 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); @@ -1756,7 +1801,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Perform redemption address redeemer = contracts.osTokenConfig.redeemer(); vm.prank(redeemer); - metaVault.redeemSubVaultsAssets(10 ether); + registry.redeemSubVaultsAssets(10 ether); // Verify meta vault still has no osToken positions in sub vaults after for (uint256 i = 0; i < subVaults.length; i++) { @@ -1774,7 +1819,7 @@ contract EthMetaVaultTest is Test, EthHelpers { returns (ExitRequest[] memory exitRequests) { uint256 subVaultsCount = _subVaults.length; - uint256 exitSubVaultsCount = metaVault.ejectingSubVault() != address(0) ? subVaultsCount - 1 : subVaultsCount; + uint256 exitSubVaultsCount = registry.ejectingSubVault() != address(0) ? subVaultsCount - 1 : subVaultsCount; exitRequests = new ExitRequest[](exitSubVaultsCount); uint256 subVaultIndex = 0; for (uint256 i = 0; i < logs.length; i++) { diff --git a/test/EthOsTokenRedeemer.t.sol b/test/EthOsTokenRedeemer.t.sol index 2922641a..cd07ce94 100644 --- a/test/EthOsTokenRedeemer.t.sol +++ b/test/EthOsTokenRedeemer.t.sol @@ -9,11 +9,12 @@ import {EthOsTokenRedeemer} from "../contracts/tokens/EthOsTokenRedeemer.sol"; import {IOsTokenRedeemer} from "../contracts/interfaces/IOsTokenRedeemer.sol"; import {EthVault, IEthVault} from "../contracts/vaults/ethereum/EthVault.sol"; import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; +import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; import {IVaultState} from "../contracts/interfaces/IVaultState.sol"; import {Errors} from "../contracts/libraries/Errors.sol"; import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; import {EthHelpers} from "./helpers/EthHelpers.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; contract EthOsTokenRedeemerTest is Test, EthHelpers { using stdStorage for StdStorage; @@ -24,6 +25,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { EthVault public vault; EthVault public vault2; EthMetaVault public metaVault; + ISubVaultsRegistry public registry; address[] public subVaults; // Test accounts @@ -132,7 +134,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { function _setupMetaVault() internal { // Deploy meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: type(uint256).max, feePercent: 0, @@ -141,8 +143,11 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { ); metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + // Get registry reference + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Get existing sub vaults (if any from fork) - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < currentSubVaults.length; i++) { subVaults.push(currentSubVaults[i]); } @@ -154,7 +159,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } // Deposit to meta vault to make it have assets @@ -1476,13 +1481,13 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(user1, address(0)); // Distribute assets to sub-vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record sub vault states before redemption uint256[] memory stakedSharesBefore = new uint256[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - stakedSharesBefore[i] = metaVault.subVaultsStates(subVaults[i]).stakedShares; + stakedSharesBefore[i] = registry.subVaultsStates(subVaults[i]).stakedShares; } // Request redemption that will require multiple sub-vaults @@ -1496,7 +1501,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Verify assets were redeemed from multiple sub-vaults uint256 subVaultsWithRedemptions = 0; for (uint256 i = 0; i < subVaults.length; i++) { - uint256 stakedSharesAfter = metaVault.subVaultsStates(subVaults[i]).stakedShares; + uint256 stakedSharesAfter = registry.subVaultsStates(subVaults[i]).stakedShares; if (stakedSharesAfter < stakedSharesBefore[i]) { subVaultsWithRedemptions++; } @@ -1516,7 +1521,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { metaVault.deposit{value: 100 ether}(user1, address(0)); // Distribute assets to sub-vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Use fixed amounts for redemption @@ -1549,13 +1554,13 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(user1, address(0)); // Distribute assets to sub-vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record initial states uint256 totalStakedBefore = 0; for (uint256 i = 0; i < subVaults.length; i++) { - totalStakedBefore += metaVault.subVaultsStates(subVaults[i]).stakedShares; + totalStakedBefore += registry.subVaultsStates(subVaults[i]).stakedShares; } // Redeem significant amount @@ -1566,7 +1571,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Verify state updates were properly applied uint256 totalStakedAfter = 0; for (uint256 i = 0; i < subVaults.length; i++) { - totalStakedAfter += metaVault.subVaultsStates(subVaults[i]).stakedShares; + totalStakedAfter += registry.subVaultsStates(subVaults[i]).stakedShares; } // Total staked shares should decrease @@ -1574,7 +1579,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Verify no negative shares or overflow for (uint256 i = 0; i < subVaults.length; i++) { - uint128 stakedShares = metaVault.subVaultsStates(subVaults[i]).stakedShares; + uint128 stakedShares = registry.subVaultsStates(subVaults[i]).stakedShares; assertGe(stakedShares, 0, "Staked shares should never be negative"); } } @@ -1590,13 +1595,13 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(user1, address(0)); // Distribute assets to sub-vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record initial staked shares uint256[] memory initialShares = new uint256[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - initialShares[i] = metaVault.subVaultsStates(subVaults[i]).stakedShares; + initialShares[i] = registry.subVaultsStates(subVaults[i]).stakedShares; } // Request a large redemption amount (uses fixed amount like other working tests) @@ -1613,7 +1618,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Check that some sub-vaults had their staked shares reduced uint256 vaultsWithReductions = 0; for (uint256 i = 0; i < subVaults.length; i++) { - uint256 currentShares = metaVault.subVaultsStates(subVaults[i]).stakedShares; + uint256 currentShares = registry.subVaultsStates(subVaults[i]).stakedShares; if (currentShares < initialShares[i]) { vaultsWithReductions++; } @@ -1633,14 +1638,14 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(user1, address(0)); // Distribute assets to sub-vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _updateMetaVaultState(); // Record initial values uint256 metaVaultBalanceBefore = address(metaVault).balance; uint256 totalSubVaultSharesBefore = 0; for (uint256 i = 0; i < subVaults.length; i++) { - totalSubVaultSharesBefore += metaVault.subVaultsStates(subVaults[i]).stakedShares; + totalSubVaultSharesBefore += registry.subVaultsStates(subVaults[i]).stakedShares; } // Perform a redemption @@ -1654,7 +1659,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Verify sub vault shares decreased uint256 totalSubVaultSharesAfter = 0; for (uint256 i = 0; i < subVaults.length; i++) { - totalSubVaultSharesAfter += metaVault.subVaultsStates(subVaults[i]).stakedShares; + totalSubVaultSharesAfter += registry.subVaultsStates(subVaults[i]).stakedShares; } assertLt(totalSubVaultSharesAfter, totalSubVaultSharesBefore, "Sub vault shares should decrease"); @@ -1668,6 +1673,61 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { ); } + // ============ test_updateVaultState Tests ============ + + function test_updateVaultState_success() public { + // Get the vault's reward nonce before update + (, uint64 nonceBefore) = contracts.keeper.rewards(address(vault)); + + // Create harvest params with some rewards + IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(address(vault), 1 ether, 0); + + // Call updateVaultState through the redeemer + _startSnapshotGas("EthOsTokenRedeemerTest_test_updateVaultState_success"); + osTokenRedeemer.updateVaultState(address(vault), harvestParams); + _stopSnapshotGas(); + + // Verify the vault state was updated (nonce should increase) + (, uint64 nonceAfter) = contracts.keeper.rewards(address(vault)); + assertGt(nonceAfter, nonceBefore, "Vault reward nonce should increase after update"); + } + + function test_updateVaultState_zeroRewards() public { + // Get the vault's reward nonce before update + (, uint64 nonceBefore) = contracts.keeper.rewards(address(vault)); + + // Create harvest params with zero rewards + IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(address(vault), 0, 0); + + // Call updateVaultState with zero rewards - should succeed + osTokenRedeemer.updateVaultState(address(vault), harvestParams); + + // Verify the vault state was updated (nonce should increase even with zero rewards) + (, uint64 nonceAfter) = contracts.keeper.rewards(address(vault)); + assertGt(nonceAfter, nonceBefore, "Vault reward nonce should increase after update"); + } + + function test_updateVaultState_invalidVault() public { + // Create harvest params + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Try to call updateVaultState on an invalid vault address + address invalidVault = makeAddr("InvalidVault"); + + // Should revert with InvalidVault error when calling on a non-registered vault address + vm.expectRevert(Errors.InvalidVault.selector); + osTokenRedeemer.updateVaultState(invalidVault, harvestParams); + } + + function test_updateVaultState_invalidRewardsRoot() public { + // Get empty harvest params (invalid rewards root) + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Should revert with InvalidRewardsRoot when using empty params + vm.expectRevert(Errors.InvalidRewardsRoot.selector); + osTokenRedeemer.updateVaultState(address(vault), harvestParams); + } + // Helper to setup meta vault with specific number of sub vaults function _setupMetaVaultWithSubVaults(uint256 numSubVaults) internal { // Clear existing subVaults array @@ -1675,7 +1735,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: type(uint256).max, feePercent: 0, @@ -1684,6 +1744,9 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { ); metaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + // Update registry reference for new meta vault + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Deploy and add sub vaults for (uint256 i = 0; i < numSubVaults; i++) { address subVault = _createSubVault(admin); @@ -1691,7 +1754,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } } } diff --git a/test/EthPrivErc20MetaVault.t.sol b/test/EthPrivErc20MetaVault.t.sol new file mode 100644 index 00000000..67c74b43 --- /dev/null +++ b/test/EthPrivErc20MetaVault.t.sol @@ -0,0 +1,385 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IEthErc20MetaVault} from "../contracts/interfaces/IEthErc20MetaVault.sol"; +import {IEthPrivErc20MetaVault} from "../contracts/interfaces/IEthPrivErc20MetaVault.sol"; +import {IVaultState} from "../contracts/interfaces/IVaultState.sol"; +import {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; +import {IVaultOsToken} from "../contracts/interfaces/IVaultOsToken.sol"; +import {IVaultWhitelist} from "../contracts/interfaces/IVaultWhitelist.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; +import {Errors} from "../contracts/libraries/Errors.sol"; +import {EthPrivErc20MetaVault} from "../contracts/vaults/ethereum/EthPrivErc20MetaVault.sol"; +import {EthHelpers} from "./helpers/EthHelpers.sol"; + +contract EthPrivErc20MetaVaultTest is Test, EthHelpers { + ForkContracts public contracts; + EthPrivErc20MetaVault public metaVault; + ISubVaultsRegistry public registry; + + address public admin; + address public sender; + address public receiver; + address public referrer; + address public whitelister; + address public nonWhitelistedUser; + + // Sub vaults + address[] public subVaults; + + function setUp() public { + // Activate Ethereum fork and get the contracts + contracts = _activateEthereumFork(); + + // Set up test accounts + admin = makeAddr("Admin"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + referrer = makeAddr("Referrer"); + whitelister = makeAddr("Whitelister"); + nonWhitelistedUser = makeAddr("NonWhitelistedUser"); + + // Deal ETH to accounts + vm.deal(admin, 100 ether); + vm.deal(sender, 100 ether); + vm.deal(nonWhitelistedUser, 100 ether); + + // Deploy meta vault using helper + bytes memory initParams = abi.encode( + IEthErc20MetaVault.EthErc20MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + name: "SW Priv Meta ETH Vault", + symbol: "swPrvMeta", + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + address vaultAddr = _createVault(VaultType.EthPrivErc20MetaVault, admin, initParams, false); + metaVault = EthPrivErc20MetaVault(payable(vaultAddr)); + + // Set whitelister + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + // Whitelist sender and receiver + vm.startPrank(whitelister); + metaVault.updateWhitelist(sender, true); + metaVault.updateWhitelist(receiver, true); + vm.stopPrank(); + + // Get registry reference + registry = _getSubVaultsRegistry(address(metaVault)); + + // Deploy and add sub vaults + for (uint256 i = 0; i < 3; i++) { + address subVault = _createEthSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + subVaults.push(subVault); + + vm.prank(admin); + registry.addSubVault(subVault); + } + } + + function test_deployment() public view { + assertEq(metaVault.vaultId(), keccak256("EthPrivErc20MetaVault"), "Incorrect vault ID"); + assertEq(metaVault.version(), 6, "Incorrect version"); + assertEq(metaVault.admin(), admin, "Incorrect admin"); + assertEq(metaVault.whitelister(), whitelister, "Incorrect whitelister"); + assertEq(metaVault.name(), "SW Priv Meta ETH Vault", "Incorrect name"); + assertEq(metaVault.symbol(), "swPrvMeta", "Incorrect symbol"); + } + + function test_cannotInitializeTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + metaVault.initialize("0x"); + } + + function test_deposit_whitelistedUser() public { + uint256 depositAmount = 10 ether; + + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, receiver, depositAmount, 0, referrer); + + vm.prank(sender); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_deposit_whitelistedUser"); + uint256 shares = metaVault.deposit{value: depositAmount}(receiver, referrer); + _stopSnapshotGas(); + + assertGt(shares, 0, "Should receive shares"); + assertEq(metaVault.balanceOf(receiver), shares, "Receiver should have shares"); + } + + function test_deposit_nonWhitelistedSender() public { + uint256 depositAmount = 10 ether; + + vm.prank(nonWhitelistedUser); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.deposit{value: depositAmount}(receiver, referrer); + } + + function test_deposit_nonWhitelistedReceiver() public { + uint256 depositAmount = 10 ether; + + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.deposit{value: depositAmount}(nonWhitelistedUser, referrer); + } + + function test_depositViaFallback_whitelistedUser() public { + uint256 depositAmount = 5 ether; + + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, sender, depositAmount, 0, address(0)); + + vm.prank(sender); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_depositViaFallback_whitelistedUser"); + Address.sendValue(payable(address(metaVault)), depositAmount); + _stopSnapshotGas(); + + assertGt(metaVault.balanceOf(sender), 0, "Sender should have shares"); + } + + function test_depositViaFallback_nonWhitelistedUser() public { + uint256 depositAmount = 5 ether; + + vm.prank(nonWhitelistedUser); + vm.expectRevert(Errors.AccessDenied.selector); + Address.sendValue(payable(address(metaVault)), depositAmount); + } + + function test_depositViaFallback_fromSubVault() public { + // First deposit to meta vault and sub vaults + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Get balance before + uint256 balanceBefore = metaVault.totalSupply(); + + // Simulate sub vault sending ETH (e.g., during claim) + // This should NOT create a deposit (and should not check whitelist) + vm.deal(subVaults[0], 1 ether); + vm.prank(subVaults[0]); + Address.sendValue(payable(address(metaVault)), 1 ether); + + // Total supply should not increase + assertEq(metaVault.totalSupply(), balanceBefore, "Total supply should not increase when sub vault sends ETH"); + } + + function test_mintOsToken_whitelistedUser() public { + // First collateralize the meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Mint osTokens + uint256 osTokenShares = depositAmount / 2; + + // Expect OsTokenMinted event + vm.expectEmit(true, false, false, false); + emit IVaultOsToken.OsTokenMinted(sender, sender, 0, osTokenShares, referrer); + + vm.prank(sender); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_mintOsToken_whitelistedUser"); + uint256 assets = metaVault.mintOsToken(sender, osTokenShares, referrer); + _stopSnapshotGas(); + + assertGt(assets, 0, "Should mint osToken assets"); + assertEq(metaVault.osTokenPositions(sender), osTokenShares, "Should have osToken position"); + } + + function test_mintOsToken_nonWhitelistedUser() public { + // First deposit with whitelisted user + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + registry.depositToSubVaults(); + + // Temporarily whitelist nonWhitelistedUser for deposit, then remove + vm.prank(whitelister); + metaVault.updateWhitelist(nonWhitelistedUser, true); + + vm.prank(nonWhitelistedUser); + metaVault.deposit{value: depositAmount}(nonWhitelistedUser, referrer); + + // Remove from whitelist + vm.prank(whitelister); + metaVault.updateWhitelist(nonWhitelistedUser, false); + + // Try to mint osToken - should fail + uint256 osTokenShares = depositAmount / 2; + vm.prank(nonWhitelistedUser); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.mintOsToken(nonWhitelistedUser, osTokenShares, referrer); + } + + function test_transfer_bothWhitelisted() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 transferAmount = 1 ether; + + // Expect Transfer event + vm.expectEmit(true, true, false, true); + emit IERC20.Transfer(sender, receiver, transferAmount); + + // Transfer shares + vm.prank(sender); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_transfer_bothWhitelisted"); + bool success = metaVault.transfer(receiver, transferAmount); + _stopSnapshotGas(); + + assertTrue(success, "Transfer should succeed"); + assertEq(metaVault.balanceOf(receiver), transferAmount, "Receiver should have shares"); + } + + function test_transfer_toNonWhitelisted() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 transferAmount = 1 ether; + + // Try to transfer to non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.transfer(nonWhitelistedUser, transferAmount); + } + + function test_transfer_fromNonWhitelisted() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Remove sender from whitelist + vm.prank(whitelister); + metaVault.updateWhitelist(sender, false); + + uint256 transferAmount = 1 ether; + + // Try to transfer from non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.transfer(receiver, transferAmount); + } + + function test_transferFrom_bothWhitelisted() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 transferAmount = 1 ether; + + // Approve spender + address spender = makeAddr("Spender"); + vm.prank(sender); + metaVault.approve(spender, transferAmount); + + // Expect Transfer event + vm.expectEmit(true, true, false, true); + emit IERC20.Transfer(sender, receiver, transferAmount); + + // Transfer from + vm.prank(spender); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_transferFrom_bothWhitelisted"); + bool success = metaVault.transferFrom(sender, receiver, transferAmount); + _stopSnapshotGas(); + + assertTrue(success, "TransferFrom should succeed"); + assertEq(metaVault.balanceOf(receiver), transferAmount, "Receiver should have shares"); + } + + function test_transferFrom_toNonWhitelisted() public { + // Deposit to get shares + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + uint256 transferAmount = 1 ether; + + // Approve spender + address spender = makeAddr("Spender"); + vm.prank(sender); + metaVault.approve(spender, transferAmount); + + // Try to transfer to non-whitelisted user + vm.prank(spender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.transferFrom(sender, nonWhitelistedUser, transferAmount); + } + + function test_updateWhitelist() public { + assertFalse(metaVault.whitelistedAccounts(nonWhitelistedUser), "Should not be whitelisted initially"); + + // Expect WhitelistUpdated event + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelistUpdated(whitelister, nonWhitelistedUser, true); + + vm.prank(whitelister); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_updateWhitelist"); + metaVault.updateWhitelist(nonWhitelistedUser, true); + _stopSnapshotGas(); + + assertTrue(metaVault.whitelistedAccounts(nonWhitelistedUser), "Should be whitelisted"); + + // Expect WhitelistUpdated event for removal + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelistUpdated(whitelister, nonWhitelistedUser, false); + + vm.prank(whitelister); + metaVault.updateWhitelist(nonWhitelistedUser, false); + + assertFalse(metaVault.whitelistedAccounts(nonWhitelistedUser), "Should not be whitelisted"); + } + + function test_updateWhitelist_onlyWhitelister() public { + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.updateWhitelist(nonWhitelistedUser, true); + } + + function test_setWhitelister() public { + address newWhitelister = makeAddr("NewWhitelister"); + + // Expect WhitelisterUpdated event + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelisterUpdated(admin, newWhitelister); + + vm.prank(admin); + _startSnapshotGas("EthPrivErc20MetaVaultTest_test_setWhitelister"); + metaVault.setWhitelister(newWhitelister); + _stopSnapshotGas(); + + assertEq(metaVault.whitelister(), newWhitelister, "Whitelister should be updated"); + } + + function test_setWhitelister_onlyAdmin() public { + address newWhitelister = makeAddr("NewWhitelister"); + + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.setWhitelister(newWhitelister); + } + + function test_setWhitelister_valueNotChanged() public { + vm.prank(admin); + vm.expectRevert(Errors.ValueNotChanged.selector); + metaVault.setWhitelister(whitelister); + } +} diff --git a/test/EthPrivMetaVault.t.sol b/test/EthPrivMetaVault.t.sol index 5b46a784..9ca04c28 100644 --- a/test/EthPrivMetaVault.t.sol +++ b/test/EthPrivMetaVault.t.sol @@ -7,8 +7,11 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {IEthPrivMetaVault} from "../contracts/interfaces/IEthPrivMetaVault.sol"; import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; import {IEthVault} from "../contracts/interfaces/IEthVault.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; import {IVaultSubVaults} from "../contracts/interfaces/IVaultSubVaults.sol"; +import {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; +import {IVaultOsToken} from "../contracts/interfaces/IVaultOsToken.sol"; +import {IVaultWhitelist} from "../contracts/interfaces/IVaultWhitelist.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; import {Errors} from "../contracts/libraries/Errors.sol"; import {EthPrivMetaVault} from "../contracts/vaults/ethereum/EthPrivMetaVault.sol"; @@ -17,6 +20,7 @@ import {EthHelpers} from "./helpers/EthHelpers.sol"; contract EthPrivMetaVaultTest is Test, EthHelpers { ForkContracts public contracts; EthPrivMetaVault public metaVault; + ISubVaultsRegistry public registry; address public admin; address public sender; @@ -47,7 +51,7 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { // Deploy private meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: _balancedCurator, capacity: type(uint256).max, feePercent: 0, @@ -56,8 +60,11 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { ); metaVault = EthPrivMetaVault(payable(_getOrCreateVault(VaultType.EthPrivMetaVault, admin, initParams, false))); + // Get registry reference + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Get existing sub vaults (if any) - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < currentSubVaults.length; i++) { subVaults.push(currentSubVaults[i]); } @@ -69,7 +76,7 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } } @@ -104,13 +111,13 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { assertEq(metaVault.version(), 6, "Incorrect version"); assertEq(metaVault.admin(), admin, "Incorrect admin"); assertEq(metaVault.whitelister(), admin, "Whitelister should be admin initially"); - assertEq(metaVault.subVaultsCurator(), _balancedCurator, "Incorrect curator"); + assertEq(registry.subVaultsCurator(), _balancedCurator, "Incorrect curator"); assertEq(metaVault.capacity(), type(uint256).max, "Incorrect capacity"); assertEq(metaVault.feePercent(), 0, "Incorrect fee percent"); assertEq(metaVault.feeRecipient(), admin, "Incorrect fee recipient"); // Verify sub vaults - address[] memory storedSubVaults = metaVault.getSubVaults(); + address[] memory storedSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < subVaults.length; i++) { assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); } @@ -169,6 +176,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { metaVault.updateWhitelist(receiver, true); vm.stopPrank(); + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, receiver, amount, expectedShares, referrer); + // Deposit as whitelisted user vm.prank(sender); _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositAsWhitelistedUser"); @@ -207,6 +218,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.prank(whitelister); metaVault.updateWhitelist(sender, true); + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, sender, amount, expectedShares, address(0)); + // Deposit using receive function as whitelisted user vm.prank(sender); _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositUsingReceiveAsWhitelistedUser"); @@ -230,7 +245,7 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { uint256 depositAmount = 10 ether; vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Sub vault is NOT whitelisted assertFalse(metaVault.whitelistedAccounts(subVaults[0]), "Sub vault should not be whitelisted"); @@ -268,7 +283,7 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Deposit to sub vaults to collateralize - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Remove sender from whitelist vm.prank(whitelister); @@ -296,10 +311,15 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Deposit to sub vaults to collateralize - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Mint osToken as whitelisted user uint256 osTokenShares = depositAmount / 2; + + // Expect OsTokenMinted event + vm.expectEmit(true, false, false, false); + emit IVaultOsToken.OsTokenMinted(sender, sender, 0, osTokenShares, referrer); + vm.prank(sender); _startSnapshotGas("EthPrivMetaVaultTest_test_canMintOsTokenAsWhitelistedUser"); uint256 assets = metaVault.mintOsToken(sender, osTokenShares, referrer); @@ -325,7 +345,7 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.prank(admin); metaVault.deposit{value: depositAmount}(admin, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Whitelist receiver but not sender vm.prank(whitelister); @@ -350,10 +370,17 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.prank(sender); metaVault.deposit{value: depositAmount}(sender, referrer); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Deposit and mint osToken as whitelisted user uint256 osTokenShares = depositAmount / 2; + + // Expect Deposited and OsTokenMinted events + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, sender, depositAmount, 0, referrer); + vm.expectEmit(true, false, false, false); + emit IVaultOsToken.OsTokenMinted(sender, sender, 0, osTokenShares, referrer); + vm.prank(sender); _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositAndMintOsTokenAsWhitelistedUser"); uint256 assets = metaVault.depositAndMintOsToken{value: depositAmount}(sender, osTokenShares, referrer); @@ -415,6 +442,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.expectRevert(Errors.AccessDenied.selector); metaVault.setWhitelister(newWhitelister); + // Expect WhitelisterUpdated event + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelisterUpdated(admin, newWhitelister); + // Admin can set whitelister vm.prank(admin); _startSnapshotGas("EthPrivMetaVaultTest_test_setWhitelister"); @@ -442,6 +473,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.expectRevert(Errors.AccessDenied.selector); metaVault.updateWhitelist(sender, true); + // Expect WhitelistUpdated event + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelistUpdated(whitelister, sender, true); + // Whitelister can update whitelist vm.prank(whitelister); _startSnapshotGas("EthPrivMetaVaultTest_test_updateWhitelist"); @@ -450,6 +485,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { assertTrue(metaVault.whitelistedAccounts(sender), "Account not whitelisted correctly"); + // Expect WhitelistUpdated event for removal + vm.expectEmit(true, true, false, true); + emit IVaultWhitelist.WhitelistUpdated(whitelister, sender, false); + // Whitelister can remove from whitelist vm.prank(whitelister); metaVault.updateWhitelist(sender, false); @@ -533,19 +572,20 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); // Get sub vault states before deposit - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaults.length); + ISubVaultsRegistry.SubVaultState[] memory initialStates = + new ISubVaultsRegistry.SubVaultState[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - initialStates[i] = metaVault.subVaultsStates(subVaults[i]); + initialStates[i] = registry.subVaultsStates(subVaults[i]); } // Deposit to sub vaults (anyone can call this) _startSnapshotGas("EthPrivMetaVaultTest_test_depositToSubVaultsWorksWithWhitelistedUser"); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _stopSnapshotGas(); // Verify sub vault balances increased for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(subVaults[i]); assertGt(finalState.stakedShares, initialStates[i].stakedShares, "Sub vault staked shares should increase"); } } @@ -570,6 +610,10 @@ contract EthPrivMetaVaultTest is Test, EthHelpers { vm.prank(whitelister); metaVault.updateWhitelist(sender, false); + // Expect ExitQueueEntered event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.ExitQueueEntered(sender, sender, 0, senderShares); + // Should still be able to exit (enter exit queue) vm.prank(sender); _startSnapshotGas("EthPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval"); diff --git a/test/SubVaultsRegistry.t.sol b/test/SubVaultsRegistry.t.sol new file mode 100644 index 00000000..b1e2612c --- /dev/null +++ b/test/SubVaultsRegistry.t.sol @@ -0,0 +1,1184 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; +import {IGnoMetaVault} from "../contracts/interfaces/IGnoMetaVault.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; +import {ISubVaultsCurator} from "../contracts/interfaces/ISubVaultsCurator.sol"; +import {Errors} from "../contracts/libraries/Errors.sol"; +import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; +import {GnoMetaVault} from "../contracts/vaults/gnosis/GnoMetaVault.sol"; +import {SubVaultsRegistry} from "../contracts/vaults/SubVaultsRegistry.sol"; +import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; +import {CuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; +import {EthHelpers} from "./helpers/EthHelpers.sol"; +import {GnoHelpers} from "./helpers/GnoHelpers.sol"; + +/// @dev Legacy interface for meta vaults that had sub-vault functions directly on the vault contract +interface ILegacyMetaVault { + struct SubVaultState { + uint128 stakedShares; + uint128 queuedShares; + } + + function subVaultsCurator() external view returns (address); + function subVaultsRewardsNonce() external view returns (uint128); + function getSubVaults() external view returns (address[] memory); + function subVaultsStates(address vault) external view returns (SubVaultState memory); +} + +/// @title SubVaultsRegistryTest +/// @notice Tests for SubVaultsRegistry contract +contract SubVaultsRegistryTest is Test, EthHelpers { + ForkContracts public contracts; + EthMetaVault public metaVault; + ISubVaultsRegistry public registry; + address public admin; + address public curator; + address[] public subVaults; + + function setUp() public { + contracts = _activateEthereumFork(); + + admin = makeAddr("Admin"); + vm.deal(admin, 100 ether); + + curator = address(new BalancedCurator()); + vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); + CuratorsRegistry(_curatorsRegistry).addCurator(curator); + + bytes memory initParams = abi.encode( + IEthMetaVault.EthMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + + for (uint256 i = 0; i < 2; i++) { + address subVault = _createEthSubVault(admin); + _collateralizeEthVault(subVault); + subVaults.push(subVault); + + vm.prank(admin); + registry.addSubVault(subVault); + } + } + + function _deployNewRegistryImpl() internal returns (SubVaultsRegistry) { + return new SubVaultsRegistry( + _curatorsRegistry, + address(contracts.vaultsRegistry), + address(contracts.keeper), + address(contracts.osTokenVaultController), + address(contracts.osTokenConfig) + ); + } + + function _deployRegistryProxy(SubVaultsRegistry impl) internal returns (SubVaultsRegistry) { + address proxy = address(new ERC1967Proxy(address(impl), "")); + return SubVaultsRegistry(proxy); + } + + /// @notice Test _authorizeUpgrade reverts when called by non-metaVault + function test_authorizeUpgrade_notMetaVault() public { + address randomCaller = makeAddr("RandomCaller"); + address newImplementation = address(_deployNewRegistryImpl()); + + vm.prank(randomCaller); + vm.expectRevert(Errors.AccessDenied.selector); + UUPSUpgradeable(address(registry)).upgradeToAndCall(newImplementation, ""); + } + + /// @notice Test _authorizeUpgrade succeeds when called via metaVault + function test_authorizeUpgrade_success() public { + address newImplementation = address(_deployNewRegistryImpl()); + + vm.prank(address(metaVault)); + UUPSUpgradeable(address(registry)).upgradeToAndCall(newImplementation, ""); + + assertEq(registry.metaVault(), address(metaVault)); + } + + /// @notice Test initialize reverts with zero address for metaVault + function test_initialize_zeroMetaVault() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + vm.expectRevert(Errors.ZeroAddress.selector); + registryProxy.initialize(address(0), curator); + } + + /// @notice Test migrate function with basic data + function test_migrate_basic() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](2); + migrateSubVaults[0] = subVaults[0]; + migrateSubVaults[1] = subVaults[1]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](2); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 100 ether, queuedShares: 10 ether}); + states[1] = ISubVaultsRegistry.SubVaultState({stakedShares: 200 ether, queuedShares: 20 ether}); + + bytes32[][] memory exits = new bytes32[][](2); + exits[0] = new bytes32[](0); + exits[1] = new bytes32[](0); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 1000 ether, + totalProcessedExitQueueTickets: 50, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + vm.expectEmit(true, false, false, false); + emit ISubVaultsRegistry.Migrated(address(this)); + + registryProxy.migrate(data); + + assertEq(registryProxy.metaVault(), address(this)); + assertEq(registryProxy.subVaultsCurator(), curator); + assertEq(registryProxy.subVaultsRewardsNonce(), 100); + assertEq(registryProxy.subVaultsTotalAssets(), 1000 ether); + + address[] memory registeredSubVaults = registryProxy.getSubVaults(); + assertEq(registeredSubVaults.length, 2); + assertEq(registeredSubVaults[0], subVaults[0]); + assertEq(registeredSubVaults[1], subVaults[1]); + + ISubVaultsRegistry.SubVaultState memory state0 = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state0.stakedShares, 100 ether); + assertEq(state0.queuedShares, 10 ether); + + ISubVaultsRegistry.SubVaultState memory state1 = registryProxy.subVaultsStates(subVaults[1]); + assertEq(state1.stakedShares, 200 ether); + assertEq(state1.queuedShares, 20 ether); + + // Verify exits are empty for both sub-vaults + bytes32[] memory exits0 = registryProxy.subVaultsExits(subVaults[0]); + assertEq(exits0.length, 0, "SubVault0 should have no exits"); + bytes32[] memory exits1 = registryProxy.subVaultsExits(subVaults[1]); + assertEq(exits1.length, 0, "SubVault1 should have no exits"); + } + + /// @notice Test migrate with ejecting sub-vault + function test_migrate_withEjectingSubVault() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](1); + migrateSubVaults[0] = subVaults[0]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](1); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 0, queuedShares: 50 ether}); + + bytes32[][] memory exits = new bytes32[][](1); + exits[0] = new bytes32[](0); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: subVaults[0], + ejectingSubVaultShares: 50 ether, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 500 ether, + totalProcessedExitQueueTickets: 25, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + assertEq(registryProxy.ejectingSubVault(), subVaults[0]); + assertEq(registryProxy.ejectingSubVaultShares(), 50 ether); + } + + /// @notice Test migrate with exit positions + function test_migrate_withExits() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](1); + migrateSubVaults[0] = subVaults[0]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](1); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 100 ether, queuedShares: 50 ether}); + + // Create some exit positions using proper encoding + bytes32[][] memory exits = new bytes32[][](1); + exits[0] = new bytes32[](2); + exits[0][0] = _packExit(1000, 25 ether); // First exit: ticket=1000, shares=25 ether + exits[0][1] = _packExit(2000, 25 ether); // Second exit: ticket=2000, shares=25 ether + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 500 ether, + totalProcessedExitQueueTickets: 25, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify migration was successful + assertEq(registryProxy.metaVault(), address(this)); + ISubVaultsRegistry.SubVaultState memory state = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state.queuedShares, 50 ether); + + // Verify exits were migrated correctly + bytes32[] memory migratedExits = registryProxy.subVaultsExits(subVaults[0]); + assertEq(migratedExits.length, 2, "Should have 2 exits"); + assertEq(migratedExits[0], exits[0][0], "First exit should match"); + assertEq(migratedExits[1], exits[0][1], "Second exit should match"); + } + + /// @notice Test migrate with multiple sub-vaults each having exits + function test_migrate_multipleSubVaultsWithExits() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](2); + migrateSubVaults[0] = subVaults[0]; + migrateSubVaults[1] = subVaults[1]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](2); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 50 ether, queuedShares: 30 ether}); + states[1] = ISubVaultsRegistry.SubVaultState({stakedShares: 75 ether, queuedShares: 45 ether}); + + // Create exit positions for both sub-vaults + bytes32[][] memory exits = new bytes32[][](2); + // Sub-vault 0: 2 exits + exits[0] = new bytes32[](2); + exits[0][0] = _packExit(100, 15 ether); + exits[0][1] = _packExit(200, 15 ether); + // Sub-vault 1: 3 exits + exits[1] = new bytes32[](3); + exits[1][0] = _packExit(300, 15 ether); + exits[1][1] = _packExit(400, 15 ether); + exits[1][2] = _packExit(500, 15 ether); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 200 ether, + totalProcessedExitQueueTickets: 0, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify states were migrated correctly + ISubVaultsRegistry.SubVaultState memory state0 = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state0.stakedShares, 50 ether, "SubVault0 staked shares mismatch"); + assertEq(state0.queuedShares, 30 ether, "SubVault0 queued shares mismatch"); + + ISubVaultsRegistry.SubVaultState memory state1 = registryProxy.subVaultsStates(subVaults[1]); + assertEq(state1.stakedShares, 75 ether, "SubVault1 staked shares mismatch"); + assertEq(state1.queuedShares, 45 ether, "SubVault1 queued shares mismatch"); + + // Verify sub-vaults list + address[] memory registeredSubVaults = registryProxy.getSubVaults(); + assertEq(registeredSubVaults.length, 2, "Should have 2 sub-vaults"); + + // Verify exits were migrated correctly for sub-vault 0 + bytes32[] memory exits0 = registryProxy.subVaultsExits(subVaults[0]); + assertEq(exits0.length, 2, "SubVault0 should have 2 exits"); + assertEq(exits0[0], _packExit(100, 15 ether), "SubVault0 first exit should match"); + assertEq(exits0[1], _packExit(200, 15 ether), "SubVault0 second exit should match"); + + // Verify exits were migrated correctly for sub-vault 1 + bytes32[] memory exits1 = registryProxy.subVaultsExits(subVaults[1]); + assertEq(exits1.length, 3, "SubVault1 should have 3 exits"); + assertEq(exits1[0], _packExit(300, 15 ether), "SubVault1 first exit should match"); + assertEq(exits1[1], _packExit(400, 15 ether), "SubVault1 second exit should match"); + assertEq(exits1[2], _packExit(500, 15 ether), "SubVault1 third exit should match"); + } + + /// @notice Test migrate with ejecting sub-vault that has exits + function test_migrate_ejectingSubVaultWithExits() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](2); + migrateSubVaults[0] = subVaults[0]; + migrateSubVaults[1] = subVaults[1]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](2); + // Sub-vault 0 is being ejected (no staked shares, only queued) + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 0, queuedShares: 100 ether}); + states[1] = ISubVaultsRegistry.SubVaultState({stakedShares: 200 ether, queuedShares: 0}); + + bytes32[][] memory exits = new bytes32[][](2); + // Ejecting sub-vault has exits + exits[0] = new bytes32[](2); + exits[0][0] = _packExit(1000, 60 ether); + exits[0][1] = _packExit(2000, 40 ether); + // Non-ejecting sub-vault has no exits + exits[1] = new bytes32[](0); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: subVaults[0], + ejectingSubVaultShares: 100 ether, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 300 ether, + totalProcessedExitQueueTickets: 50, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify ejecting sub-vault state + assertEq(registryProxy.ejectingSubVault(), subVaults[0], "Ejecting sub-vault mismatch"); + assertEq(registryProxy.ejectingSubVaultShares(), 100 ether, "Ejecting shares mismatch"); + + ISubVaultsRegistry.SubVaultState memory state0 = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state0.queuedShares, 100 ether, "Ejecting sub-vault queued shares mismatch"); + + // Verify ejecting sub-vault exits were migrated correctly + bytes32[] memory ejectingExits = registryProxy.subVaultsExits(subVaults[0]); + assertEq(ejectingExits.length, 2, "Ejecting sub-vault should have 2 exits"); + assertEq(ejectingExits[0], _packExit(1000, 60 ether), "Ejecting sub-vault first exit should match"); + assertEq(ejectingExits[1], _packExit(2000, 40 ether), "Ejecting sub-vault second exit should match"); + + // Verify non-ejecting sub-vault has no exits + bytes32[] memory nonEjectingExits = registryProxy.subVaultsExits(subVaults[1]); + assertEq(nonEjectingExits.length, 0, "Non-ejecting sub-vault should have no exits"); + } + + /// @notice Test migrate preserves exit order (FIFO) + function test_migrate_exitsPreserveOrder() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](1); + migrateSubVaults[0] = subVaults[0]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](1); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 0, queuedShares: 100 ether}); + + // Create exits with specific ticket numbers to verify order + bytes32[][] memory exits = new bytes32[][](1); + exits[0] = new bytes32[](4); + exits[0][0] = _packExit(111, 25 ether); // First in queue + exits[0][1] = _packExit(222, 25 ether); + exits[0][2] = _packExit(333, 25 ether); + exits[0][3] = _packExit(444, 25 ether); // Last in queue + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 100 ether, + totalProcessedExitQueueTickets: 0, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify state + ISubVaultsRegistry.SubVaultState memory state = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state.queuedShares, 100 ether, "Queued shares mismatch"); + + // Verify exits preserve order (FIFO) + bytes32[] memory migratedExits = registryProxy.subVaultsExits(subVaults[0]); + assertEq(migratedExits.length, 4, "Should have 4 exits"); + assertEq(migratedExits[0], _packExit(111, 25 ether), "First exit (ticket=111) should be first"); + assertEq(migratedExits[1], _packExit(222, 25 ether), "Second exit (ticket=222) should be second"); + assertEq(migratedExits[2], _packExit(333, 25 ether), "Third exit (ticket=333) should be third"); + assertEq(migratedExits[3], _packExit(444, 25 ether), "Fourth exit (ticket=444) should be last"); + } + + /// @notice Test migrate with maximum number of exits + function test_migrate_manyExits() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](1); + migrateSubVaults[0] = subVaults[0]; + + uint256 numExits = 50; + uint96 sharesPerExit = 2 ether; + uint128 totalQueuedShares = uint128(numExits * sharesPerExit); + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](1); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 0, queuedShares: totalQueuedShares}); + + bytes32[][] memory exits = new bytes32[][](1); + exits[0] = new bytes32[](numExits); + for (uint256 i = 0; i < numExits; i++) { + exits[0][i] = _packExit(uint160(i * 1000), sharesPerExit); + } + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: totalQueuedShares, + totalProcessedExitQueueTickets: 0, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify state + ISubVaultsRegistry.SubVaultState memory state = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state.queuedShares, totalQueuedShares, "Queued shares mismatch after many exits migration"); + + // Verify all exits were migrated correctly + bytes32[] memory migratedExits = registryProxy.subVaultsExits(subVaults[0]); + assertEq(migratedExits.length, numExits, "Should have all exits migrated"); + for (uint256 i = 0; i < numExits; i++) { + assertEq(migratedExits[i], _packExit(uint160(i * 1000), sharesPerExit), "Exit should match at index"); + } + } + + /// @notice Helper function to pack exit data (positionTicket + shares) + function _packExit(uint160 positionTicket, uint96 shares) internal pure returns (bytes32) { + return Packing.pack_20_12(bytes20(positionTicket), bytes12(shares)); + } + + /// @notice Test migrate reverts when called twice + function test_migrate_alreadyInitialized() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](0); + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](0); + bytes32[][] memory exits = new bytes32[][](0); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 0, + totalProcessedExitQueueTickets: 0, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + // First migrate succeeds + registryProxy.migrate(data); + + // Second migrate should fail (already initialized) + vm.expectRevert(); + registryProxy.migrate(data); + } + + /// @notice Test canUpdateState returns correct values + function test_canUpdateState() public { + // Registry should have a valid rewards nonce + uint128 currentNonce = registry.subVaultsRewardsNonce(); + assertTrue(currentNonce > 0, "Rewards nonce should be set"); + + // Get current keeper nonce + uint64 keeperNonce = contracts.keeper.rewardsNonce(); + + // If keeper nonce is ahead of registry nonce, canUpdateState should be true + if (keeperNonce > currentNonce) { + assertTrue(registry.canUpdateState()); + } else { + assertFalse(registry.canUpdateState()); + } + } + + /// @notice Test isSubVault returns correct values + function test_isSubVault() public { + // Registered sub-vaults should return true + assertTrue(registry.isSubVault(subVaults[0])); + assertTrue(registry.isSubVault(subVaults[1])); + + // Non-registered addresses should return false + assertFalse(registry.isSubVault(address(0))); + assertFalse(registry.isSubVault(makeAddr("RandomAddress"))); + } + + /// @notice Test isCollateralized returns correct values + function test_isCollateralized() public { + // Registry with sub-vaults should be collateralized + assertTrue(registry.isCollateralized()); + + // Create a new meta vault without sub-vaults + bytes memory initParams = abi.encode( + IEthMetaVault.EthMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault emptyMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry emptyRegistry = ISubVaultsRegistry(emptyMetaVault.subVaultsRegistry()); + + // Empty registry should not be collateralized + assertFalse(emptyRegistry.isCollateralized()); + } + + /// @notice Test harvestSubVaultsAssets reverts when called by non-metaVault + function test_harvestSubVaultsAssets_notMetaVault() public { + address randomCaller = makeAddr("RandomCaller"); + + vm.prank(randomCaller); + vm.expectRevert(Errors.AccessDenied.selector); + registry.harvestSubVaultsAssets(); + } + + /// @notice Test enterSubVaultsExitQueue reverts when called by non-metaVault + function test_enterSubVaultsExitQueue_notMetaVault() public { + address randomCaller = makeAddr("RandomCaller"); + + vm.prank(randomCaller); + vm.expectRevert(Errors.AccessDenied.selector); + registry.enterSubVaultsExitQueue(); + } + + /// @notice Test redeemSubVaultsAssets reverts when called by non-redeemer + function test_redeemSubVaultsAssets_notRedeemer() public { + address randomCaller = makeAddr("RandomCaller"); + + vm.prank(randomCaller); + vm.expectRevert(Errors.AccessDenied.selector); + registry.redeemSubVaultsAssets(100 ether); + } + + /// @notice Test redeemSubVaultsAssets reverts with zero assets + function test_redeemSubVaultsAssets_zeroAssets() public { + address redeemer = contracts.osTokenConfig.redeemer(); + + vm.prank(redeemer); + vm.expectRevert(Errors.InvalidAssets.selector); + registry.redeemSubVaultsAssets(0); + } + + /// @notice Test calculateSubVaultsRedemptions returns empty when sufficient withdrawable assets + function test_calculateSubVaultsRedemptions_sufficientWithdrawable() public { + // Deposit to meta vault to create withdrawable assets + vm.prank(admin); + metaVault.deposit{value: 10 ether}(admin, address(0)); + + // Calculate redemptions for amount less than withdrawable + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(1 ether); + + // Should return empty array since withdrawable assets are sufficient + assertEq(requests.length, 0, "Should return empty array when withdrawable assets sufficient"); + } + + /// @notice Test depositToSubVaults reverts when not harvested + function test_depositToSubVaults_notHarvested() public { + // Advance the keeper nonce to make the registry not harvested + uint64 currentNonce = contracts.keeper.rewardsNonce(); + _setKeeperRewardsNonce(currentNonce + 2); + + vm.expectRevert(Errors.NotHarvested.selector); + registry.depositToSubVaults(); + } + + /// @notice Test depositToSubVaults reverts with no available assets + function test_depositToSubVaults_noAssets() public { + // Ensure registry is harvested + uint64 currentNonce = contracts.keeper.rewardsNonce(); + _setKeeperRewardsNonce(currentNonce + 1); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], currentNonce + 1); + } + metaVault.updateState(_getEmptyHarvestParams()); + + // Simulate security deposit being staked by setting vault balance to 0 + vm.deal(address(metaVault), 0); + + // Try to deposit with no available assets + vm.expectRevert(Errors.InvalidAssets.selector); + registry.depositToSubVaults(); + } + + /// @notice Test depositToSubVaults success + function test_depositToSubVaults_success() public { + // Deposit to meta vault + vm.prank(admin); + metaVault.deposit{value: 10 ether}(admin, address(0)); + + // Ensure registry is harvested + uint64 currentNonce = contracts.keeper.rewardsNonce(); + _setKeeperRewardsNonce(currentNonce + 1); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], currentNonce + 1); + } + metaVault.updateState(_getEmptyHarvestParams()); + + // Deposit to sub-vaults + registry.depositToSubVaults(); + + // Verify sub-vaults received deposits + uint256 totalStaked; + for (uint256 i = 0; i < subVaults.length; i++) { + ISubVaultsRegistry.SubVaultState memory state = registry.subVaultsStates(subVaults[i]); + totalStaked += state.stakedShares; + } + assertGt(totalStaked, 0, "Sub-vaults should have staked shares"); + } + + /// @notice Test calculateSubVaultsRedemptions with ejecting sub-vault + function test_calculateSubVaultsRedemptions_withEjectingSubVault() public { + // Deposit to meta vault + vm.prank(admin); + metaVault.deposit{value: 10 ether}(admin, address(0)); + + // Harvest and deposit to sub-vaults + uint64 currentNonce = contracts.keeper.rewardsNonce(); + _setKeeperRewardsNonce(currentNonce + 1); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], currentNonce + 1); + } + metaVault.updateState(_getEmptyHarvestParams()); + registry.depositToSubVaults(); + + // Eject a sub-vault + vm.prank(admin); + registry.ejectSubVault(subVaults[0]); + + // Harvest after ejection - need to set nonce ahead by 1 only + uint128 registryNonce = registry.subVaultsRewardsNonce(); + _setKeeperRewardsNonce(uint64(registryNonce + 1)); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], uint64(registryNonce + 1)); + } + metaVault.updateState(_getEmptyHarvestParams()); + + // Calculate redemptions - should consider ejecting sub-vault shares + ISubVaultsCurator.ExitRequest[] memory requests = registry.calculateSubVaultsRedemptions(1 ether); + + // Result depends on withdrawable assets, may be empty if withdrawable is sufficient + assertTrue(true, "calculateSubVaultsRedemptions should not revert with ejecting sub-vault"); + } + + /// @notice Test migrate with empty exits arrays + function test_migrate_emptyExitsPerSubVault() public { + SubVaultsRegistry registryProxy = _deployRegistryProxy(_deployNewRegistryImpl()); + + address[] memory migrateSubVaults = new address[](2); + migrateSubVaults[0] = subVaults[0]; + migrateSubVaults[1] = subVaults[1]; + + ISubVaultsRegistry.SubVaultState[] memory states = new ISubVaultsRegistry.SubVaultState[](2); + states[0] = ISubVaultsRegistry.SubVaultState({stakedShares: 100 ether, queuedShares: 0}); + states[1] = ISubVaultsRegistry.SubVaultState({stakedShares: 100 ether, queuedShares: 0}); + + // Empty exits for both sub-vaults + bytes32[][] memory exits = new bytes32[][](2); + exits[0] = new bytes32[](0); + exits[1] = new bytes32[](0); + + ISubVaultsRegistry.MigrationData memory data = ISubVaultsRegistry.MigrationData({ + curator: curator, + ejectingSubVault: address(0), + ejectingSubVaultShares: 0, + subVaultsRewardsNonce: 100, + subVaultsTotalAssets: 200 ether, + totalProcessedExitQueueTickets: 0, + subVaults: migrateSubVaults, + subVaultsStates: states, + subVaultsExits: exits + }); + + registryProxy.migrate(data); + + // Verify states + ISubVaultsRegistry.SubVaultState memory state0 = registryProxy.subVaultsStates(subVaults[0]); + assertEq(state0.stakedShares, 100 ether, "SubVault0 staked shares mismatch"); + assertEq(state0.queuedShares, 0, "SubVault0 should have no queued shares"); + + // Verify exits are empty for both sub-vaults + bytes32[] memory exits0 = registryProxy.subVaultsExits(subVaults[0]); + assertEq(exits0.length, 0, "SubVault0 should have no exits"); + bytes32[] memory exits1 = registryProxy.subVaultsExits(subVaults[1]); + assertEq(exits1.length, 0, "SubVault1 should have no exits"); + } + + /// @notice Test claimSubVaultsExitedAssets reverts with invalid data + function test_claimSubVaultsExitedAssets_emptyRequests() public { + ISubVaultsRegistry.SubVaultExitRequest[] memory exitRequests = new ISubVaultsRegistry.SubVaultExitRequest[](0); + + // Empty requests should not revert but do nothing + registry.claimSubVaultsExitedAssets(exitRequests); + } +} + +/// @title VaultSubVaultsUpgradeEthTest +/// @notice Tests for __VaultSubVaults_upgrade function on Ethereum +contract VaultSubVaultsUpgradeEthTest is Test, EthHelpers { + // Existing Ethereum meta vault address for fork testing + address private constant FORK_ETH_META_VAULT = 0x34284C27A2304132aF751b0dEc5bBa2CF98eD039; + + // Pre-upgrade state storage + struct PreUpgradeState { + address curator; + uint128 rewardsNonce; + address[] subVaults; + } + + ForkContracts public contracts; + PreUpgradeState public preUpgradeState; + mapping(address => ISubVaultsRegistry.SubVaultState) public preUpgradeSubVaultStates; + + function setUp() public { + contracts = _activateEthereumFork(); + } + + /// @notice Captures the pre-upgrade state from the existing v5 meta vault + function _capturePreUpgradeState(address vault) internal { + ILegacyMetaVault legacyVault = ILegacyMetaVault(vault); + + preUpgradeState.curator = legacyVault.subVaultsCurator(); + preUpgradeState.rewardsNonce = legacyVault.subVaultsRewardsNonce(); + preUpgradeState.subVaults = legacyVault.getSubVaults(); + + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + address subVault = preUpgradeState.subVaults[i]; + ILegacyMetaVault.SubVaultState memory legacyState = legacyVault.subVaultsStates(subVault); + preUpgradeSubVaultStates[subVault] = ISubVaultsRegistry.SubVaultState({ + stakedShares: legacyState.stakedShares, queuedShares: legacyState.queuedShares + }); + } + } + + /// @notice Test upgrade of existing mainnet meta vault preserves all state + function test_upgrade_existingMainnetVault_preservesState() public { + // Skip if not using fork vaults + if (!vm.envBool("TEST_USE_FORK_VAULTS")) { + return; + } + + EthMetaVault vault = EthMetaVault(payable(FORK_ETH_META_VAULT)); + + // Verify vault is at version 5 before upgrade + assertEq(vault.version(), 5, "Fork vault should be version 5 before upgrade"); + + // Capture pre-upgrade state + _capturePreUpgradeState(FORK_ETH_META_VAULT); + + // Perform upgrade + _upgradeVault(VaultType.EthMetaVault, FORK_ETH_META_VAULT); + + // Verify version was upgraded + assertEq(vault.version(), 6, "Vault should be version 6 after upgrade"); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify SubVaultsRegistry was created + assertTrue(address(registry) != address(0), "SubVaultsRegistry should be created"); + + // Verify curator was migrated + assertEq(registry.subVaultsCurator(), preUpgradeState.curator, "Curator should be preserved"); + + // Verify rewards nonce was migrated + assertEq(registry.subVaultsRewardsNonce(), preUpgradeState.rewardsNonce, "Rewards nonce should be preserved"); + + // Verify sub-vaults list was migrated + address[] memory postSubVaults = registry.getSubVaults(); + assertEq(postSubVaults.length, preUpgradeState.subVaults.length, "Sub-vaults count should be preserved"); + + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + assertEq(postSubVaults[i], preUpgradeState.subVaults[i], "Sub-vault address should be preserved"); + + // Verify sub-vault state was migrated + ISubVaultsRegistry.SubVaultState memory postState = registry.subVaultsStates(preUpgradeState.subVaults[i]); + ISubVaultsRegistry.SubVaultState memory preState = preUpgradeSubVaultStates[preUpgradeState.subVaults[i]]; + + assertEq(postState.stakedShares, preState.stakedShares, "Staked shares should be preserved"); + assertEq(postState.queuedShares, preState.queuedShares, "Queued shares should be preserved"); + } + + // Verify registry is functional by checking metaVault reference + assertEq(registry.metaVault(), FORK_ETH_META_VAULT, "Registry metaVault should point to the meta vault"); + } + + /// @notice Test upgrade of existing mainnet meta vault - vault remains functional after upgrade + function test_upgrade_existingMainnetVault_remainsFunctional() public { + // Skip if not using fork vaults + if (!vm.envBool("TEST_USE_FORK_VAULTS")) { + return; + } + + EthMetaVault vault = EthMetaVault(payable(FORK_ETH_META_VAULT)); + + // Capture pre-upgrade state + _capturePreUpgradeState(FORK_ETH_META_VAULT); + + // Perform upgrade + _upgradeVault(VaultType.EthMetaVault, FORK_ETH_META_VAULT); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify vault can still accept deposits + address depositor = makeAddr("Depositor"); + vm.deal(depositor, 10 ether); + + uint256 totalSharesBefore = vault.totalShares(); + uint256 depositAmount = 1 ether; + + vm.prank(depositor); + uint256 shares = vault.deposit{value: depositAmount}(depositor, address(0)); + + assertGt(shares, 0, "Deposit should return shares"); + assertEq(vault.getShares(depositor), shares, "Depositor should have shares"); + assertEq(vault.totalShares(), totalSharesBefore + shares, "Total shares should increase"); + + // Verify state update works (if sub-vaults exist) + if (preUpgradeState.subVaults.length > 0) { + // Increment nonces for sub-vaults + uint64 newNonce = contracts.keeper.rewardsNonce() + 1; + _setKeeperRewardsNonce(newNonce); + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + _setVaultRewardsNonce(preUpgradeState.subVaults[i], newNonce); + } + + // State update should succeed + vault.updateState(_getEmptyHarvestParams()); + + // Verify rewards nonce was updated + assertEq(registry.subVaultsRewardsNonce(), newNonce, "Rewards nonce should be updated"); + } + } + + /// @notice Test upgrade of newly deployed Ethereum meta vault - verifies initialization creates SubVaultsRegistry + function test_newlyDeployedVault_hasSubVaultsRegistry() public { + // Create a new meta vault (current version) + address admin = makeAddr("Admin"); + vm.deal(admin, 100 ether); + + // Create a curator + address curator = address(new BalancedCurator()); + vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); + CuratorsRegistry(_curatorsRegistry).addCurator(curator); + + bytes memory initParams = abi.encode( + IEthMetaVault.EthMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 500, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + // Create a new vault (this will be at current version, using initialization not upgrade) + address vaultAddress = _createVault(VaultType.EthMetaVault, admin, initParams, false); + EthMetaVault vault = EthMetaVault(payable(vaultAddress)); + + // Verify vault is at current version (should be 6) + assertEq(vault.version(), 6, "New vault should be version 6"); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify SubVaultsRegistry was created during initialization + assertTrue(address(registry) != address(0), "SubVaultsRegistry should be created"); + + // Verify curator was set correctly + assertEq(registry.subVaultsCurator(), curator, "Curator should be set"); + + // Verify empty sub-vaults list + address[] memory subVaults = registry.getSubVaults(); + assertEq(subVaults.length, 0, "Should have no sub-vaults"); + + // Verify vault is functional + vm.prank(admin); + uint256 shares = vault.deposit{value: 1 ether}(admin, address(0)); + assertGt(shares, 0, "Deposit should succeed"); + + // Verify registry is properly linked + assertEq(registry.metaVault(), vaultAddress, "Registry metaVault should point to vault"); + } + + /// @notice Test newly deployed Ethereum meta vault with sub-vaults + function test_newlyDeployedVault_withSubVaults_functional() public { + // Create a new meta vault (current version) + address admin = makeAddr("Admin"); + vm.deal(admin, 100 ether); + + // Create a curator + address curator = address(new BalancedCurator()); + vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); + CuratorsRegistry(_curatorsRegistry).addCurator(curator); + + bytes memory initParams = abi.encode( + IEthMetaVault.EthMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 500, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + // Create a new vault + address vaultAddress = _createVault(VaultType.EthMetaVault, admin, initParams, false); + EthMetaVault vault = EthMetaVault(payable(vaultAddress)); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Create and add sub-vaults + address[] memory subVaults = new address[](2); + for (uint256 i = 0; i < 2; i++) { + subVaults[i] = _createEthSubVault(admin); + _collateralizeEthVault(subVaults[i]); + + vm.prank(admin); + registry.addSubVault(subVaults[i]); + } + + // Verify sub-vaults were added + address[] memory registeredSubVaults = registry.getSubVaults(); + assertEq(registeredSubVaults.length, 2, "Should have 2 sub-vaults"); + + // Deposit to the meta vault + vm.prank(admin); + vault.deposit{value: 10 ether}(admin, address(0)); + + // Deposit to sub-vaults + registry.depositToSubVaults(); + + // Verify sub-vault states have assets + for (uint256 i = 0; i < subVaults.length; i++) { + ISubVaultsRegistry.SubVaultState memory state = registry.subVaultsStates(subVaults[i]); + assertGt(state.stakedShares, 0, "Sub-vault should have staked shares"); + } + + // Verify state update works + uint64 newNonce = contracts.keeper.rewardsNonce() + 1; + _setKeeperRewardsNonce(newNonce); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], newNonce); + } + + vault.updateState(_getEmptyHarvestParams()); + assertEq(registry.subVaultsRewardsNonce(), newNonce, "Rewards nonce should be updated"); + } +} + +/// @title VaultSubVaultsUpgradeGnoTest +/// @notice Tests for __VaultSubVaults_upgrade function on Gnosis network +contract VaultSubVaultsUpgradeGnoTest is Test, GnoHelpers { + // Existing Gnosis meta vault address for fork testing + address private constant FORK_GNO_META_VAULT = 0x34284C27A2304132aF751b0dEc5bBa2CF98eD039; + + // Pre-upgrade state storage + struct PreUpgradeState { + address curator; + uint128 rewardsNonce; + address[] subVaults; + } + + ForkContracts public contracts; + PreUpgradeState public preUpgradeState; + mapping(address => ISubVaultsRegistry.SubVaultState) public preUpgradeSubVaultStates; + + function setUp() public { + contracts = _activateGnosisFork(); + } + + /// @notice Captures the pre-upgrade state from the existing v3 Gnosis meta vault + function _capturePreUpgradeState(address vault) internal { + ILegacyMetaVault legacyVault = ILegacyMetaVault(vault); + + preUpgradeState.curator = legacyVault.subVaultsCurator(); + preUpgradeState.rewardsNonce = legacyVault.subVaultsRewardsNonce(); + preUpgradeState.subVaults = legacyVault.getSubVaults(); + + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + address subVault = preUpgradeState.subVaults[i]; + ILegacyMetaVault.SubVaultState memory legacyState = legacyVault.subVaultsStates(subVault); + preUpgradeSubVaultStates[subVault] = ISubVaultsRegistry.SubVaultState({ + stakedShares: legacyState.stakedShares, queuedShares: legacyState.queuedShares + }); + } + } + + /// @notice Test upgrade of existing Gnosis meta vault preserves all state + function test_upgrade_existingGnosisVault_preservesState() public { + // Skip if not using fork vaults + if (!vm.envBool("TEST_USE_FORK_VAULTS")) { + return; + } + + GnoMetaVault vault = GnoMetaVault(payable(FORK_GNO_META_VAULT)); + + // Verify vault is at version 3 before upgrade + uint256 version = vault.version(); + if (version != 3) { + return; + } + + // Capture pre-upgrade state + _capturePreUpgradeState(FORK_GNO_META_VAULT); + + // Perform upgrade + _upgradeVault(VaultType.GnoMetaVault, FORK_GNO_META_VAULT); + + // Verify version was upgraded + assertEq(vault.version(), 4, "Vault should be version 4 after upgrade"); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify SubVaultsRegistry was created + assertTrue(address(registry) != address(0), "SubVaultsRegistry should be created"); + + // Verify curator was migrated + assertEq(registry.subVaultsCurator(), preUpgradeState.curator, "Curator should be preserved"); + + // Verify rewards nonce was migrated + assertEq(registry.subVaultsRewardsNonce(), preUpgradeState.rewardsNonce, "Rewards nonce should be preserved"); + + // Verify sub-vaults list was migrated + address[] memory postSubVaults = registry.getSubVaults(); + assertEq(postSubVaults.length, preUpgradeState.subVaults.length, "Sub-vaults count should be preserved"); + + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + assertEq(postSubVaults[i], preUpgradeState.subVaults[i], "Sub-vault address should be preserved"); + + // Verify sub-vault state was migrated + ISubVaultsRegistry.SubVaultState memory postState = registry.subVaultsStates(preUpgradeState.subVaults[i]); + ISubVaultsRegistry.SubVaultState memory preState = preUpgradeSubVaultStates[preUpgradeState.subVaults[i]]; + + assertEq(postState.stakedShares, preState.stakedShares, "Staked shares should be preserved"); + assertEq(postState.queuedShares, preState.queuedShares, "Queued shares should be preserved"); + } + + // Verify registry is functional by checking metaVault reference + assertEq(registry.metaVault(), FORK_GNO_META_VAULT, "Registry metaVault should point to the meta vault"); + } + + /// @notice Test upgrade of existing Gnosis meta vault - vault remains functional after upgrade + function test_upgrade_existingGnosisVault_remainsFunctional() public { + // Skip if not using fork vaults + if (!vm.envBool("TEST_USE_FORK_VAULTS")) { + return; + } + + GnoMetaVault vault = GnoMetaVault(payable(FORK_GNO_META_VAULT)); + + // Verify vault is at version 3 before upgrade + uint256 version = vault.version(); + if (version != 3) { + return; + } + + // Capture pre-upgrade state + _capturePreUpgradeState(FORK_GNO_META_VAULT); + + // Perform upgrade + _upgradeVault(VaultType.GnoMetaVault, FORK_GNO_META_VAULT); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify vault can still accept deposits + address depositor = makeAddr("Depositor"); + _mintGnoToken(depositor, 10 ether); + + uint256 totalSharesBefore = vault.totalShares(); + uint256 depositAmount = 1 ether; + + vm.startPrank(depositor); + IERC20(address(contracts.gnoToken)).approve(address(vault), depositAmount); + uint256 shares = vault.deposit(depositAmount, depositor, address(0)); + vm.stopPrank(); + + assertGt(shares, 0, "Deposit should return shares"); + assertEq(vault.getShares(depositor), shares, "Depositor should have shares"); + assertEq(vault.totalShares(), totalSharesBefore + shares, "Total shares should increase"); + + // Verify state update works (if sub-vaults exist) + if (preUpgradeState.subVaults.length > 0) { + // Increment nonces for sub-vaults + uint64 newNonce = contracts.keeper.rewardsNonce() + 1; + _setKeeperRewardsNonce(newNonce); + for (uint256 i = 0; i < preUpgradeState.subVaults.length; i++) { + _setVaultRewardsNonce(preUpgradeState.subVaults[i], newNonce); + } + + // State update should succeed + vault.updateState(_getEmptyHarvestParams()); + + // Verify rewards nonce was updated + assertEq(registry.subVaultsRewardsNonce(), newNonce, "Rewards nonce should be updated"); + } + } + + /// @notice Test upgrade of newly deployed Gnosis meta vault with no sub-vaults + function test_upgrade_newlyDeployedGnosisVault_noSubVaults() public { + // Create a new meta vault + address admin = makeAddr("Admin"); + _mintGnoToken(admin, 100 ether); + + // Create a curator + address curator = address(new BalancedCurator()); + vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); + CuratorsRegistry(_curatorsRegistry).addCurator(curator); + + bytes memory initParams = abi.encode( + IGnoMetaVault.GnoMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 500, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + // Create a new vault (this will be at current version, not previous) + address vaultAddress = _createVault(VaultType.GnoMetaVault, admin, initParams, false); + GnoMetaVault vault = GnoMetaVault(payable(vaultAddress)); + + // Verify vault is at current version (should be 4, already upgraded in factory) + assertEq(vault.version(), 4, "New vault should be version 4"); + + // Get registry reference + ISubVaultsRegistry registry = ISubVaultsRegistry(vault.subVaultsRegistry()); + + // Verify SubVaultsRegistry was created during initialization + assertTrue(address(registry) != address(0), "SubVaultsRegistry should be created"); + + // Verify curator was set correctly + assertEq(registry.subVaultsCurator(), curator, "Curator should be set"); + + // Verify empty sub-vaults list + address[] memory subVaults = registry.getSubVaults(); + assertEq(subVaults.length, 0, "Should have no sub-vaults"); + + // Verify vault is functional + vm.startPrank(admin); + IERC20(address(contracts.gnoToken)).approve(vaultAddress, 1 ether); + uint256 shares = vault.deposit(1 ether, admin, address(0)); + vm.stopPrank(); + + assertGt(shares, 0, "Deposit should succeed"); + } +} diff --git a/test/SubVaultsRegistryFactory.t.sol b/test/SubVaultsRegistryFactory.t.sol new file mode 100644 index 00000000..6bdb580b --- /dev/null +++ b/test/SubVaultsRegistryFactory.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.22; + +import {Test} from "forge-std/Test.sol"; +import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; +import {Errors} from "../contracts/libraries/Errors.sol"; +import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; +import {SubVaultsRegistryFactory} from "../contracts/vaults/SubVaultsRegistryFactory.sol"; +import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; +import {CuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; +import {EthHelpers} from "./helpers/EthHelpers.sol"; + +/// @title SubVaultsRegistryFactoryTest +/// @notice Tests for SubVaultsRegistryFactory contract +contract SubVaultsRegistryFactoryTest is Test, EthHelpers { + ForkContracts public contracts; + SubVaultsRegistryFactory public factory; + + function setUp() public { + contracts = _activateEthereumFork(); + + // Get the deployed factory + factory = SubVaultsRegistryFactory(_subVaultsRegistryFactory); + } + + /// @notice Test createSubVaultsRegistry reverts when called by non-vault + function test_createSubVaultsRegistry_notVault() public { + address notAVault = makeAddr("NotAVault"); + + vm.prank(notAVault); + vm.expectRevert(Errors.InvalidVault.selector); + factory.createSubVaultsRegistry(); + } + + /// @notice Test createSubVaultsRegistry reverts when called by random contract + function test_createSubVaultsRegistry_randomContract() public { + // Deploy a random contract that is not registered as a vault + address randomContract = address(new RandomContract()); + + vm.prank(randomContract); + vm.expectRevert(Errors.InvalidVault.selector); + factory.createSubVaultsRegistry(); + } + + /// @notice Test implementation is set correctly + function test_implementation() public view { + assertTrue(factory.implementation() != address(0), "Implementation should be set"); + } + + /// @notice Test createSubVaultsRegistry success when called by registered vault + function test_createSubVaultsRegistry_success() public { + // Create a curator first + address curator = address(new BalancedCurator()); + vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); + CuratorsRegistry(_curatorsRegistry).addCurator(curator); + + // Create a meta vault - this internally calls createSubVaultsRegistry + address admin = makeAddr("Admin"); + vm.deal(admin, 100 ether); + + bytes memory initParams = abi.encode( + IEthMetaVault.EthMetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + // The factory is used internally when creating a meta vault + EthMetaVault metaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // Verify the SubVaultsRegistry was created + address registryAddr = metaVault.subVaultsRegistry(); + assertTrue(registryAddr != address(0), "SubVaultsRegistry should be created"); + + // Verify the registry is properly initialized + ISubVaultsRegistry registry = ISubVaultsRegistry(registryAddr); + assertEq(registry.metaVault(), address(metaVault)); + assertEq(registry.subVaultsCurator(), curator); + } +} + +/// @notice Helper contract for testing +contract RandomContract {} diff --git a/test/VaultSubVaults.t.sol b/test/VaultSubVaults.t.sol index 088a81fb..44628f8f 100644 --- a/test/VaultSubVaults.t.sol +++ b/test/VaultSubVaults.t.sol @@ -6,11 +6,11 @@ import {Vm} from "forge-std/Vm.sol"; import {IEthVault} from "../contracts/interfaces/IEthVault.sol"; import {IEthMetaVault} from "../contracts/interfaces/IEthMetaVault.sol"; import {IVaultSubVaults} from "../contracts/interfaces/IVaultSubVaults.sol"; +import {ISubVaultsRegistry} from "../contracts/interfaces/ISubVaultsRegistry.sol"; import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; import {IVaultState} from "../contracts/interfaces/IVaultState.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; import {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; -import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; import {Errors} from "../contracts/libraries/Errors.sol"; import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; @@ -28,8 +28,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { uint64 timestamp; } + function _getRegistry(address vault) internal view returns (ISubVaultsRegistry) { + return ISubVaultsRegistry(EthMetaVault(payable(vault)).subVaultsRegistry()); + } + ForkContracts public contracts; EthMetaVault public metaVault; + ISubVaultsRegistry public registry; address public admin; address public curator; @@ -52,7 +57,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -61,8 +66,11 @@ contract VaultSubVaultsTest is Test, EthHelpers { ); metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + // Get registry reference + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Get existing sub vaults (if any) from fork vault - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < currentSubVaults.length; i++) { subVaults.push(currentSubVaults[i]); } @@ -74,7 +82,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } // Deposit funds to meta vault @@ -94,24 +102,24 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Expect revert when a non-admin tries to set the curator vm.prank(nonAdmin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.setSubVaultsCurator(newCurator); + registry.setSubVaultsCurator(newCurator); } function test_setSubVaultsCurator_zeroAddress() public { // Action & Assert: Expect revert when trying to set the curator to the zero address vm.prank(admin); vm.expectRevert(Errors.ZeroAddress.selector); - metaVault.setSubVaultsCurator(address(0)); + registry.setSubVaultsCurator(address(0)); } function test_setSubVaultsCurator_sameValue() public { // Setup: Get the current curator - address currentCurator = metaVault.subVaultsCurator(); + address currentCurator = registry.subVaultsCurator(); // Action & Assert: Expect revert when trying to set the curator to the current value vm.prank(admin); vm.expectRevert(Errors.ValueNotChanged.selector); - metaVault.setSubVaultsCurator(currentCurator); + registry.setSubVaultsCurator(currentCurator); } function test_setSubVaultsCurator_notRegisteredCurator() public { @@ -121,7 +129,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Expect revert when trying to set an unregistered curator vm.prank(admin); vm.expectRevert(Errors.InvalidCurator.selector); - metaVault.setSubVaultsCurator(unregisteredCurator); + registry.setSubVaultsCurator(unregisteredCurator); } function test_setSubVaultsCurator_success() public { @@ -135,17 +143,17 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Expect the SubVaultsCuratorUpdated event vm.expectEmit(true, false, false, true); - emit IVaultSubVaults.SubVaultsCuratorUpdated(admin, newCurator); + emit ISubVaultsRegistry.SubVaultsCuratorUpdated(newCurator); // Action: Set the new curator vm.prank(admin); - metaVault.setSubVaultsCurator(newCurator); + registry.setSubVaultsCurator(newCurator); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the curator was updated - assertEq(metaVault.subVaultsCurator(), newCurator); + assertEq(registry.subVaultsCurator(), newCurator); } function test_addSubVault_notAdmin() public { @@ -159,21 +167,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Non-admin cannot add a sub vault vm.prank(nonAdmin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); } function test_addSubVault_zeroAddress() public { // Action & Assert: Cannot add zero address as sub vault vm.prank(admin); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.addSubVault(address(0)); + registry.addSubVault(address(0)); } function test_addSubVault_sameVaultAddress() public { // Action & Assert: Cannot add meta vault itself as a sub vault vm.prank(admin); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.addSubVault(address(metaVault)); + registry.addSubVault(address(metaVault)); } function test_addSubVault_notRegisteredVault() public { @@ -183,7 +191,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot add non-registered vault vm.prank(admin); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.addSubVault(fakeVault); + registry.addSubVault(fakeVault); } function test_addSubVault_alreadyAddedSubVault() public { @@ -193,12 +201,12 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot add already added sub vault vm.prank(admin); vm.expectRevert(Errors.AlreadyAdded.selector); - metaVault.addSubVault(existingSubVault); + registry.addSubVault(existingSubVault); } function test_addSubVault_moreThanMaxSubVaults() public { // Get current sub vault count - uint256 currentCount = metaVault.getSubVaults().length; + uint256 currentCount = registry.getSubVaults().length; uint256 maxSubVaults = 50; // Add sub vaults until we reach the max @@ -210,11 +218,11 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Add the sub vault vm.prank(admin); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); } // Verify we now have exactly 50 sub vaults - assertEq(metaVault.getSubVaults().length, 50, "Should have 50 sub vaults"); + assertEq(registry.getSubVaults().length, 50, "Should have 50 sub vaults"); // Try to add one more (the 51st) address oneMoreVault = _createSubVault(admin); @@ -223,10 +231,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { // This should revert with CapacityExceeded vm.prank(admin); vm.expectRevert(Errors.CapacityExceeded.selector); - metaVault.addSubVault(oneMoreVault); + registry.addSubVault(oneMoreVault); // Verify the number of sub vaults remains at 50 - assertEq(metaVault.getSubVaults().length, 50, "Should still have 50 sub vaults"); + assertEq(registry.getSubVaults().length, 50, "Should still have 50 sub vaults"); } function test_addSubVault_notCollateralized() public { @@ -236,7 +244,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot add non-collateralized vault vm.prank(admin); vm.expectRevert(Errors.NotCollateralized.selector); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); } function test_addSubVault_unprocessedLegacyExitQueueTickets() public { @@ -264,10 +272,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { // 5. Try to add the vault to the meta vault vm.prank(admin); vm.expectRevert(Errors.ExitRequestNotProcessed.selector); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); // 6. Verify the vault wasn't added - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); bool found = false; for (uint256 i = 0; i < subVaultsAfter.length; i++) { if (subVaultsAfter[i] == newSubVault) { @@ -293,13 +301,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot add vault with different rewards nonce vm.prank(admin); vm.expectRevert(Errors.NotHarvested.selector); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); } function test_addSubVault_firstSubVault() public { // create new meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -309,6 +317,8 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // create new sub vault address subVault = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); @@ -326,13 +336,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action: Add the new sub vault vm.prank(admin); - newMetaVault.addSubVault(subVault); + newRegistry.addSubVault(subVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the sub vault was added - address[] memory subVaultsAfter = newMetaVault.getSubVaults(); + address[] memory subVaultsAfter = newRegistry.getSubVaults(); assertEq(subVaultsAfter.length, 1, "Sub vaults length should be 1"); assertEq(subVaultsAfter[0], subVault, "Sub vault address mismatch"); } @@ -347,17 +357,17 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Expect the SubVaultAdded event vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.SubVaultAdded(admin, newSubVault); + emit ISubVaultsRegistry.SubVaultAdded(newSubVault); // Action: Add the new sub vault vm.prank(admin); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the sub vault was added - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); bool found = false; for (uint256 i = 0; i < subVaultsAfter.length; i++) { if (subVaultsAfter[i] == newSubVault) { @@ -378,7 +388,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Non-admin cannot eject a sub vault vm.prank(nonAdmin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.ejectSubVault(subVaultToEject); + registry.ejectSubVault(subVaultToEject); } function test_ejectSubVault_alreadyEjecting() public { @@ -387,23 +397,22 @@ contract VaultSubVaultsTest is Test, EthHelpers { address secondSubVault = subVaults[1]; // Deposit to sub vaults first to ensure they have staked shares - vm.prank(admin); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Eject the first sub vault vm.prank(admin); - metaVault.ejectSubVault(firstSubVault); + registry.ejectSubVault(firstSubVault); // Action & Assert: Cannot eject another sub vault while one is already being ejected vm.prank(admin); vm.expectRevert(Errors.EjectingVault.selector); - metaVault.ejectSubVault(secondSubVault); + registry.ejectSubVault(secondSubVault); } function test_ejectSubVault_singleSubVaultLeft() public { // Create a new meta vault with empty sub vaults to allow ejecting all but one bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -413,28 +422,30 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Create and add 2 empty sub vaults address subVault1 = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault1); vm.prank(admin); - newMetaVault.addSubVault(subVault1); + newRegistry.addSubVault(subVault1); address subVault2 = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault2); vm.prank(admin); - newMetaVault.addSubVault(subVault2); + newRegistry.addSubVault(subVault2); // Eject the first sub vault (empty, so it's immediately removed) vm.prank(admin); - newMetaVault.ejectSubVault(subVault1); + newRegistry.ejectSubVault(subVault1); - address[] memory remainingSubVaults = newMetaVault.getSubVaults(); + address[] memory remainingSubVaults = newRegistry.getSubVaults(); assertEq(remainingSubVaults.length, 1, "Should have 1 sub vault left"); // Action & Assert: Cannot eject the last sub vault vm.prank(admin); vm.expectRevert(Errors.EmptySubVaults.selector); - newMetaVault.ejectSubVault(subVault2); + newRegistry.ejectSubVault(subVault2); } function test_ejectSubVault_notInSubVaults() public { @@ -445,13 +456,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot eject a vault that's not in sub vaults vm.prank(admin); vm.expectRevert(Errors.AlreadyRemoved.selector); - metaVault.ejectSubVault(nonSubVault); + registry.ejectSubVault(nonSubVault); } function test_ejectSubVault_emptySubVault() public { // Create a new meta vault with empty sub vaults (no staked shares) bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -461,54 +472,56 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Create and add empty sub vaults (not depositing to them) address subVault1 = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault1); vm.prank(admin); - newMetaVault.addSubVault(subVault1); + newRegistry.addSubVault(subVault1); address subVault2 = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault2); vm.prank(admin); - newMetaVault.addSubVault(subVault2); + newRegistry.addSubVault(subVault2); address subVault3 = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault3); vm.prank(admin); - newMetaVault.addSubVault(subVault3); + newRegistry.addSubVault(subVault3); // Verify sub vaults have no staked shares - assertEq(newMetaVault.subVaultsStates(subVault1).stakedShares, 0, "Sub vault 1 should have no staked shares"); - assertEq(newMetaVault.subVaultsStates(subVault2).stakedShares, 0, "Sub vault 2 should have no staked shares"); + assertEq(newRegistry.subVaultsStates(subVault1).stakedShares, 0, "Sub vault 1 should have no staked shares"); + assertEq(newRegistry.subVaultsStates(subVault2).stakedShares, 0, "Sub vault 2 should have no staked shares"); // Start gas measurement _startSnapshotGas("test_ejectSubVault_emptySubVault"); // Expect SubVaultEjected event vm.expectEmit(true, true, false, false); - emit IVaultSubVaults.SubVaultEjected(admin, subVault1); + emit ISubVaultsRegistry.SubVaultEjected(subVault1); // Action: Eject the sub vault vm.prank(admin); - newMetaVault.ejectSubVault(subVault1); + newRegistry.ejectSubVault(subVault1); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the sub vault was removed from the list - address[] memory subVaultsAfter = newMetaVault.getSubVaults(); + address[] memory subVaultsAfter = newRegistry.getSubVaults(); assertEq(subVaultsAfter.length, 2, "Should have 2 sub vaults left"); // Expect SubVaultEjected event vm.expectEmit(true, true, false, false); - emit IVaultSubVaults.SubVaultEjected(admin, subVault2); + emit ISubVaultsRegistry.SubVaultEjected(subVault2); // Can remove another sub vault vm.prank(admin); - newMetaVault.ejectSubVault(subVault2); + newRegistry.ejectSubVault(subVault2); // Assert: Verify the sub vault was removed from the list - subVaultsAfter = newMetaVault.getSubVaults(); + subVaultsAfter = newRegistry.getSubVaults(); assertEq(subVaultsAfter.length, 1, "Should have 1 sub vault left"); } @@ -517,8 +530,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { address subVaultToEject = subVaults[0]; // Deposit to sub vaults to get collateralized state - vm.prank(admin); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Start gas measurement _startSnapshotGas("test_ejectSubVault_subVaultWithShares"); @@ -529,16 +541,16 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action: Eject the sub vault vm.prank(admin); - metaVault.ejectSubVault(subVaultToEject); + registry.ejectSubVault(subVaultToEject); // Stop gas measurement _stopSnapshotGas(); - assertEq(metaVault.ejectingSubVault(), subVaultToEject); + assertEq(registry.ejectingSubVault(), subVaultToEject); // And verify it's in the list bool found = false; - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); for (uint256 i = 0; i < subVaultsAfter.length; i++) { if (subVaultsAfter[i] == subVaultToEject) { found = true; @@ -553,7 +565,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { address subVaultToEject = subVaults[0]; // Deposit to sub vaults to get collateralized state - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // user enters exit queue metaVault.enterExitQueue(metaVault.getShares(address(this)) / 2, address(this)); @@ -571,14 +583,14 @@ contract VaultSubVaultsTest is Test, EthHelpers { metaVault.updateState(_getEmptyHarvestParams()); - IVaultSubVaults.SubVaultState memory stateBefore = metaVault.subVaultsStates(subVaultToEject); + ISubVaultsRegistry.SubVaultState memory stateBefore = registry.subVaultsStates(subVaultToEject); assertGt(stateBefore.queuedShares, 0, "Queued shares should be greater than 0"); assertGt(stateBefore.stakedShares, 0, "Staked shares should be greater than 0"); // Action: Eject the sub vault vm.prank(admin); - metaVault.ejectSubVault(subVaultToEject); - IVaultSubVaults.SubVaultState memory stateAfter = metaVault.subVaultsStates(subVaultToEject); + registry.ejectSubVault(subVaultToEject); + ISubVaultsRegistry.SubVaultState memory stateAfter = registry.subVaultsStates(subVaultToEject); assertEq( stateBefore.queuedShares + stateBefore.stakedShares, stateAfter.queuedShares, @@ -594,13 +606,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Expect revert when trying to deposit when not harvested vm.expectRevert(Errors.NotHarvested.selector); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); } function test_depositToSubVaults_emptySubVaults() public { // Setup: Create a new meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -610,32 +622,33 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Action & Assert: Expect revert when trying to deposit to empty sub vaults - vm.prank(admin); vm.expectRevert(Errors.EmptySubVaults.selector); - newMetaVault.depositToSubVaults(); + newRegistry.depositToSubVaults(); } function test_depositToSubVaults_noAvailableAssets() public { vm.deal(address(metaVault), 0); vm.expectRevert(Errors.InvalidAssets.selector); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); } function test_depositToSubVaults_singleSubVault() public { // Setup: Remove all but one sub vault for (uint256 i = 1; i < subVaults.length; i++) { vm.prank(admin); - metaVault.ejectSubVault(subVaults[i]); + registry.ejectSubVault(subVaults[i]); } // Verify there's only one sub vault left - address[] memory remainingSubVaults = metaVault.getSubVaults(); + address[] memory remainingSubVaults = registry.getSubVaults(); assertEq(remainingSubVaults.length, 1, "Should have only one sub vault"); // Get initial state of the remaining sub vault address depositSubVault = remainingSubVaults[0]; - IVaultSubVaults.SubVaultState memory initialState = metaVault.subVaultsStates(depositSubVault); + ISubVaultsRegistry.SubVaultState memory initialState = registry.subVaultsStates(depositSubVault); uint256 availableAssets = metaVault.withdrawableAssets(); uint256 newShares = IEthVault(depositSubVault).convertToShares(availableAssets); @@ -644,7 +657,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_singleSubVault"); // Action: Deposit to sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Stop gas measurement _stopSnapshotGas(); @@ -653,7 +666,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { assertApproxEqAbs(metaVault.withdrawableAssets(), 0, 2, "Withdrawable assets should be 0"); // Assert: Verify the sub vault received staked shares (allow small tolerance for rounding) - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(remainingSubVaults[0]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(remainingSubVaults[0]); assertApproxEqAbs( finalState.stakedShares, initialState.stakedShares + newShares, @@ -665,10 +678,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_depositToSubVaults_multipleSubVaults() public { // Setup: Get initial state of all sub vaults uint256 subVaultCount = subVaults.length; - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaultCount); + ISubVaultsRegistry.SubVaultState[] memory initialStates = new ISubVaultsRegistry.SubVaultState[](subVaultCount); uint256 initialTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { - initialStates[i] = metaVault.subVaultsStates(subVaults[i]); + initialStates[i] = registry.subVaultsStates(subVaults[i]); initialTotalStaked += initialStates[i].stakedShares; } @@ -676,7 +689,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_multipleSubVaults"); // Action: Deposit to sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Stop gas measurement _stopSnapshotGas(); @@ -687,7 +700,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Assert: Verify all sub vaults received staked shares and total increased uint256 finalTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(subVaults[i]); assertGe( finalState.stakedShares, initialStates[i].stakedShares, "Sub vault staked shares should not decrease" ); @@ -699,7 +712,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_depositToSubVaults_ejectingSubVault() public { - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Deposit funds to meta vault vm.deal(address(this), 10 ether); @@ -708,21 +721,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Use the last sub vault to eject (guaranteed to exist) address ejectingSubVault = subVaults[subVaults.length - 1]; vm.prank(metaVault.admin()); - metaVault.ejectSubVault(ejectingSubVault); + registry.ejectSubVault(ejectingSubVault); // Setup: Get initial state of all sub vaults uint256 subVaultCount = subVaults.length; - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaultCount); + ISubVaultsRegistry.SubVaultState[] memory initialStates = new ISubVaultsRegistry.SubVaultState[](subVaultCount); uint256 initialTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { - initialStates[i] = metaVault.subVaultsStates(subVaults[i]); + initialStates[i] = registry.subVaultsStates(subVaults[i]); initialTotalStaked += initialStates[i].stakedShares; } _startSnapshotGas("test_depositToSubVaults_ejectingSubVault"); // Action: Deposit to sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _stopSnapshotGas(); @@ -730,7 +743,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { assertApproxEqAbs(metaVault.withdrawableAssets(), 0, 2, "Withdrawable assets should be 0"); // Assert: Verify ejecting sub vault received no new staked shares - IVaultSubVaults.SubVaultState memory ejectingFinalState = metaVault.subVaultsStates(ejectingSubVault); + ISubVaultsRegistry.SubVaultState memory ejectingFinalState = registry.subVaultsStates(ejectingSubVault); assertEq( ejectingFinalState.stakedShares, initialStates[subVaults.length - 1].stakedShares, @@ -740,7 +753,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Assert: Verify other sub vaults received staked shares uint256 finalTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(subVaults[i]); finalTotalStaked += finalState.stakedShares; } assertGt(finalTotalStaked, initialTotalStaked, "Total staked shares should have increased"); @@ -748,7 +761,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_depositToSubVaults_maxVaults() public { // Get current sub vaults and add more to reach maximum (50) - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); uint256 currentCount = currentSubVaults.length; address[] memory maxSubVaults = new address[](50); @@ -760,18 +773,18 @@ contract VaultSubVaultsTest is Test, EthHelpers { _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); vm.prank(admin); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); maxSubVaults[i] = newSubVault; } // Verify we have exactly 50 sub vaults - currentSubVaults = metaVault.getSubVaults(); + currentSubVaults = registry.getSubVaults(); assertEq(currentSubVaults.length, 50, "Should have exactly 50 sub vaults"); // Get initial state of all sub vaults - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](50); + ISubVaultsRegistry.SubVaultState[] memory initialStates = new ISubVaultsRegistry.SubVaultState[](50); for (uint256 i = 0; i < 50; i++) { - initialStates[i] = metaVault.subVaultsStates(maxSubVaults[i]); + initialStates[i] = registry.subVaultsStates(maxSubVaults[i]); } // Calculate available assets and expected distribution @@ -788,7 +801,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_maxVaults"); // Action: Deposit to all 50 sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Stop gas measurement _stopSnapshotGas(); @@ -796,7 +809,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Assert: Verify each sub vault received its portion of assets uint256 totalStakedShares = 0; for (uint256 i = 0; i < 50; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(maxSubVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(maxSubVaults[i]); uint256 newShares = finalState.stakedShares - initialStates[i].stakedShares; // We want to be a bit flexible with the exact share calculation due to rounding @@ -827,7 +840,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_noSubVaults() public { // Create a new meta vault without any sub vaults bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -911,7 +924,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_unprocessedSubVaultExit() public { // Create a new meta vault to have precise control over state bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -921,19 +934,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Create and add sub vaults address[] memory newSubVaults = new address[](3); for (uint256 i = 0; i < 3; i++) { newSubVaults[i] = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVaults[i]); vm.prank(admin); - newMetaVault.addSubVault(newSubVaults[i]); + newRegistry.addSubVault(newSubVaults[i]); } // Deposit to meta vault and distribute to sub vaults vm.deal(address(this), 10 ether); newMetaVault.deposit{value: 10 ether}(address(this), address(0)); - newMetaVault.depositToSubVaults(); + newRegistry.depositToSubVaults(); // user enters exit queue newMetaVault.enterExitQueue(newMetaVault.getShares(address(this)), address(this)); @@ -959,10 +974,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { } // set up exit requests for sub vaults using captured position tickets - IVaultSubVaults.SubVaultExitRequest[] memory exitRequests = - new IVaultSubVaults.SubVaultExitRequest[](exitPositions.length); + ISubVaultsRegistry.SubVaultExitRequest[] memory exitRequests = + new ISubVaultsRegistry.SubVaultExitRequest[](exitPositions.length); for (uint256 i = 0; i < exitPositions.length; i++) { - exitRequests[i] = IVaultSubVaults.SubVaultExitRequest({ + exitRequests[i] = ISubVaultsRegistry.SubVaultExitRequest({ vault: exitPositions[i].vault, exitQueueIndex: uint256( IVaultEnterExit(exitPositions[i].vault).getExitQueueIndex(exitPositions[i].positionTicket) @@ -984,7 +999,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // claim exited assets vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - newMetaVault.claimSubVaultsExitedAssets(exitRequests); + newRegistry.claimSubVaultsExitedAssets(exitRequests); // succeeds uint256 feeRecipientShares = newMetaVault.getShares(newMetaVault.feeRecipient()); @@ -1003,7 +1018,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_newTotalAssets() public { // Create new meta vault with new sub vaults for precise state control bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -1012,20 +1027,22 @@ contract VaultSubVaultsTest is Test, EthHelpers { ); IEthMetaVault newMetaVault = IEthMetaVault(_createVault(VaultType.EthMetaVault, admin, initParams, false)); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Create and add new sub vaults address[] memory newSubVaults = new address[](3); for (uint256 i = 0; i < 3; i++) { newSubVaults[i] = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVaults[i]); vm.prank(admin); - newMetaVault.addSubVault(newSubVaults[i]); + newRegistry.addSubVault(newSubVaults[i]); } // Deposit to meta vault and then to sub vaults uint256 depositAmount = 10 ether; vm.deal(address(this), depositAmount); newMetaVault.deposit{value: depositAmount}(address(this), address(0)); - newMetaVault.depositToSubVaults(); + newRegistry.depositToSubVaults(); uint256 totalAssetsBefore = newMetaVault.totalAssets(); @@ -1036,7 +1053,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _setVaultRewardsNonce(newSubVaults[i], newNonce); } - assertTrue(newMetaVault.canUpdateState(), "Meta vault should be able to update state"); + assertTrue(newRegistry.canUpdateState(), "Meta vault should be able to update state"); // update nonce for meta vault newMetaVault.updateState(_getEmptyHarvestParams()); @@ -1115,10 +1132,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { // claim exited assets vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - IVaultSubVaults.SubVaultExitRequest[] memory claims = - new IVaultSubVaults.SubVaultExitRequest[](exitRequests2.length); + ISubVaultsRegistry.SubVaultExitRequest[] memory claims = + new ISubVaultsRegistry.SubVaultExitRequest[](exitRequests2.length); for (uint256 i = 0; i < exitRequests2.length; i++) { - claims[i] = IVaultSubVaults.SubVaultExitRequest({ + claims[i] = ISubVaultsRegistry.SubVaultExitRequest({ vault: exitRequests2[i].vault, exitQueueIndex: uint256( IVaultEnterExit(exitRequests2[i].vault).getExitQueueIndex(exitRequests2[i].positionTicket) @@ -1127,7 +1144,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { }); } - newMetaVault.claimSubVaultsExitedAssets(claims); + newRegistry.claimSubVaultsExitedAssets(claims); assertEq( newMetaVault.totalAssets(), expectedTotalAssets, @@ -1152,7 +1169,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_enterExitQueueMaxVaults() public { // Get current sub vaults and add more to reach maximum (50) - address[] memory currentSubVaults = metaVault.getSubVaults(); + address[] memory currentSubVaults = registry.getSubVaults(); uint256 currentCount = currentSubVaults.length; address[] memory maxSubVaults = new address[](50); @@ -1164,21 +1181,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); vm.prank(admin); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); maxSubVaults[i] = newSubVault; } // Verify we have exactly 50 sub vaults - currentSubVaults = metaVault.getSubVaults(); + currentSubVaults = registry.getSubVaults(); assertEq(currentSubVaults.length, 50, "Should have exactly 50 sub vaults"); // Deposit assets to all sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Get initial state of all sub vaults - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](50); + ISubVaultsRegistry.SubVaultState[] memory initialStates = new ISubVaultsRegistry.SubVaultState[](50); for (uint256 i = 0; i < 50; i++) { - initialStates[i] = metaVault.subVaultsStates(maxSubVaults[i]); + initialStates[i] = registry.subVaultsStates(maxSubVaults[i]); assertGt(initialStates[i].stakedShares, 0, "Sub vault should have staked shares after deposit"); } @@ -1216,7 +1233,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { uint256 vaultsWithExits = 0; for (uint256 i = 0; i < 50; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(maxSubVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(maxSubVaults[i]); uint256 queuedSharesDelta = finalState.queuedShares - initialStates[i].queuedShares; uint256 stakedSharesDelta = initialStates[i].stakedShares - finalState.stakedShares; @@ -1248,7 +1265,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { uint256 assetsMoved = 0; for (uint256 i = 0; i < 50; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(maxSubVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(maxSubVaults[i]); uint256 queuedSharesDelta = finalState.queuedShares - initialStates[i].queuedShares; if (queuedSharesDelta > 0) { assetsMoved += IVaultState(maxSubVaults[i]).convertToAssets(queuedSharesDelta); @@ -1262,7 +1279,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets() public { // Create a new meta vault to have precise control over state bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -1271,6 +1288,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { ); EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); // Create and add sub vaults address[] memory newSubVaults = new address[](3); @@ -1278,13 +1296,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { newSubVaults[i] = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVaults[i]); vm.prank(admin); - newMetaVault.addSubVault(newSubVaults[i]); + newRegistry.addSubVault(newSubVaults[i]); } // Deposit to meta vault and distribute to sub vaults vm.deal(address(this), 10 ether); newMetaVault.deposit{value: 10 ether}(address(this), address(0)); - newMetaVault.depositToSubVaults(); + newRegistry.depositToSubVaults(); // Get a reference to a single sub vault we'll use for the test address testSubVault = newSubVaults[0]; @@ -1305,7 +1323,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { newMetaVault.updateState(_getEmptyHarvestParams()); ExitRequest[] memory exitPositions = _extractExitPositions(newSubVaults, vm.getRecordedLogs(), timestamp); - IVaultSubVaults.SubVaultState memory stateBefore = newMetaVault.subVaultsStates(testSubVault); + ISubVaultsRegistry.SubVaultState memory stateBefore = newRegistry.subVaultsStates(testSubVault); // Process the exit request but only provide small amount of funds uint256 processedAssets = 0.1 ether; @@ -1328,10 +1346,11 @@ contract VaultSubVaultsTest is Test, EthHelpers { } // Create a single exit request to claim using captured position ticket - IVaultSubVaults.SubVaultExitRequest[] memory singleExitRequest = new IVaultSubVaults.SubVaultExitRequest[](1); + ISubVaultsRegistry.SubVaultExitRequest[] memory singleExitRequest = + new ISubVaultsRegistry.SubVaultExitRequest[](1); int256 exitQueueIndex = IVaultEnterExit(testSubVault).getExitQueueIndex(testSubVaultPositionTicket); - singleExitRequest[0] = IVaultSubVaults.SubVaultExitRequest({ + singleExitRequest[0] = ISubVaultsRegistry.SubVaultExitRequest({ vault: testSubVault, exitQueueIndex: uint256(exitQueueIndex), timestamp: timestamp }); @@ -1341,7 +1360,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _startSnapshotGas("VaultSubVaultsTest_test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets"); // Claim the exited assets - newMetaVault.claimSubVaultsExitedAssets(singleExitRequest); + newRegistry.claimSubVaultsExitedAssets(singleExitRequest); // Stop gas measurement _stopSnapshotGas(); @@ -1359,7 +1378,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { assertEq(claimedAssets, processedAssets, "Claimed assets should be equal to processed assets"); // The sub vault's queued shares should decrease but not to zero - IVaultSubVaults.SubVaultState memory stateAfter = newMetaVault.subVaultsStates(testSubVault); + ISubVaultsRegistry.SubVaultState memory stateAfter = newRegistry.subVaultsStates(testSubVault); assertLt(stateAfter.queuedShares, stateBefore.queuedShares, "Queued shares should decrease"); assertGt(stateAfter.queuedShares, 0, "Queued shares should not be zero - only partial was processed"); } @@ -1367,7 +1386,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_claimSubVaultsExitedAssets_ejectingSubVault() public { // Create a new meta vault to have precise control over state bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, @@ -1377,19 +1396,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault newMetaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + ISubVaultsRegistry newRegistry = _getRegistry(address(newMetaVault)); + // Create and add sub vaults address[] memory newSubVaults = new address[](3); for (uint256 i = 0; i < 3; i++) { newSubVaults[i] = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVaults[i]); vm.prank(admin); - newMetaVault.addSubVault(newSubVaults[i]); + newRegistry.addSubVault(newSubVaults[i]); } // Deposit to meta vault and distribute to sub vaults vm.deal(address(this), 10 ether); newMetaVault.deposit{value: 10 ether}(address(this), address(0)); - newMetaVault.depositToSubVaults(); + newRegistry.depositToSubVaults(); // Choose a sub vault to eject address ejectingSubVault = newSubVaults[0]; @@ -1397,7 +1418,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Eject the sub vault - record logs to capture position ticket vm.recordLogs(); vm.prank(admin); - newMetaVault.ejectSubVault(ejectingSubVault); + newRegistry.ejectSubVault(ejectingSubVault); uint64 ejectTimestamp = uint64(vm.getBlockTimestamp()); address[] memory ejectingVaults = new address[](1); ejectingVaults[0] = ejectingSubVault; @@ -1405,10 +1426,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { _extractExitPositions(ejectingVaults, vm.getRecordedLogs(), ejectTimestamp); // Verify the ejecting sub vault is set correctly - assertEq(newMetaVault.ejectingSubVault(), ejectingSubVault, "Ejecting sub vault should be set"); + assertEq(newRegistry.ejectingSubVault(), ejectingSubVault, "Ejecting sub vault should be set"); // Verify the vault has moved from staked to queued shares - IVaultSubVaults.SubVaultState memory state = newMetaVault.subVaultsStates(ejectingSubVault); + ISubVaultsRegistry.SubVaultState memory state = newRegistry.subVaultsStates(ejectingSubVault); assertEq(state.stakedShares, 0, "Staked shares should be zero after ejection"); assertGt(state.queuedShares, 0, "Queued shares should be positive after ejection"); @@ -1442,8 +1463,8 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Create exit requests for claiming using captured position ticket from ejection require(ejectPositions.length > 0, "Should have captured position ticket from ejection"); - IVaultSubVaults.SubVaultExitRequest[] memory claimRequests = new IVaultSubVaults.SubVaultExitRequest[](1); - claimRequests[0] = IVaultSubVaults.SubVaultExitRequest({ + ISubVaultsRegistry.SubVaultExitRequest[] memory claimRequests = new ISubVaultsRegistry.SubVaultExitRequest[](1); + claimRequests[0] = ISubVaultsRegistry.SubVaultExitRequest({ vault: ejectingSubVault, exitQueueIndex: uint256( IVaultEnterExit(ejectingSubVault).getExitQueueIndex(ejectPositions[0].positionTicket) @@ -1452,17 +1473,17 @@ contract VaultSubVaultsTest is Test, EthHelpers { }); // Claim exited assets - newMetaVault.claimSubVaultsExitedAssets(claimRequests); + newRegistry.claimSubVaultsExitedAssets(claimRequests); // Verify the ejecting sub vault is removed from the state - assertEq(newMetaVault.ejectingSubVault(), address(0), "Ejecting sub vault should be removed"); + assertEq(newRegistry.ejectingSubVault(), address(0), "Ejecting sub vault should be removed"); assertEq( - newMetaVault.subVaultsStates(ejectingSubVault).stakedShares, + newRegistry.subVaultsStates(ejectingSubVault).stakedShares, 0, "Ejecting sub vault should have zero staked shares after claim" ); assertEq( - newMetaVault.subVaultsStates(ejectingSubVault).queuedShares, + newRegistry.subVaultsStates(ejectingSubVault).queuedShares, 0, "Ejecting sub vault should have zero queued shares after claim" ); @@ -1473,27 +1494,27 @@ contract VaultSubVaultsTest is Test, EthHelpers { address metaSubVault = _createMetaSubVault(admin); // Verify pendingMetaSubVault is empty - assertEq(metaVault.pendingMetaSubVault(), address(0), "Pending meta sub vault should be empty"); + assertEq(registry.pendingMetaSubVault(), address(0), "Pending meta sub vault should be empty"); // Expect the MetaSubVaultProposed event vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.MetaSubVaultProposed(admin, metaSubVault); + emit ISubVaultsRegistry.MetaSubVaultProposed(metaSubVault); // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_proposesMetaVault"); // Action: Add the meta vault as sub vault (should only propose, not add) vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the meta vault is pending, not added - assertEq(metaVault.pendingMetaSubVault(), metaSubVault, "Meta vault should be pending"); + assertEq(registry.pendingMetaSubVault(), metaSubVault, "Meta vault should be pending"); // Verify the vault was NOT added to the sub vaults list yet - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); bool found = false; for (uint256 i = 0; i < subVaultsAfter.length; i++) { if (subVaultsAfter[i] == metaSubVault) { @@ -1511,52 +1532,55 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Propose the first meta sub vault vm.prank(admin); - metaVault.addSubVault(metaSubVault1); + registry.addSubVault(metaSubVault1); // Verify pendingMetaSubVault is set - assertEq(metaVault.pendingMetaSubVault(), metaSubVault1, "First meta sub vault should be pending"); + assertEq(registry.pendingMetaSubVault(), metaSubVault1, "First meta sub vault should be pending"); // Action & Assert: Cannot propose another meta sub vault while one is pending vm.prank(admin); vm.expectRevert(Errors.AlreadyAdded.selector); - metaVault.addSubVault(metaSubVault2); + registry.addSubVault(metaSubVault2); } function test_acceptMetaSubVault_notVaultsRegistryOwner() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Action & Assert: Non-VaultsRegistry owner cannot accept meta sub vault address nonOwner = makeAddr("NonOwner"); vm.prank(nonOwner); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.acceptMetaSubVault(metaSubVault); + registry.acceptMetaSubVault(metaSubVault); // Also test that admin cannot accept vm.prank(admin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.acceptMetaSubVault(metaSubVault); + registry.acceptMetaSubVault(metaSubVault); } function test_acceptMetaSubVault_zeroAddress() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Action & Assert: Cannot accept zero address vm.prank(contracts.vaultsRegistry.owner()); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.acceptMetaSubVault(address(0)); + registry.acceptMetaSubVault(address(0)); } function test_acceptMetaSubVault_invalidVault() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Setup: Create another meta sub vault (not pending) address otherMetaSubVault = _createMetaSubVault(admin); @@ -1564,7 +1588,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot accept a vault that is not pending vm.prank(contracts.vaultsRegistry.owner()); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.acceptMetaSubVault(otherMetaSubVault); + registry.acceptMetaSubVault(otherMetaSubVault); } function test_acceptMetaSubVault_success() public { @@ -1573,28 +1597,28 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Propose meta sub vault vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Get sub vault count before accepting - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + uint256 subVaultsCountBefore = registry.getSubVaults().length; // Expect the SubVaultAdded event vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.SubVaultAdded(contracts.vaultsRegistry.owner(), metaSubVault); + emit ISubVaultsRegistry.SubVaultAdded(metaSubVault); // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_acceptMetaSubVault_success"); // Action: Accept meta sub vault by VaultsRegistry owner vm.prank(contracts.vaultsRegistry.owner()); - metaVault.acceptMetaSubVault(metaSubVault); + registry.acceptMetaSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the meta vault was added as sub vault - assertEq(metaVault.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); - address[] memory subVaultsAfter = metaVault.getSubVaults(); + assertEq(registry.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); + address[] memory subVaultsAfter = registry.getSubVaults(); assertEq(subVaultsAfter.length, subVaultsCountBefore + 1, "Sub vault count should increase by 1"); bool found = false; @@ -1610,33 +1634,36 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_rejectMetaSubVault_notAdminOrOwner() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Action & Assert: Non-admin/non-owner cannot reject meta sub vault address nonAdmin = makeAddr("NonAdmin"); vm.prank(nonAdmin); vm.expectRevert(Errors.AccessDenied.selector); - metaVault.rejectMetaSubVault(metaSubVault); + registry.rejectMetaSubVault(metaSubVault); } function test_rejectMetaSubVault_zeroAddress() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Action & Assert: Cannot reject zero address vm.prank(admin); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.rejectMetaSubVault(address(0)); + registry.rejectMetaSubVault(address(0)); } function test_rejectMetaSubVault_invalidVault() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Setup: Create another meta sub vault (not pending) address otherMetaSubVault = _createMetaSubVault(admin); @@ -1644,35 +1671,36 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot reject a vault that is not pending vm.prank(admin); vm.expectRevert(Errors.InvalidVault.selector); - metaVault.rejectMetaSubVault(otherMetaSubVault); + registry.rejectMetaSubVault(otherMetaSubVault); } function test_rejectMetaSubVault_byOwner_success() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Get sub vault count before rejection - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + uint256 subVaultsCountBefore = registry.getSubVaults().length; // Expect the MetaSubVaultRejected event vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.MetaSubVaultRejected(contracts.vaultsRegistry.owner(), metaSubVault); + emit ISubVaultsRegistry.MetaSubVaultRejected(metaSubVault); // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_rejectMetaSubVault_byOwner_success"); // Action: Reject meta sub vault by VaultsRegistry owner vm.prank(contracts.vaultsRegistry.owner()); - metaVault.rejectMetaSubVault(metaSubVault); + registry.rejectMetaSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the pending meta sub vault was cleared and vault not added - assertEq(metaVault.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); - address[] memory subVaultsAfter = metaVault.getSubVaults(); + assertEq(registry.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); + address[] memory subVaultsAfter = registry.getSubVaults(); assertEq(subVaultsAfter.length, subVaultsCountBefore, "Sub vault count should not change"); bool found = false; @@ -1688,29 +1716,30 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_rejectMetaSubVault_byAdmin_success() public { // Setup: Create and propose meta sub vault address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Get sub vault count before rejection - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + uint256 subVaultsCountBefore = registry.getSubVaults().length; // Expect the MetaSubVaultRejected event vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.MetaSubVaultRejected(admin, metaSubVault); + emit ISubVaultsRegistry.MetaSubVaultRejected(metaSubVault); // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_rejectMetaSubVault_byAdmin_success"); // Action: Reject meta sub vault by admin vm.prank(admin); - metaVault.rejectMetaSubVault(metaSubVault); + registry.rejectMetaSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the pending meta sub vault was cleared and vault not added - assertEq(metaVault.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); - address[] memory subVaultsAfter = metaVault.getSubVaults(); + assertEq(registry.pendingMetaSubVault(), address(0), "Pending meta sub vault should be cleared"); + address[] memory subVaultsAfter = registry.getSubVaults(); assertEq(subVaultsAfter.length, subVaultsCountBefore, "Sub vault count should not change"); bool found = false; @@ -1726,7 +1755,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_addSubVault_metaVaultAsSubVault_notCollateralized() public { // Setup: Create meta vault but don't collateralize it bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: type(uint256).max, feePercent: 0, @@ -1738,7 +1767,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action & Assert: Cannot add non-collateralized meta vault vm.prank(admin); vm.expectRevert(Errors.NotCollateralized.selector); - metaVault.addSubVault(unCollateralizedMetaVault); + registry.addSubVault(unCollateralizedMetaVault); } function test_addSubVault_metaVaultAsSubVault_notHarvested() public { @@ -1752,12 +1781,12 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Propose meta sub vault (this should succeed) vm.prank(admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Action & Assert: Accept should fail because meta vault has different rewards nonce vm.prank(contracts.vaultsRegistry.owner()); vm.expectRevert(Errors.NotHarvested.selector); - metaVault.acceptMetaSubVault(metaSubVault); + registry.acceptMetaSubVault(metaSubVault); } function test_ejectSubVault_metaVaultAsSubVault_emptySubVault() public { @@ -1765,24 +1794,24 @@ contract VaultSubVaultsTest is Test, EthHelpers { address metaSubVault = _setupMetaSubVault(admin); // Get sub vault count before ejection - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + uint256 subVaultsCountBefore = registry.getSubVaults().length; // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_emptySubVault"); // Expect SubVaultEjected event vm.expectEmit(true, true, false, false); - emit IVaultSubVaults.SubVaultEjected(admin, metaSubVault); + emit ISubVaultsRegistry.SubVaultEjected(metaSubVault); // Action: Eject the meta vault sub vault vm.prank(admin); - metaVault.ejectSubVault(metaSubVault); + registry.ejectSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the meta vault was removed from the list - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); assertEq(subVaultsAfter.length, subVaultsCountBefore - 1, "Meta vault sub vault should be removed"); bool found = false; @@ -1804,10 +1833,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { metaVault.deposit{value: 5 ether}(address(this), address(0)); // Deposit to sub vaults to get collateralized state - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Verify the meta sub vault has staked shares - IVaultSubVaults.SubVaultState memory stateBefore = metaVault.subVaultsStates(metaSubVault); + ISubVaultsRegistry.SubVaultState memory stateBefore = registry.subVaultsStates(metaSubVault); assertGt(stateBefore.stakedShares, 0, "Meta sub vault should have staked shares"); // Start gas measurement @@ -1819,22 +1848,22 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Action: Eject the meta vault sub vault with shares vm.prank(admin); - metaVault.ejectSubVault(metaSubVault); + registry.ejectSubVault(metaSubVault); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify ejecting sub vault is set - assertEq(metaVault.ejectingSubVault(), metaSubVault, "Ejecting sub vault should be the meta sub vault"); + assertEq(registry.ejectingSubVault(), metaSubVault, "Ejecting sub vault should be the meta sub vault"); // Verify state changes - shares moved from staked to queued - IVaultSubVaults.SubVaultState memory stateAfter = metaVault.subVaultsStates(metaSubVault); + ISubVaultsRegistry.SubVaultState memory stateAfter = registry.subVaultsStates(metaSubVault); assertEq(stateAfter.stakedShares, 0, "Staked shares should be zero after ejection"); assertEq(stateAfter.queuedShares, stateBefore.stakedShares, "Queued shares should equal previous staked shares"); // Verify the meta vault is still in the list (ejecting) bool found = false; - address[] memory subVaultsAfter = metaVault.getSubVaults(); + address[] memory subVaultsAfter = registry.getSubVaults(); for (uint256 i = 0; i < subVaultsAfter.length; i++) { if (subVaultsAfter[i] == metaSubVault) { found = true; @@ -1863,7 +1892,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_withMetaVaultSubVault"); // Action: Deposit to sub vaults (including meta sub vault) - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Stop gas measurement _stopSnapshotGas(); @@ -1873,9 +1902,9 @@ contract VaultSubVaultsTest is Test, EthHelpers { assertGt(metaSubVaultBalanceAfter, metaSubVaultBalanceBefore, "Meta sub vault should receive funds"); // Verify sub vault states are updated - address[] memory allSubVaults = metaVault.getSubVaults(); + address[] memory allSubVaults = registry.getSubVaults(); for (uint256 i = 0; i < allSubVaults.length; i++) { - IVaultSubVaults.SubVaultState memory state = metaVault.subVaultsStates(allSubVaults[i]); + ISubVaultsRegistry.SubVaultState memory state = registry.subVaultsStates(allSubVaults[i]); if (allSubVaults[i] == metaSubVault) { assertGt(state.stakedShares, 0, "Meta sub vault should have staked shares"); } @@ -1886,25 +1915,27 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); + ISubVaultsRegistry metaSubVaultRegistry = _getRegistry(metaSubVault); + // Deposit to main meta vault vm.deal(address(this), 20 ether); metaVault.deposit{value: 20 ether}(address(this), address(0)); // Deposit to sub vaults (including meta sub vault) - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Verify meta sub vault received funds assertGt(metaSubVault.balance, 0, "Meta sub vault should have received funds"); // Now deposit from meta sub vault to its own sub vault - EthMetaVault(payable(metaSubVault)).depositToSubVaults(); + metaSubVaultRegistry.depositToSubVaults(); // Assert: Verify nested meta vault deposited to its sub vault - address[] memory nestedSubVaults = EthMetaVault(payable(metaSubVault)).getSubVaults(); + address[] memory nestedSubVaults = metaSubVaultRegistry.getSubVaults(); assertEq(nestedSubVaults.length, 1, "Meta sub vault should have exactly 1 sub vault"); address nestedSubVault = nestedSubVaults[0]; - IVaultSubVaults.SubVaultState memory state = EthMetaVault(payable(metaSubVault)).subVaultsStates(nestedSubVault); + ISubVaultsRegistry.SubVaultState memory state = metaSubVaultRegistry.subVaultsStates(nestedSubVault); assertGt(state.stakedShares, 0, "Nested sub vault should have staked shares"); assertGt(nestedSubVault.balance, 0, "Nested sub vault should have received funds"); } @@ -1915,7 +1946,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { vm.deal(address(this), 5 ether); metaVault.deposit{value: 5 ether}(address(this), address(0)); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Set up current nonce for all vaults uint64 currentNonce = contracts.keeper.rewardsNonce() + 1; @@ -1931,7 +1962,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _setVaultRewardsNonce(subVaults[i], currentNonce); } - uint256 nonceBefore = metaVault.subVaultsRewardsNonce(); + uint256 nonceBefore = registry.subVaultsRewardsNonce(); // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_updateState_withMetaVaultSubVault_success"); @@ -1944,7 +1975,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Assert: Verify state was updated successfully // Check that nonce was updated - uint256 metaVaultNonce = metaVault.subVaultsRewardsNonce(); + uint256 metaVaultNonce = registry.subVaultsRewardsNonce(); assertEq(metaVaultNonce, currentNonce, "Meta vault nonce should be updated"); assertGt(metaVaultNonce, nonceBefore, "Meta vault nonce should have increased"); } @@ -1991,17 +2022,17 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Propose and accept the meta sub vault on the main metaVault vm.prank(_admin); - metaVault.addSubVault(metaSubVault); + registry.addSubVault(metaSubVault); // Accept the meta sub vault by VaultsRegistry owner vm.prank(contracts.vaultsRegistry.owner()); - metaVault.acceptMetaSubVault(metaSubVault); + registry.acceptMetaSubVault(metaSubVault); } function _createMetaSubVault(address _admin) internal returns (address metaSubVault) { // Deploy meta vault that will be used as sub vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IEthMetaVault.EthMetaVaultInitParams({ subVaultsCurator: curator, capacity: type(uint256).max, feePercent: 0, // 0% @@ -2014,13 +2045,15 @@ contract VaultSubVaultsTest is Test, EthHelpers { address subVault = _createSubVault(_admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + ISubVaultsRegistry metaSubVaultRegistry = _getRegistry(metaSubVault); + vm.prank(_admin); - EthMetaVault(payable(metaSubVault)).addSubVault(subVault); + metaSubVaultRegistry.addSubVault(subVault); } function _harvestMetaSubVault(address metaSubVault, int160 totalReward, uint160 unlockedMevReward) internal { // Get the sub vault of the meta sub vault - address[] memory metaSubSubVaults = EthMetaVault(payable(metaSubVault)).getSubVaults(); + address[] memory metaSubSubVaults = _getRegistry(metaSubVault).getSubVaults(); require(metaSubSubVaults.length > 0, "Meta sub vault should have sub vaults"); // First harvest the sub vault of the meta sub vault @@ -2034,7 +2067,9 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function _setMetaVaultRewardsNonce(address vault, uint128 rewardsNonce) internal { - stdstore.target(vault).sig("subVaultsRewardsNonce()").checked_write(rewardsNonce); + // subVaultsRewardsNonce is now on the SubVaultsRegistry, not the meta vault + address vaultRegistry = IVaultSubVaults(vault).subVaultsRegistry(); + stdstore.target(vaultRegistry).sig("subVaultsRewardsNonce()").checked_write(rewardsNonce); } function _extractExitPositions(address[] memory _subVaults, Vm.Log[] memory logs, uint64 timestamp) @@ -2043,7 +2078,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { returns (ExitRequest[] memory exitRequests) { uint256 subVaultsCount = _subVaults.length; - uint256 exitSubVaultsCount = metaVault.ejectingSubVault() != address(0) ? subVaultsCount - 1 : subVaultsCount; + uint256 exitSubVaultsCount = registry.ejectingSubVault() != address(0) ? subVaultsCount - 1 : subVaultsCount; exitRequests = new ExitRequest[](exitSubVaultsCount); uint256 subVaultIndex = 0; for (uint256 i = 0; i < logs.length; i++) { diff --git a/test/gnosis/GnoGenesisVault.t.sol b/test/gnosis/GnoGenesisVault.t.sol index 7d8228c2..f5f3b767 100644 --- a/test/gnosis/GnoGenesisVault.t.sol +++ b/test/gnosis/GnoGenesisVault.t.sol @@ -59,56 +59,6 @@ contract GnoGenesisVaultTest is Test, GnoHelpers { IGnoGenesisVault(_vault).initialize(initParams); } - function test_upgradesCorrectly() public { - // Get or create a vault - address vaultAddr = _getForkVault(VaultType.GnoGenesisVault); - GnoGenesisVault existingVault = GnoGenesisVault(payable(vaultAddr)); - - _depositToVault(address(existingVault), 15 ether, user, user); - _registerGnoValidator(address(existingVault), 1 ether, true); - - vm.prank(user); - existingVault.enterExitQueue(10 ether, user); - - // Record initial state - uint256 totalExitingAssetsBefore = IVaultStateV3(address(existingVault)).totalExitingAssets(); - uint256 queuedSharesBefore = IVaultStateV3(address(existingVault)).queuedShares(); - uint256 initialTotalAssets = existingVault.totalAssets(); - uint256 initialTotalShares = existingVault.totalShares(); - uint256 senderBalanceBefore = existingVault.getShares(user); - uint256 initialCapacity = existingVault.capacity(); - uint256 initialFeePercent = existingVault.feePercent(); - address validatorsManager = existingVault.validatorsManager(); - address feeRecipient = existingVault.feeRecipient(); - address adminBefore = existingVault.admin(); - - assertEq(existingVault.vaultId(), keccak256("GnoGenesisVault")); - assertEq(existingVault.version(), 3); - - _startSnapshotGas("GnoGenesisVaultTest_test_upgradesCorrectly"); - _upgradeVault(VaultType.GnoGenesisVault, address(existingVault)); - _stopSnapshotGas(); - - (uint128 queuedShares,,, uint128 totalExitingAssets,) = existingVault.getExitQueueData(); - assertEq(existingVault.vaultId(), keccak256("GnoGenesisVault")); - assertEq(existingVault.version(), 4); - assertEq(existingVault.admin(), adminBefore); - assertEq(existingVault.capacity(), initialCapacity); - assertEq(existingVault.feePercent(), initialFeePercent); - assertEq(existingVault.feeRecipient(), feeRecipient); - assertEq(existingVault.validatorsManager(), validatorsManager); - assertEq(queuedShares, queuedSharesBefore); - assertEq(existingVault.totalShares(), initialTotalShares); - assertEq(existingVault.totalAssets(), initialTotalAssets); - assertEq(totalExitingAssets, totalExitingAssetsBefore); - assertEq(existingVault.validatorsManagerNonce(), 0); - assertEq(existingVault.getShares(user), senderBalanceBefore); - assertEq( - contracts.gnoToken.allowance(address(existingVault), address(contracts.validatorsRegistry)), - type(uint256).max - ); - } - function test_cannotInitializeTwice() public { // Get or create a vault address vaultAddr = _getOrCreateVault(VaultType.GnoGenesisVault, admin, initParams, false); diff --git a/test/gnosis/GnoMetaVault.t.sol b/test/gnosis/GnoMetaVault.t.sol index d37e3271..30fc118e 100644 --- a/test/gnosis/GnoMetaVault.t.sol +++ b/test/gnosis/GnoMetaVault.t.sol @@ -9,8 +9,8 @@ import {IGnoMetaVault} from "../../contracts/interfaces/IGnoMetaVault.sol"; import {IGnoVault} from "../../contracts/interfaces/IGnoVault.sol"; import {IVaultState} from "../../contracts/interfaces/IVaultState.sol"; import {IVaultSubVaults} from "../../contracts/interfaces/IVaultSubVaults.sol"; +import {ISubVaultsRegistry} from "../../contracts/interfaces/ISubVaultsRegistry.sol"; import {IVaultEnterExit} from "../../contracts/interfaces/IVaultEnterExit.sol"; -import {IMetaVault} from "../../contracts/interfaces/IMetaVault.sol"; import {Errors} from "../../contracts/libraries/Errors.sol"; import {GnoMetaVault} from "../../contracts/vaults/gnosis/GnoMetaVault.sol"; import {GnoMetaVaultFactory} from "../../contracts/vaults/gnosis/GnoMetaVaultFactory.sol"; @@ -22,6 +22,7 @@ import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; contract GnoMetaVaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoMetaVault public metaVault; + ISubVaultsRegistry public registry; address public admin; address public curator; @@ -59,7 +60,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ + IGnoMetaVault.GnoMetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -68,6 +69,9 @@ contract GnoMetaVaultTest is Test, GnoHelpers { ); metaVault = GnoMetaVault(payable(_getOrCreateVault(VaultType.GnoMetaVault, admin, initParams, false))); + // Get registry reference + registry = ISubVaultsRegistry(metaVault.subVaultsRegistry()); + // Deploy and add sub vaults for (uint256 i = 0; i < 3; i++) { address subVault = _createSubVault(admin); @@ -75,7 +79,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { subVaults.push(subVault); vm.prank(admin); - metaVault.addSubVault(subVault); + registry.addSubVault(subVault); } } @@ -96,13 +100,13 @@ contract GnoMetaVaultTest is Test, GnoHelpers { assertEq(metaVault.vaultId(), keccak256("GnoMetaVault"), "Incorrect vault ID"); assertEq(metaVault.version(), 4, "Incorrect version"); assertEq(metaVault.admin(), admin, "Incorrect admin"); - assertEq(metaVault.subVaultsCurator(), curator, "Incorrect curator"); + assertEq(registry.subVaultsCurator(), curator, "Incorrect curator"); assertEq(metaVault.capacity(), 1000 ether, "Incorrect capacity"); assertEq(metaVault.feePercent(), 1000, "Incorrect fee percent"); assertEq(metaVault.feeRecipient(), admin, "Incorrect fee recipient"); // Verify sub vaults - address[] memory storedSubVaults = metaVault.getSubVaults(); + address[] memory storedSubVaults = registry.getSubVaults(); assertEq(storedSubVaults.length, 3, "Incorrect number of sub vaults"); for (uint256 i = 0; i < 3; i++) { assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); @@ -117,6 +121,10 @@ contract GnoMetaVaultTest is Test, GnoHelpers { vm.startPrank(sender); IERC20(address(contracts.gnoToken)).approve(address(metaVault), depositAmount); + // Expect Deposited event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.Deposited(sender, receiver, depositAmount, expectedShares, referrer); + _startSnapshotGas("GnoMetaVaultTest_test_deposit"); uint256 shares = metaVault.deposit(depositAmount, receiver, referrer); _stopSnapshotGas(); @@ -143,14 +151,15 @@ contract GnoMetaVaultTest is Test, GnoHelpers { assertGt(metaVault.withdrawableAssets(), 0, "Withdrawable assets should be greater than 0"); // Get sub vault states before deposit - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaults.length); + ISubVaultsRegistry.SubVaultState[] memory initialStates = + new ISubVaultsRegistry.SubVaultState[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - initialStates[i] = metaVault.subVaultsStates(subVaults[i]); + initialStates[i] = registry.subVaultsStates(subVaults[i]); } // Call depositToSubVaults _startSnapshotGas("GnoMetaVaultTest_test_depositToSubVaults"); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); _stopSnapshotGas(); // Verify withdrawable assets are empty @@ -158,7 +167,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { // Verify sub vault balances increased for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaults[i]); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(subVaults[i]); assertGt(finalState.stakedShares, initialStates[i].stakedShares, "Sub vault staked shares should increase"); } } @@ -169,21 +178,21 @@ contract GnoMetaVaultTest is Test, GnoHelpers { _collateralizeGnoVault(newSubVault); // Get sub vault count before adding - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + uint256 subVaultsCountBefore = registry.getSubVaults().length; + + // Expect SubVaultAdded event + vm.expectEmit(true, false, false, true); + emit ISubVaultsRegistry.SubVaultAdded(newSubVault); // Add the new sub vault vm.prank(admin); _startSnapshotGas("GnoMetaVaultTest_test_addSubVault"); - metaVault.addSubVault(newSubVault); + registry.addSubVault(newSubVault); _stopSnapshotGas(); // Verify sub vault was added - address[] memory storedSubVaults = metaVault.getSubVaults(); + address[] memory storedSubVaults = registry.getSubVaults(); assertEq(storedSubVaults.length, subVaultsCountBefore + 1, "Sub vault not added"); - - // Verify approval for GNO token transfer - uint256 allowance = IERC20(address(contracts.gnoToken)).allowance(address(metaVault), newSubVault); - assertEq(allowance, type(uint256).max, "GNO token allowance not set correctly"); } function test_ejectSubVault() public { @@ -195,26 +204,26 @@ contract GnoMetaVaultTest is Test, GnoHelpers { metaVault.deposit(depositAmount, sender, referrer); vm.stopPrank(); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Get a sub vault to eject address subVaultToEject = subVaults[0]; // Get initial state - IVaultSubVaults.SubVaultState memory initialState = metaVault.subVaultsStates(subVaultToEject); + ISubVaultsRegistry.SubVaultState memory initialState = registry.subVaultsStates(subVaultToEject); require(initialState.stakedShares > 0, "Sub vault should have staked shares"); // Eject the sub vault vm.prank(admin); _startSnapshotGas("GnoMetaVaultTest_test_ejectSubVault"); - metaVault.ejectSubVault(subVaultToEject); + registry.ejectSubVault(subVaultToEject); _stopSnapshotGas(); // Verify ejecting sub vault is set - assertEq(metaVault.ejectingSubVault(), subVaultToEject, "Ejecting sub vault not set correctly"); + assertEq(registry.ejectingSubVault(), subVaultToEject, "Ejecting sub vault not set correctly"); // Verify state changes - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaultToEject); + ISubVaultsRegistry.SubVaultState memory finalState = registry.subVaultsStates(subVaultToEject); assertEq(finalState.stakedShares, 0, "Staked shares should be zero"); assertEq(finalState.queuedShares, initialState.stakedShares, "Queued shares should equal initial staked shares"); @@ -232,7 +241,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { metaVault.deposit(depositAmount, sender, referrer); vm.stopPrank(); - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Update nonces for sub vaults uint64 newNonce = contracts.keeper.rewardsNonce() + 2; @@ -241,8 +250,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { _setVaultRewardsNonce(subVaults[i], newNonce); } - bool updateRequiredAfter = metaVault.isStateUpdateRequired(); - assertTrue(updateRequiredAfter, "State update should be required"); + assertTrue(registry.isStateUpdateRequired(), "State update should be required"); // Create harvest params IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); @@ -272,8 +280,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { assertTrue(foundEvent, "RewardsNonceUpdated event was not emitted"); // Verify state update is no longer required - updateRequiredAfter = metaVault.isStateUpdateRequired(); - assertFalse(updateRequiredAfter, "State update should no longer be required"); + assertFalse(registry.isStateUpdateRequired(), "State update should no longer be required"); } function test_enterExitQueue() public { @@ -288,6 +295,10 @@ contract GnoMetaVaultTest is Test, GnoHelpers { // Get shares of sender uint256 senderShares = metaVault.getShares(sender); + // Expect ExitQueueEntered event + vm.expectEmit(true, true, false, false); + emit IVaultEnterExit.ExitQueueEntered(sender, sender, 0, senderShares); + // Enter exit queue vm.prank(sender); _startSnapshotGas("GnoMetaVaultTest_test_enterExitQueue"); @@ -320,7 +331,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { vm.stopPrank(); // Deposit to sub vaults - metaVault.depositToSubVaults(); + registry.depositToSubVaults(); // Enter exit queue with all shares uint256 senderShares = metaVault.getShares(sender); @@ -349,10 +360,10 @@ contract GnoMetaVaultTest is Test, GnoHelpers { } // Prepare exit requests for claiming from sub vaults to meta vault - IVaultSubVaults.SubVaultExitRequest[] memory exitRequests = - new IVaultSubVaults.SubVaultExitRequest[](subVaults.length); + ISubVaultsRegistry.SubVaultExitRequest[] memory exitRequests = + new ISubVaultsRegistry.SubVaultExitRequest[](subVaults.length); for (uint256 i = 0; i < subVaults.length; i++) { - exitRequests[i] = IVaultSubVaults.SubVaultExitRequest({ + exitRequests[i] = ISubVaultsRegistry.SubVaultExitRequest({ vault: subVaults[i], exitQueueIndex: uint256(IVaultEnterExit(subVaults[i]).getExitQueueIndex(0)), timestamp: exitTimestamp @@ -363,7 +374,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { vm.warp(block.timestamp + _exitingAssetsClaimDelay + 1); // Claim exited assets from sub vaults to meta vault - metaVault.claimSubVaultsExitedAssets(exitRequests); + registry.claimSubVaultsExitedAssets(exitRequests); // Update nonces for sub vaults to process exit queue newNonce = contracts.keeper.rewardsNonce() + 1; diff --git a/test/gnosis/GnoPrivMetaVault.t.sol b/test/gnosis/GnoPrivMetaVault.t.sol deleted file mode 100644 index 29d1ba81..00000000 --- a/test/gnosis/GnoPrivMetaVault.t.sol +++ /dev/null @@ -1,475 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.22; - -import {Test} from "forge-std/Test.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {IGnoPrivMetaVault} from "../../contracts/interfaces/IGnoPrivMetaVault.sol"; -import {IGnoMetaVault} from "../../contracts/interfaces/IGnoMetaVault.sol"; -import {IGnoVault} from "../../contracts/interfaces/IGnoVault.sol"; -import {IMetaVault} from "../../contracts/interfaces/IMetaVault.sol"; -import {IVaultSubVaults} from "../../contracts/interfaces/IVaultSubVaults.sol"; -import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; -import {Errors} from "../../contracts/libraries/Errors.sol"; -import {GnoPrivMetaVault} from "../../contracts/vaults/gnosis/GnoPrivMetaVault.sol"; -import {BalancedCurator} from "../../contracts/curators/BalancedCurator.sol"; -import {CuratorsRegistry} from "../../contracts/curators/CuratorsRegistry.sol"; -import {GnoHelpers} from "../helpers/GnoHelpers.sol"; - -contract GnoPrivMetaVaultTest is Test, GnoHelpers { - ForkContracts public contracts; - GnoPrivMetaVault public metaVault; - - address public admin; - address public sender; - address public receiver; - address public referrer; - address public whitelister; - address public other; - address public curator; - - // Sub vaults - address[] public subVaults; - - // Test constants - uint256 constant GNO_AMOUNT = 10 ether; - - function setUp() public { - // Activate Gnosis fork and get contracts - contracts = _activateGnosisFork(); - - // Set up test accounts - admin = makeAddr("Admin"); - sender = makeAddr("Sender"); - receiver = makeAddr("Receiver"); - referrer = makeAddr("Referrer"); - whitelister = makeAddr("Whitelister"); - other = makeAddr("Other"); - - // Mint GNO tokens to accounts - _mintGnoToken(admin, 100 ether); - _mintGnoToken(sender, 100 ether); - _mintGnoToken(other, 100 ether); - _mintGnoToken(address(this), 100 ether); - - // Create a curator - curator = address(new BalancedCurator()); - - // Register the curator in the registry - vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); - CuratorsRegistry(_curatorsRegistry).addCurator(curator); - - // Deploy private meta vault - bytes memory initParams = abi.encode( - IMetaVault.MetaVaultInitParams({ - subVaultsCurator: curator, - capacity: type(uint256).max, - feePercent: 0, - metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" - }) - ); - metaVault = GnoPrivMetaVault(payable(_getOrCreateVault(VaultType.GnoPrivMetaVault, admin, initParams, false))); - - // Deploy and add sub vaults - for (uint256 i = 0; i < 3; i++) { - address subVault = _createSubVault(admin); - _collateralizeGnoVault(subVault); - subVaults.push(subVault); - - vm.prank(admin); - metaVault.addSubVault(subVault); - } - } - - function _createSubVault(address _admin) internal returns (address) { - bytes memory initParams = abi.encode( - IGnoVault.GnoVaultInitParams({ - capacity: 1000 ether, - feePercent: 5, // 5% - metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" - }) - ); - - return _createVault(VaultType.GnoVault, _admin, initParams, false); - } - - function _updateMetaVaultState() internal { - // Update nonces for sub vaults to prepare for state update - uint64 newNonce = contracts.keeper.rewardsNonce() + 1; - _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); - } - - // Update meta vault state - metaVault.updateState(_getEmptyHarvestParams()); - } - - // ============ Deployment Tests ============ - - function test_deployment() public view { - assertEq(metaVault.vaultId(), keccak256("GnoPrivMetaVault"), "Incorrect vault ID"); - assertEq(metaVault.version(), 4, "Incorrect version"); - assertEq(metaVault.admin(), admin, "Incorrect admin"); - assertEq(metaVault.whitelister(), admin, "Whitelister should be admin initially"); - assertEq(metaVault.subVaultsCurator(), curator, "Incorrect curator"); - assertEq(metaVault.capacity(), type(uint256).max, "Incorrect capacity"); - assertEq(metaVault.feePercent(), 0, "Incorrect fee percent"); - assertEq(metaVault.feeRecipient(), admin, "Incorrect fee recipient"); - - // Verify sub vaults - address[] memory storedSubVaults = metaVault.getSubVaults(); - assertEq(storedSubVaults.length, 3, "Incorrect number of sub vaults"); - for (uint256 i = 0; i < 3; i++) { - assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); - } - } - - function test_cannotInitializeTwice() public { - vm.expectRevert(Initializable.InvalidInitialization.selector); - metaVault.initialize("0x"); - } - - // ============ Whitelist Deposit Tests ============ - - function test_cannotDepositFromNotWhitelistedSender() public { - uint256 amount = GNO_AMOUNT; - - // Set whitelister and whitelist receiver but not sender - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(receiver, true); - - // Approve tokens - vm.prank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), amount); - - // Try to deposit from non-whitelisted user - vm.prank(sender); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.deposit(amount, receiver, referrer); - } - - function test_cannotDepositToNotWhitelistedReceiver() public { - uint256 amount = GNO_AMOUNT; - - // Set whitelister and whitelist sender but not receiver - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve tokens - vm.prank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), amount); - - // Try to deposit to non-whitelisted receiver - vm.prank(sender); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.deposit(amount, receiver, referrer); - } - - function test_canDepositAsWhitelistedUser() public { - uint256 amount = GNO_AMOUNT; - uint256 totalSharesBefore = metaVault.totalShares(); - uint256 expectedShares = metaVault.convertToShares(amount); - - // Set whitelister and whitelist both sender and receiver - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.startPrank(whitelister); - metaVault.updateWhitelist(sender, true); - metaVault.updateWhitelist(receiver, true); - vm.stopPrank(); - - // Approve tokens - vm.prank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), amount); - - // Deposit as whitelisted user - vm.prank(sender); - _startSnapshotGas("GnoPrivMetaVaultTest_test_canDepositAsWhitelistedUser"); - uint256 shares = metaVault.deposit(amount, receiver, referrer); - _stopSnapshotGas(); - - // Check balances - assertEq(shares, expectedShares, "Incorrect shares minted"); - assertEq(metaVault.getShares(receiver), expectedShares, "Receiver did not receive shares"); - assertEq(metaVault.totalShares(), totalSharesBefore + expectedShares, "Incorrect total shares"); - } - - // ============ Whitelist MintOsToken Tests ============ - - function test_cannotMintOsTokenFromNotWhitelistedUser() public { - uint256 depositAmount = GNO_AMOUNT; - - // Set whitelister and whitelist user for initial deposit - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve and deposit GNO to get vault shares - vm.startPrank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), depositAmount); - metaVault.deposit(depositAmount, sender, referrer); - vm.stopPrank(); - - // Deposit to sub vaults to collateralize - metaVault.depositToSubVaults(); - - // Remove sender from whitelist - vm.prank(whitelister); - metaVault.updateWhitelist(sender, false); - - // Try to mint osToken from non-whitelisted user - uint256 osTokenShares = depositAmount / 2; - vm.prank(sender); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.mintOsToken(sender, osTokenShares, referrer); - } - - function test_canMintOsTokenAsWhitelistedUser() public { - uint256 depositAmount = GNO_AMOUNT; - - // Set whitelister and whitelist sender - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve and deposit GNO to get vault shares - vm.startPrank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), depositAmount); - metaVault.deposit(depositAmount, sender, referrer); - vm.stopPrank(); - - // Deposit to sub vaults to collateralize - metaVault.depositToSubVaults(); - - // Mint osToken as whitelisted user - uint256 osTokenShares = depositAmount / 2; - vm.prank(sender); - _startSnapshotGas("GnoPrivMetaVaultTest_test_canMintOsTokenAsWhitelistedUser"); - uint256 assets = metaVault.mintOsToken(sender, osTokenShares, referrer); - _stopSnapshotGas(); - - // Check osToken position - uint128 shares = metaVault.osTokenPositions(sender); - assertEq(shares, osTokenShares, "Incorrect osToken shares"); - assertGt(assets, 0, "No osToken assets minted"); - } - - // ============ Whitelister Management Tests ============ - - function test_setWhitelister() public { - address newWhitelister = makeAddr("NewWhitelister"); - - // Non-admin cannot set whitelister - vm.prank(other); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.setWhitelister(newWhitelister); - - // Admin can set whitelister - vm.prank(admin); - _startSnapshotGas("GnoPrivMetaVaultTest_test_setWhitelister"); - metaVault.setWhitelister(newWhitelister); - _stopSnapshotGas(); - - assertEq(metaVault.whitelister(), newWhitelister, "Whitelister not set correctly"); - } - - function test_setWhitelister_valueNotChanged() public { - address currentWhitelister = metaVault.whitelister(); - - vm.prank(admin); - vm.expectRevert(Errors.ValueNotChanged.selector); - metaVault.setWhitelister(currentWhitelister); - } - - function test_updateWhitelist() public { - // Set whitelister - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - // Non-whitelister cannot update whitelist - vm.prank(other); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.updateWhitelist(sender, true); - - // Whitelister can update whitelist - vm.prank(whitelister); - _startSnapshotGas("GnoPrivMetaVaultTest_test_updateWhitelist"); - metaVault.updateWhitelist(sender, true); - _stopSnapshotGas(); - - assertTrue(metaVault.whitelistedAccounts(sender), "Account not whitelisted correctly"); - - // Whitelister can remove from whitelist - vm.prank(whitelister); - metaVault.updateWhitelist(sender, false); - - assertFalse(metaVault.whitelistedAccounts(sender), "Account not removed from whitelist correctly"); - } - - // ============ Whitelist State Preservation Tests ============ - - function test_whitelistUpdateDoesNotAffectExistingFunds() public { - uint256 amount = GNO_AMOUNT; - uint256 expectedShares = metaVault.convertToShares(amount); - - // Set whitelister and whitelist sender - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve and deposit GNO to get vault shares - vm.startPrank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), amount * 2); - metaVault.deposit(amount, sender, referrer); - vm.stopPrank(); - - uint256 initialBalance = metaVault.getShares(sender); - assertEq(initialBalance, expectedShares, "Initial shares incorrect"); - - // Remove sender from whitelist - vm.prank(whitelister); - metaVault.updateWhitelist(sender, false); - - // Verify share balance remains the same - assertEq(metaVault.getShares(sender), initialBalance, "Balance should not change when whitelisting is removed"); - - // Verify cannot make new deposits but still has existing shares - vm.prank(sender); - vm.expectRevert(Errors.AccessDenied.selector); - metaVault.deposit(amount, sender, referrer); - } - - function test_changingWhitelisterPreservesWhitelistState() public { - // Set initial whitelister - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - // Whitelist sender - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - assertTrue(metaVault.whitelistedAccounts(sender), "Sender should be whitelisted"); - - // Change whitelister - address newWhitelister = makeAddr("NewWhitelister"); - vm.prank(admin); - metaVault.setWhitelister(newWhitelister); - - // Verify sender is still whitelisted - assertTrue(metaVault.whitelistedAccounts(sender), "Sender whitelist status should be preserved"); - - // New whitelister can modify whitelist - vm.prank(newWhitelister); - metaVault.updateWhitelist(sender, false); - - assertFalse(metaVault.whitelistedAccounts(sender), "Sender should be removed from whitelist"); - } - - // ============ Meta Vault Operations with Whitelist Tests ============ - - function test_depositToSubVaultsWorksWithWhitelistedUser() public { - uint256 depositAmount = GNO_AMOUNT; - - // Set whitelister and whitelist sender - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve and deposit - vm.startPrank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), depositAmount); - metaVault.deposit(depositAmount, sender, referrer); - vm.stopPrank(); - - // Get sub vault states before deposit - IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaults.length); - for (uint256 i = 0; i < subVaults.length; i++) { - initialStates[i] = metaVault.subVaultsStates(subVaults[i]); - } - - // Deposit to sub vaults (anyone can call this) - _startSnapshotGas("GnoPrivMetaVaultTest_test_depositToSubVaultsWorksWithWhitelistedUser"); - metaVault.depositToSubVaults(); - _stopSnapshotGas(); - - // Verify sub vault balances increased - for (uint256 i = 0; i < subVaults.length; i++) { - IVaultSubVaults.SubVaultState memory finalState = metaVault.subVaultsStates(subVaults[i]); - assertGt(finalState.stakedShares, initialStates[i].stakedShares, "Sub vault staked shares should increase"); - } - } - - function test_enterExitQueueWorksForWhitelistedUserAfterRemoval() public { - uint256 depositAmount = GNO_AMOUNT; - - // Set whitelister and whitelist sender - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - vm.prank(whitelister); - metaVault.updateWhitelist(sender, true); - - // Approve and deposit - vm.startPrank(sender); - IERC20(address(contracts.gnoToken)).approve(address(metaVault), depositAmount); - metaVault.deposit(depositAmount, sender, referrer); - vm.stopPrank(); - - uint256 senderShares = metaVault.getShares(sender); - - // Remove from whitelist - vm.prank(whitelister); - metaVault.updateWhitelist(sender, false); - - // Should still be able to exit (enter exit queue) - vm.prank(sender); - _startSnapshotGas("GnoPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval"); - metaVault.enterExitQueue(senderShares, sender); - _stopSnapshotGas(); - - // Verify shares were reduced - assertEq(metaVault.getShares(sender), 0, "Shares should be 0 after entering exit queue"); - } - - function test_donateAssets_whitelistNotRequired() public { - uint256 donationAmount = 1 ether; - - // Set whitelister (don't whitelist sender) - vm.prank(admin); - metaVault.setWhitelister(whitelister); - - // Get vault state before donation - uint256 vaultBalanceBefore = contracts.gnoToken.balanceOf(address(metaVault)); - - // Approve GNO token for donation - vm.startPrank(sender); - contracts.gnoToken.approve(address(metaVault), donationAmount); - - // Donation should work without whitelisting - metaVault.donateAssets(donationAmount); - vm.stopPrank(); - - // Verify donation was received - assertEq( - contracts.gnoToken.balanceOf(address(metaVault)), - vaultBalanceBefore + donationAmount, - "Meta vault GNO balance should increase" - ); - } -} diff --git a/test/gnosis/GnoVault.t.sol b/test/gnosis/GnoVault.t.sol index 8fb0d1ca..14e4f0c2 100644 --- a/test/gnosis/GnoVault.t.sol +++ b/test/gnosis/GnoVault.t.sol @@ -205,9 +205,6 @@ contract GnoVaultTest is Test, GnoHelpers { assertEq(contracts.gnoToken.allowance(address(prevVault), address(contracts.validatorsRegistry)), 0); // Upgrade the vault - vm.expectEmit(true, false, false, true); - emit IVaultState.CheckpointCreated(depositAssets, depositAssets); - _startSnapshotGas("GnoVaultTest_test_upgradesCorrectly"); _upgradeVault(VaultType.GnoVault, address(prevVault)); _stopSnapshotGas(); diff --git a/test/gnosis/VaultGnoStaking.t.sol b/test/gnosis/VaultGnoStaking.t.sol index ef083216..d0bdc0e1 100644 --- a/test/gnosis/VaultGnoStaking.t.sol +++ b/test/gnosis/VaultGnoStaking.t.sol @@ -10,7 +10,6 @@ import {GnoHelpers} from "../helpers/GnoHelpers.sol"; import {Errors} from "../../contracts/libraries/Errors.sol"; import {GnoVault} from "../../contracts/vaults/gnosis/GnoVault.sol"; import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; -import {ITokensConverterFactory} from "../../contracts/interfaces/ITokensConverterFactory.sol"; contract VaultGnoStakingTest is Test, GnoHelpers { ForkContracts public contracts; @@ -121,7 +120,7 @@ contract VaultGnoStakingTest is Test, GnoHelpers { _collateralizeGnoVault(address(vault)); IKeeperRewards.HarvestParams memory harvestParams = _setGnoVaultReward(address(vault), 0, 1 ether); - address converter = ITokensConverterFactory(_tokensConverterFactory).getTokensConverter(address(vault)); + address converter = _getTokensConverter(address(vault)); uint256 balanceBefore = contracts.sdaiToken.balanceOf(converter); // Update state which will trigger _processTotalAssetsDelta @@ -138,7 +137,7 @@ contract VaultGnoStakingTest is Test, GnoHelpers { } function test_processTotalAssetsDelta_smallXdaiBalance() public { - address converter = ITokensConverterFactory(_tokensConverterFactory).getTokensConverter(address(vault)); + address converter = _getTokensConverter(address(vault)); // Deposit GNO _depositGno(depositAmount, sender, sender); @@ -341,7 +340,7 @@ contract VaultGnoStakingTest is Test, GnoHelpers { uint256 sendAmount = 0.5 ether; vm.deal(sender, sendAmount); - address converter = ITokensConverterFactory(_tokensConverterFactory).getTokensConverter(address(vault)); + address converter = _getTokensConverter(address(vault)); uint256 balanceBefore = contracts.sdaiToken.balanceOf(converter); vm.prank(sender); diff --git a/test/helpers/EthHelpers.sol b/test/helpers/EthHelpers.sol index 186067f8..c0d245fa 100644 --- a/test/helpers/EthHelpers.sol +++ b/test/helpers/EthHelpers.sol @@ -12,7 +12,7 @@ import {ISharedMevEscrow} from "../../contracts/interfaces/ISharedMevEscrow.sol" import {IEthValidatorsRegistry} from "../../contracts/interfaces/IEthValidatorsRegistry.sol"; import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; import {IVaultState} from "../../contracts/interfaces/IVaultState.sol"; -import {IMetaVault} from "../../contracts/interfaces/IMetaVault.sol"; +import {IEthMetaVault} from "../../contracts/interfaces/IEthMetaVault.sol"; import {IConsolidationsChecker} from "../../contracts/interfaces/IConsolidationsChecker.sol"; import {ConsolidationsChecker} from "../../contracts/validators/ConsolidationsChecker.sol"; import {EthBlocklistErc20Vault} from "../../contracts/vaults/ethereum/EthBlocklistErc20Vault.sol"; @@ -26,13 +26,19 @@ import {EthVaultFactory} from "../../contracts/vaults/ethereum/EthVaultFactory.s import {IEthFoxVault, EthFoxVault} from "../../contracts/vaults/ethereum/custom/EthFoxVault.sol"; import {EthMetaVault} from "../../contracts/vaults/ethereum/EthMetaVault.sol"; import {EthPrivMetaVault} from "../../contracts/vaults/ethereum/EthPrivMetaVault.sol"; +import {EthErc20MetaVault, IEthErc20MetaVault} from "../../contracts/vaults/ethereum/EthErc20MetaVault.sol"; +import {EthPrivErc20MetaVault} from "../../contracts/vaults/ethereum/EthPrivErc20MetaVault.sol"; import {EthMetaVaultFactory} from "../../contracts/vaults/ethereum/EthMetaVaultFactory.sol"; +import {SubVaultsRegistry} from "../../contracts/vaults/SubVaultsRegistry.sol"; +import {SubVaultsRegistryFactory} from "../../contracts/vaults/SubVaultsRegistryFactory.sol"; import {Keeper} from "../../contracts/keeper/Keeper.sol"; import {ValidatorsConsolidationsMock} from "../../contracts/mocks/ValidatorsConsolidationsMock.sol"; import {ValidatorsHelpers} from "./ValidatorsHelpers.sol"; import {ValidatorsWithdrawalsMock} from "../../contracts/mocks/ValidatorsWithdrawalsMock.sol"; import {VaultsRegistry, IVaultsRegistry} from "../../contracts/vaults/VaultsRegistry.sol"; import {CuratorsRegistry} from "../../contracts/curators/CuratorsRegistry.sol"; +import {IVaultSubVaults} from "../../contracts/interfaces/IVaultSubVaults.sol"; +import {ISubVaultsRegistry} from "../../contracts/interfaces/ISubVaultsRegistry.sol"; abstract contract EthHelpers is Test, ValidatorsHelpers { using stdStorage for StdStorage; @@ -66,7 +72,9 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { EthPrivErc20Vault, EthFoxVault, EthMetaVault, - EthPrivMetaVault + EthPrivMetaVault, + EthErc20MetaVault, + EthPrivErc20MetaVault } struct ForkContracts { @@ -86,6 +94,7 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { address internal _validatorsWithdrawals; address internal _validatorsConsolidations; + address internal _subVaultsRegistryFactory; function _activateEthereumFork() internal returns (ForkContracts memory) { vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), forkBlockNumber); @@ -93,6 +102,13 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { _validatorsWithdrawals = address(new ValidatorsWithdrawalsMock()); _validatorsConsolidations = address(new ValidatorsConsolidationsMock()); + // Deploy SubVaultsRegistryFactory + address subVaultsRegistryImpl = address( + new SubVaultsRegistry(_curatorsRegistry, _vaultsRegistry, _keeper, _osTokenVaultController, _osTokenConfig) + ); + _subVaultsRegistryFactory = + address(new SubVaultsRegistryFactory(subVaultsRegistryImpl, IVaultsRegistry(_vaultsRegistry))); + return ForkContracts({ keeper: Keeper(_keeper), validatorsRegistry: IEthValidatorsRegistry(_validatorsRegistry), @@ -329,7 +345,10 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { } address vaultAddress; - if (vaultType == VaultType.EthMetaVault) { + if ( + vaultType == VaultType.EthMetaVault || vaultType == VaultType.EthPrivMetaVault + || vaultType == VaultType.EthErc20MetaVault || vaultType == VaultType.EthPrivErc20MetaVault + ) { EthMetaVaultFactory factory = _getOrCreateMetaFactory(vaultType); vm.deal(admin, admin.balance + _securityDeposit); vm.prank(admin); @@ -377,7 +396,10 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { if (vaultType == VaultType.EthFoxVault) { if (currentVersion == 2) return; require(currentVersion == 1, "Invalid vault version"); - } else if (vaultType == VaultType.EthMetaVault || vaultType == VaultType.EthPrivMetaVault) { + } else if ( + vaultType == VaultType.EthMetaVault || vaultType == VaultType.EthPrivMetaVault + || vaultType == VaultType.EthErc20MetaVault || vaultType == VaultType.EthPrivErc20MetaVault + ) { if (currentVersion == 6) return; require(currentVersion == 5, "Invalid vault version"); } else { @@ -455,28 +477,33 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { uint64(_exitingAssetsClaimDelay) ); impl = address(new EthFoxVault(ethFoxVaultArgs)); - } else if (_vaultType == VaultType.EthMetaVault) { - IMetaVault.MetaVaultConstructorArgs memory ethMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( - _keeper, - _vaultsRegistry, - _osTokenVaultController, - _osTokenConfig, - _osTokenVaultEscrow, - _curatorsRegistry, - uint64(_exitingAssetsClaimDelay) - ); - impl = address(new EthMetaVault(ethMetaVaultArgs)); - } else if (_vaultType == VaultType.EthPrivMetaVault) { - IMetaVault.MetaVaultConstructorArgs memory ethMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( + } else if (_vaultType == VaultType.EthMetaVault || _vaultType == VaultType.EthPrivMetaVault) { + IEthMetaVault.EthMetaVaultConstructorArgs memory ethMetaVaultArgs = IEthMetaVault.EthMetaVaultConstructorArgs( _keeper, _vaultsRegistry, _osTokenVaultController, _osTokenConfig, _osTokenVaultEscrow, - _curatorsRegistry, + _subVaultsRegistryFactory, uint64(_exitingAssetsClaimDelay) ); - impl = address(new EthPrivMetaVault(ethMetaVaultArgs)); + impl = _vaultType == VaultType.EthMetaVault + ? address(new EthMetaVault(ethMetaVaultArgs)) + : address(new EthPrivMetaVault(ethMetaVaultArgs)); + } else if (_vaultType == VaultType.EthErc20MetaVault || _vaultType == VaultType.EthPrivErc20MetaVault) { + IEthErc20MetaVault.EthErc20MetaVaultConstructorArgs memory ethErc20MetaVaultArgs = + IEthErc20MetaVault.EthErc20MetaVaultConstructorArgs( + _keeper, + _vaultsRegistry, + _osTokenVaultController, + _osTokenConfig, + _osTokenVaultEscrow, + _subVaultsRegistryFactory, + uint64(_exitingAssetsClaimDelay) + ); + impl = _vaultType == VaultType.EthErc20MetaVault + ? address(new EthErc20MetaVault(ethErc20MetaVaultArgs)) + : address(new EthPrivErc20MetaVault(ethErc20MetaVaultArgs)); } else { revert("Unsupported vault type"); } @@ -505,4 +532,20 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { stdstore.enable_packed_slots().target(_keeper).sig("rewardsNonce()").checked_write(rewardsNonce); } + + function _getSubVaultsRegistry(address vault) internal view returns (ISubVaultsRegistry) { + return ISubVaultsRegistry(IVaultSubVaults(vault).subVaultsRegistry()); + } + + function _createEthSubVault(address _admin) internal returns (address) { + bytes memory initParams = abi.encode( + IEthVault.EthVaultInitParams({ + capacity: 1000 ether, + feePercent: 5, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + return _createVault(VaultType.EthVault, _admin, initParams, false); + } } diff --git a/test/helpers/GnoHelpers.sol b/test/helpers/GnoHelpers.sol index 16b22812..13d0f5c7 100644 --- a/test/helpers/GnoHelpers.sol +++ b/test/helpers/GnoHelpers.sol @@ -14,7 +14,8 @@ import {IGnoValidatorsRegistry} from "../../contracts/interfaces/IGnoValidatorsR import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; import {IVaultState} from "../../contracts/interfaces/IVaultState.sol"; import {IConsolidationsChecker} from "../../contracts/interfaces/IConsolidationsChecker.sol"; -import {IMetaVault} from "../../contracts/interfaces/IMetaVault.sol"; +import {IGnoMetaVault} from "../../contracts/interfaces/IGnoMetaVault.sol"; +import {ITokensConverterFactory} from "../../contracts/interfaces/ITokensConverterFactory.sol"; import {ConsolidationsChecker} from "../../contracts/validators/ConsolidationsChecker.sol"; import {GnoBlocklistErc20Vault} from "../../contracts/vaults/gnosis/GnoBlocklistErc20Vault.sol"; import {GnoBlocklistVault} from "../../contracts/vaults/gnosis/GnoBlocklistVault.sol"; @@ -24,9 +25,10 @@ import {GnoPrivErc20Vault} from "../../contracts/vaults/gnosis/GnoPrivErc20Vault import {GnoPrivVault} from "../../contracts/vaults/gnosis/GnoPrivVault.sol"; import {GnoVault, IGnoVault} from "../../contracts/vaults/gnosis/GnoVault.sol"; import {GnoMetaVault} from "../../contracts/vaults/gnosis/GnoMetaVault.sol"; -import {GnoPrivMetaVault} from "../../contracts/vaults/gnosis/GnoPrivMetaVault.sol"; import {GnoMetaVaultFactory} from "../../contracts/vaults/gnosis/GnoMetaVaultFactory.sol"; import {GnoVaultFactory} from "../../contracts/vaults/gnosis/GnoVaultFactory.sol"; +import {SubVaultsRegistry} from "../../contracts/vaults/SubVaultsRegistry.sol"; +import {SubVaultsRegistryFactory} from "../../contracts/vaults/SubVaultsRegistryFactory.sol"; import {Keeper} from "../../contracts/keeper/Keeper.sol"; import {ValidatorsConsolidationsMock} from "../../contracts/mocks/ValidatorsConsolidationsMock.sol"; import {ValidatorsWithdrawalsMock} from "../../contracts/mocks/ValidatorsWithdrawalsMock.sol"; @@ -42,7 +44,7 @@ interface IGnoToken { abstract contract GnoHelpers is Test, ValidatorsHelpers { using stdStorage for StdStorage; - uint256 internal constant forkBlockNumber = 40107000; + uint256 internal constant forkBlockNumber = 44470000; uint256 internal constant _securityDeposit = 1e9; address private constant _keeper = 0xcAC0e3E35d3BA271cd2aaBE688ac9DB1898C26aa; address private constant _validatorsRegistry = 0x0B98057eA310F4d31F2a452B414647007d1645d9; @@ -68,8 +70,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { GnoErc20Vault, GnoBlocklistErc20Vault, GnoPrivErc20Vault, - GnoMetaVault, - GnoPrivMetaVault + GnoMetaVault } struct ForkContracts { @@ -93,6 +94,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { address private _validatorsWithdrawals; address private _validatorsConsolidations; address internal _curatorsRegistry; + address internal _subVaultsRegistryFactory; function _activateGnosisFork() internal returns (ForkContracts memory) { vm.createSelectFork(vm.envString("GNOSIS_RPC_URL"), forkBlockNumber); @@ -102,6 +104,13 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { _consolidationsChecker = address(new ConsolidationsChecker(address(_keeper))); _curatorsRegistry = address(new CuratorsRegistry()); + // Deploy SubVaultsRegistryFactory + address subVaultsRegistryImpl = address( + new SubVaultsRegistry(_curatorsRegistry, _vaultsRegistry, _keeper, _osTokenVaultController, _osTokenConfig) + ); + _subVaultsRegistryFactory = + address(new SubVaultsRegistryFactory(subVaultsRegistryImpl, IVaultsRegistry(_vaultsRegistry))); + return ForkContracts({ keeper: Keeper(_keeper), validatorsRegistry: IGnoValidatorsRegistry(_validatorsRegistry), @@ -295,14 +304,23 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { return address(0); } + function _getTokensConverter(address vault) internal view returns (address) { + // Return hardcoded converter addresses for fork vaults + if (vault == 0x00025C729A3364FaEf02c7D1F577068d87E90ba6) { + return 0xf0A005327E740daaAaf1DCeB86ca57e5C4df767B; + } + // For non-fork vaults, use the factory prediction + return ITokensConverterFactory(_tokensConverterFactory).getTokensConverter(vault); + } + function _getVaultRewards(address vault, int160 newTotalReward, uint160 newUnlockedMevReward) private view returns (int160, uint160) { if (vault == 0x4b4406Ed8659D03423490D8b62a1639206dA0A7a) { - newTotalReward += 16036446295848871046698; - newUnlockedMevReward += 16104786197270190915179; + newTotalReward += 23125290825193839796698; + newUnlockedMevReward += 38182422752656127683973; } if (!vm.envBool("TEST_USE_FORK_VAULTS")) { @@ -310,18 +328,20 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { } if (vault == 0x00025C729A3364FaEf02c7D1F577068d87E90ba6) { - newTotalReward += 597138686177531250000; - newUnlockedMevReward += 2173687084505551299451; + newTotalReward += 1475599926192906250000; + newUnlockedMevReward += 4881832791211425238314; } else if (vault == 0x79Dbec2d18A758C62D410F9763956D52fbd4A3CC) { - newTotalReward += 2986604545031250000; - newUnlockedMevReward += 8209964011439485540; + newTotalReward += 7939266211593750000; + newUnlockedMevReward += 21543285647562346901; } else if (vault == 0x52Bd0fbF4839824680001d3653f2d503C6081085) { - newTotalReward += 55585164426875000000; + newTotalReward += 55733289651656250000; + newUnlockedMevReward += 99060935491632538003; } else if (vault == 0x33C346928eD9249Cf1d5fc16aE32a8CFFa1671AD) { - newTotalReward += 118624342091343750000; - newUnlockedMevReward += 263665552420563946481; + newTotalReward += 176255397509625000000; + newUnlockedMevReward += 438358384873681497429; } else if (vault == 0xdfdA4238359703180DAEc01e48F4625C1569c4dE) { - newTotalReward += 45747108062500000; + newTotalReward += 87578485156250000; + newUnlockedMevReward += 29001412383187947; } return (newTotalReward, newUnlockedMevReward); } @@ -331,7 +351,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { returns (address) { address vaultAddress; - if (vaultType == VaultType.GnoMetaVault || vaultType == VaultType.GnoPrivMetaVault) { + if (vaultType == VaultType.GnoMetaVault) { GnoMetaVaultFactory factory = _getOrCreateMetaFactory(vaultType); vm.startPrank(admin); IERC20(_gnoToken).approve(address(factory), _securityDeposit); @@ -354,6 +374,12 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { { GnoVaultFactory factory = _getPrevVersionVaultFactory(vaultType); + // Re-register the old factory if it's not already registered + if (!VaultsRegistry(_vaultsRegistry).factories(address(factory))) { + vm.prank(VaultsRegistry(_vaultsRegistry).owner()); + VaultsRegistry(_vaultsRegistry).addFactory(address(factory)); + } + vm.startPrank(admin); IERC20(_gnoToken).approve(address(factory), _securityDeposit); address vaultAddress = factory.createVault(initParams, isOwnMevEscrow); @@ -368,7 +394,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { if (vaultType == VaultType.GnoGenesisVault) { if (currentVersion == 4) return; require(currentVersion == 3, "Invalid vault version"); - } else if (vaultType == VaultType.GnoMetaVault || vaultType == VaultType.GnoPrivMetaVault) { + } else if (vaultType == VaultType.GnoMetaVault) { if (currentVersion == 4) return; require(currentVersion == 3, "Invalid vault version"); } else { @@ -435,27 +461,16 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { } else if (_vaultType == VaultType.GnoPrivErc20Vault) { impl = address(new GnoPrivErc20Vault(gnoErc20Args)); } else if (_vaultType == VaultType.GnoMetaVault) { - IMetaVault.MetaVaultConstructorArgs memory gnoMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( + IGnoMetaVault.GnoMetaVaultConstructorArgs memory gnoMetaVaultArgs = IGnoMetaVault.GnoMetaVaultConstructorArgs( _keeper, _vaultsRegistry, _osTokenVaultController, _osTokenConfig, _osTokenVaultEscrow, - _curatorsRegistry, + _subVaultsRegistryFactory, uint64(_exitingAssetsClaimDelay) ); impl = address(new GnoMetaVault(_gnoToken, gnoMetaVaultArgs)); - } else if (_vaultType == VaultType.GnoPrivMetaVault) { - IMetaVault.MetaVaultConstructorArgs memory gnoMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( - _keeper, - _vaultsRegistry, - _osTokenVaultController, - _osTokenConfig, - _osTokenVaultEscrow, - _curatorsRegistry, - uint64(_exitingAssetsClaimDelay) - ); - impl = address(new GnoPrivMetaVault(_gnoToken, gnoMetaVaultArgs)); } else { revert("Unsupported vault type"); }