diff --git a/.env.example b/.env.example index eb165fc7..e1b089f9 100644 --- a/.env.example +++ b/.env.example @@ -21,29 +21,23 @@ REMOVE_PREV_FACTORIES=false # contract variables # mainnet -#META_VAULT_FACTORY_OWNER=0x2685C0e39EEAAd383fB71ec3F493991d532A87ae #OS_TOKEN_REDEEMER_OWNER=0x2685C0e39EEAAd383fB71ec3F493991d532A87ae #OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY=43200 #VALIDATORS_REGISTRY=0x00000000219ab540356cBB839Cbe05303d7705Fa # hoodi -#META_VAULT_FACTORY_OWNER=0xFF2B6d2d5c205b99E2e6f607B6aFA3127B9957B6 #OS_TOKEN_REDEEMER_OWNER=0xFF2B6d2d5c205b99E2e6f607B6aFA3127B9957B6 #OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY=43200 #VALIDATORS_REGISTRY=0x00000000219ab540356cBB839Cbe05303d7705Fa # chiado -#META_VAULT_FACTORY_OWNER=0xFF2B6d2d5c205b99E2e6f607B6aFA3127B9957B6 #OS_TOKEN_REDEEMER_OWNER=0xFF2B6d2d5c205b99E2e6f607B6aFA3127B9957B6 #OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY=43200 -#TOKENS_CONVERTER_FACTORY=0xd1C58a7a8809fe48dD4BE2a43e2a604a68e51503 #VALIDATORS_REGISTRY=0xb97036A26259B7147018913bD58a774cf91acf25 #GNO_TOKEN=0x19C653Da7c37c66208fbfbE8908A5051B57b4C70 # gnosis -#META_VAULT_FACTORY_OWNER=0x2685C0e39EEAAd383fB71ec3F493991d532A87ae #OS_TOKEN_REDEEMER_OWNER=0x2685C0e39EEAAd383fB71ec3F493991d532A87ae #OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY=43200 -#TOKENS_CONVERTER_FACTORY=0xdeC758323bF734c72F909965841fC2dba3C8c007 #VALIDATORS_REGISTRY=0x0B98057eA310F4d31F2a452B414647007d1645d9 #GNO_TOKEN=0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb \ No newline at end of file diff --git a/contracts/interfaces/IEthMetaVault.sol b/contracts/interfaces/IEthMetaVault.sol index a065f1af..9e1ddb35 100644 --- a/contracts/interfaces/IEthMetaVault.sol +++ b/contracts/interfaces/IEthMetaVault.sol @@ -3,64 +3,14 @@ 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 {IMulticall} from "./IMulticall.sol"; +import {IMetaVault} from "./IMetaVault.sol"; /** * @title IEthMetaVault * @author StakeWise * @notice Defines the interface for the EthMetaVault contract */ -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 curatorsRegistry The address of the CuratorsRegistry contract - * @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 curatorsRegistry; - 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; - } - +interface IEthMetaVault is IMetaVault { /** * @notice Initializes or upgrades the EthMetaVault contract. Must transfer security deposit during the deployment. * @param params The encoded parameters for initializing the EthVault contract diff --git a/contracts/interfaces/IEthMetaVaultFactory.sol b/contracts/interfaces/IEthMetaVaultFactory.sol index 7c2b9f99..7757e608 100644 --- a/contracts/interfaces/IEthMetaVaultFactory.sol +++ b/contracts/interfaces/IEthMetaVaultFactory.sol @@ -10,12 +10,11 @@ pragma solidity ^0.8.22; interface IEthMetaVaultFactory { /** * @notice Event emitted on a MetaVault creation - * @param caller The address of the factory caller * @param admin The address of the Vault admin * @param vault The address of the created Vault * @param params The encoded parameters for initializing the Vault contract */ - event MetaVaultCreated(address indexed caller, address indexed admin, address indexed vault, bytes params); + event MetaVaultCreated(address indexed admin, address indexed vault, bytes params); /** * @notice The address of the Vault implementation contract used for proxy creation @@ -31,8 +30,7 @@ interface IEthMetaVaultFactory { /** * @notice Create Vault. Must transfer security deposit together with a call. - * @param admin The address of the Vault admin * @param params The encoded parameters for initializing the Vault contract */ - function createVault(address admin, bytes calldata params) external payable returns (address vault); + function createVault(bytes calldata params) external payable returns (address vault); } diff --git a/contracts/interfaces/IEthPrivMetaVault.sol b/contracts/interfaces/IEthPrivMetaVault.sol new file mode 100644 index 00000000..d45d3edc --- /dev/null +++ b/contracts/interfaces/IEthPrivMetaVault.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IVaultWhitelist} from "./IVaultWhitelist.sol"; +import {IEthMetaVault} from "./IEthMetaVault.sol"; + +/** + * @title IEthPrivMetaVault + * @author StakeWise + * @notice Defines the interface for the EthPrivMetaVault contract + */ +interface IEthPrivMetaVault is IEthMetaVault, IVaultWhitelist {} diff --git a/contracts/interfaces/IGnoMetaVault.sol b/contracts/interfaces/IGnoMetaVault.sol index 3bd02008..439b19a3 100644 --- a/contracts/interfaces/IGnoMetaVault.sol +++ b/contracts/interfaces/IGnoMetaVault.sol @@ -2,71 +2,19 @@ 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 {IMetaVault} from "./IMetaVault.sol"; /** * @title IGnoMetaVault * @author StakeWise * @notice Defines the interface for the GnoMetaVault contract */ -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 curatorsRegistry The address of the CuratorsRegistry contract - * @param gnoToken The address of the GNO token - * @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 curatorsRegistry; - address gnoToken; - 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; - } - +interface IGnoMetaVault is IMetaVault { /** * @notice Initializes or upgrades the GnoMetaVault contract. Must transfer security deposit during the deployment. * @param params The encoded parameters for initializing the GnoVault contract */ - function initialize(bytes calldata params) external payable; + function initialize(bytes calldata params) external; /** * @notice Deposit GNO to the Vault diff --git a/contracts/interfaces/IGnoMetaVaultFactory.sol b/contracts/interfaces/IGnoMetaVaultFactory.sol index 6996fcd9..d58f680a 100644 --- a/contracts/interfaces/IGnoMetaVaultFactory.sol +++ b/contracts/interfaces/IGnoMetaVaultFactory.sol @@ -10,12 +10,11 @@ pragma solidity ^0.8.22; interface IGnoMetaVaultFactory { /** * @notice Event emitted on a MetaVault creation - * @param caller The address of the factory caller * @param admin The address of the Vault admin * @param vault The address of the created Vault * @param params The encoded parameters for initializing the Vault contract */ - event MetaVaultCreated(address indexed caller, address indexed admin, address indexed vault, bytes params); + event MetaVaultCreated(address indexed admin, address indexed vault, bytes params); /** * @notice The address of the Vault implementation contract used for proxy creation @@ -31,8 +30,7 @@ interface IGnoMetaVaultFactory { /** * @notice Create Vault. Must transfer security deposit together with a call. - * @param admin The address of the Vault admin * @param params The encoded parameters for initializing the Vault contract */ - function createVault(address admin, bytes calldata params) external returns (address vault); + function createVault(bytes calldata params) external returns (address vault); } diff --git a/contracts/interfaces/IGnoPrivMetaVault.sol b/contracts/interfaces/IGnoPrivMetaVault.sol new file mode 100644 index 00000000..6ea8513e --- /dev/null +++ b/contracts/interfaces/IGnoPrivMetaVault.sol @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 00000000..49be4645 --- /dev/null +++ b/contracts/interfaces/IMetaVault.sol @@ -0,0 +1,86 @@ +// 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 b4c36f68..609532c3 100644 --- a/contracts/interfaces/IOsTokenRedeemer.sol +++ b/contracts/interfaces/IOsTokenRedeemer.sol @@ -25,7 +25,7 @@ interface IOsTokenRedeemer is IMulticall { * @param vault The address of the Vault * @param owner The address of the position owner * @param leafShares The amount of OsToken shares used to calculate the merkle leaf - * @param osTokenSharesToRedeem The amount of OsToken shares to redeem + * @param sharesToRedeem The amount of OsToken shares to redeem */ struct OsTokenPosition { address vault; @@ -45,28 +45,7 @@ interface IOsTokenRedeemer is IMulticall { * @param merkleRoot The Merkle root of the redeemable positions * @param ipfsHash The IPFS hash of the redeemable positions */ - event RedeemablePositionsProposed(bytes32 indexed merkleRoot, string ipfsHash); - - /** - * @notice Event emitted when the pending redeemable positions are accepted - * @param merkleRoot The Merkle root of the accepted redeemable positions - * @param ipfsHash The IPFS hash of the accepted redeemable positions - */ - event RedeemablePositionsAccepted(bytes32 indexed merkleRoot, string ipfsHash); - - /** - * @notice Event emitted when the new redeemable positions are denied - * @param merkleRoot The Merkle root of the denied redeemable positions - * @param ipfsHash The IPFS hash of the denied redeemable positions - */ - event RedeemablePositionsDenied(bytes32 indexed merkleRoot, string ipfsHash); - - /** - * @notice Event emitted when the redeemable positions are removed - * @param merkleRoot The Merkle root of the removed redeemable positions - * @param ipfsHash The IPFS hash of the removed redeemable positions - */ - event RedeemablePositionsRemoved(bytes32 indexed merkleRoot, string ipfsHash); + event RedeemablePositionsUpdated(bytes32 indexed merkleRoot, string ipfsHash); /** * @notice Event emitted on shares added to the exit queue @@ -174,7 +153,7 @@ interface IOsTokenRedeemer is IMulticall { function exitQueueTimestamp() external view returns (uint256); /** - * @notice The address that can propose redeemable OsToken positions + * @notice The address authorized to redeem OsToken positions * @return The address of the positions manager */ function positionsManager() external view returns (address); @@ -196,6 +175,19 @@ interface IOsTokenRedeemer is IMulticall { view returns (uint256 queuedShares, uint256 unclaimedAssets, uint256 totalTickets); + /** + * @notice Gets the cumulative tickets in the exit queue + * @return The cumulative tickets in the exit queue + */ + function getExitQueueCumulativeTickets() external view returns (uint256); + + /** + * @notice Calculates the missing assets in the exit queue for a target cumulative tickets. + * @param targetCumulativeTickets The target cumulative tickets in the exit queue + * @return missingAssets The number of missing assets in the exit queue + */ + function getExitQueueMissingAssets(uint256 targetCumulativeTickets) external view returns (uint256 missingAssets); + /** * @notice Checks if the exit queue can be processed * @return True if the exit queue can be processed, false otherwise @@ -209,13 +201,6 @@ interface IOsTokenRedeemer is IMulticall { */ function redeemablePositions() external view returns (bytes32 merkleRoot, string memory ipfsHash); - /** - * @notice The pending redeemable positions Merkle root and IPFS hash that is waiting to be accepted - * @return merkleRoot The Merkle root of the pending redeemable positions - * @return ipfsHash The IPFS hash of the pending redeemable positions - */ - function pendingRedeemablePositions() external view returns (bytes32 merkleRoot, string memory ipfsHash); - /** * @notice Gets the index of the exit queue for a given position ticket. * @param positionTicket The position ticket to search for @@ -244,25 +229,10 @@ interface IOsTokenRedeemer is IMulticall { function setPositionsManager(address positionsManager_) external; /** - * @notice Proposes new redeemable positions. Can only be called by the positions manager. - * @param newPositions The new redeemable positions to propose + * @notice Set new redeemable positions. Can only be called by the owner. + * @param newPositions The new redeemable positions */ - function proposeRedeemablePositions(RedeemablePositions calldata newPositions) external; - - /** - * @notice Accepts the pending redeemable positions. Can only be called by the owner. - */ - function acceptRedeemablePositions() external; - - /** - * @notice Denies the pending redeemable positions. Can only be called by the owner. - */ - function denyRedeemablePositions() external; - - /** - * @notice Removes the redeemable positions. Can only be called by the owner. - */ - function removeRedeemablePositions() external; + function setRedeemablePositions(RedeemablePositions calldata newPositions) external; /** * @notice Permit OsToken shares to be used for redemption. @@ -289,6 +259,24 @@ interface IOsTokenRedeemer is IMulticall { */ function claimExitedAssets(uint256 positionTicket, uint256 exitQueueIndex) external; + /** + * @notice Redeem OsToken shares from a specific sub-vault. Can only be called by the meta vault. + * @param subVault The address of the sub-vault + * @param osTokenShares The number of OsToken shares to redeem + * @return The amount of redeemed assets + */ + function redeemSubVaultOsToken(address subVault, uint256 osTokenShares) external returns (uint256); + + /** + * @notice Redeem assets from the sub-vaults to the meta vault. Can only be called by the positions manager. + * @param metaVault The address of the meta vault + * @param assetsToRedeem The number of assets to redeem + * @return totalRedeemedAssets The total number of redeemed assets + */ + function redeemSubVaultsAssets(address metaVault, uint256 assetsToRedeem) + external + returns (uint256 totalRedeemedAssets); + /** * @notice Redeem OsToken shares from the vault positions. * @param positions The array of OsToken positions to redeem diff --git a/contracts/interfaces/IOsTokenVaultEscrow.sol b/contracts/interfaces/IOsTokenVaultEscrow.sol index 459b9b9d..27aaf7f6 100644 --- a/contracts/interfaces/IOsTokenVaultEscrow.sol +++ b/contracts/interfaces/IOsTokenVaultEscrow.sol @@ -192,8 +192,7 @@ interface IOsTokenVaultEscrow is IMulticall { * @param osTokenShares The amount of osToken shares to redeem * @param receiver The address of the receiver of the redeemed assets */ - function redeemOsToken(address vault, uint256 exitPositionTicket, uint256 osTokenShares, address receiver) - external; + function redeemOsToken(address vault, uint256 exitPositionTicket, uint256 osTokenShares, address receiver) external; /** * @notice Updates the authenticator. Can only be called by the owner. diff --git a/contracts/interfaces/IValidatorsChecker.sol b/contracts/interfaces/IValidatorsChecker.sol index 83b6e18d..e6255b06 100644 --- a/contracts/interfaces/IValidatorsChecker.sol +++ b/contracts/interfaces/IValidatorsChecker.sol @@ -59,13 +59,16 @@ interface IValidatorsChecker is IMulticall { * @notice Function for getting the exit queue missing assets * @param vault The address of the vault * @param withdrawingAssets The amount of assets currently being withdrawn from validators + * @param redemptionAssets The amount of assets to be redeemed * @param targetCumulativeTickets The target cumulative tickets * @return missingAssets The exit queue missing assets */ - function getExitQueueMissingAssets(address vault, uint256 withdrawingAssets, uint256 targetCumulativeTickets) - external - view - returns (uint256 missingAssets); + function getExitQueueMissingAssets( + address vault, + uint256 withdrawingAssets, + uint256 redemptionAssets, + uint256 targetCumulativeTickets + ) external view returns (uint256 missingAssets); /** * @notice Function for checking validators manager signature diff --git a/contracts/interfaces/IVaultSubVaults.sol b/contracts/interfaces/IVaultSubVaults.sol index 032d09a3..1d79a8a6 100644 --- a/contracts/interfaces/IVaultSubVaults.sol +++ b/contracts/interfaces/IVaultSubVaults.sol @@ -49,6 +49,20 @@ interface IVaultSubVaults { */ 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 * @param caller The address of the caller @@ -82,6 +96,12 @@ interface IVaultSubVaults { */ 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 @@ -125,6 +145,18 @@ interface IVaultSubVaults { */ 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 + */ + function rejectMetaSubVault(address metaSubVault) external; + /** * @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. diff --git a/contracts/interfaces/IVaultValidators.sol b/contracts/interfaces/IVaultValidators.sol index 156d6c84..37fae766 100644 --- a/contracts/interfaces/IVaultValidators.sol +++ b/contracts/interfaces/IVaultValidators.sol @@ -100,9 +100,7 @@ interface IVaultValidators is IVaultAdmin, IVaultState { * @param validators The concatenated validators data * @param validatorsManagerSignature The optional signature from the validators manager */ - function withdrawValidators(bytes calldata validators, bytes calldata validatorsManagerSignature) - external - payable; + function withdrawValidators(bytes calldata validators, bytes calldata validatorsManagerSignature) external payable; /** * @notice Function for consolidating single or multiple validators diff --git a/contracts/keeper/KeeperRewards.sol b/contracts/keeper/KeeperRewards.sol index 8a15e238..0f3f668a 100644 --- a/contracts/keeper/KeeperRewards.sol +++ b/contracts/keeper/KeeperRewards.sol @@ -178,13 +178,11 @@ abstract contract KeeperRewards is KeeperOracles, IKeeperRewards { } // verify the proof - if ( - !MerkleProof.verifyCalldata( + if (!MerkleProof.verifyCalldata( params.proof, params.rewardsRoot, keccak256(bytes.concat(keccak256(abi.encode(msg.sender, params.reward, params.unlockedMevReward)))) - ) - ) { + )) { revert Errors.InvalidProof(); } diff --git a/contracts/keeper/KeeperValidators.sol b/contracts/keeper/KeeperValidators.sol index d0279593..70342a2a 100644 --- a/contracts/keeper/KeeperValidators.sol +++ b/contracts/keeper/KeeperValidators.sol @@ -80,7 +80,9 @@ abstract contract KeeperValidators is KeeperOracles, KeeperRewards, IKeeperValid string calldata exitSignaturesIpfsHash, bytes calldata oraclesSignatures ) external override { - if (!(_vaultsRegistry.vaults(vault) && isCollateralized(vault))) revert Errors.InvalidVault(); + if (!(_vaultsRegistry.vaults(vault) && isCollateralized(vault))) { + revert Errors.InvalidVault(); + } if (deadline < block.timestamp) revert Errors.DeadlineExpired(); // SLOAD to memory diff --git a/contracts/libraries/EIP712Utils.sol b/contracts/libraries/EIP712Utils.sol index e5470f1d..cea132a4 100644 --- a/contracts/libraries/EIP712Utils.sol +++ b/contracts/libraries/EIP712Utils.sol @@ -20,8 +20,9 @@ library EIP712Utils { * @return The hash of the EIP712 typed data */ function computeDomainSeparator(string memory name, address verifyingContract) external view returns (bytes32) { - return keccak256( - abi.encode(_domainTypeHash, keccak256(bytes(name)), _versionHash, block.chainid, verifyingContract) - ); + return + keccak256( + abi.encode(_domainTypeHash, keccak256(bytes(name)), _versionHash, block.chainid, verifyingContract) + ); } } diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index 73e6ce13..07f61758 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -61,6 +61,5 @@ library Errors { error InvalidCurator(); error RewardsNonceIsHigher(); error InvalidRedeemablePositions(); - error RedeemablePositionsProposed(); error InvalidDelay(); } diff --git a/contracts/libraries/SubVaultExits.sol b/contracts/libraries/SubVaultExits.sol new file mode 100644 index 00000000..4a3bcf94 --- /dev/null +++ b/contracts/libraries/SubVaultExits.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; +import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol"; +import {Errors} from "./Errors.sol"; + +/** + * @title SubVaultExits + * @author StakeWise + * @notice Includes the common functionality for managing the meta vault sub-vaults exits + */ +library SubVaultExits { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + /** + * @dev Fetches the sub-vault exit data + * @param subVaultsExits The mapping of sub-vault exits queues + * @param vault The address of the sub-vault + * @return positionTicket The position ticket of the sub-vault + * @return shares The shares to be exited from the sub-vault + */ + function peekSubVaultExit( + mapping(address vault => DoubleEndedQueue.Bytes32Deque) storage subVaultsExits, + address vault + ) internal view returns (uint160 positionTicket, uint96 shares) { + if (subVaultsExits[vault].empty()) { + return (0, 0); + } + bytes32 packed = subVaultsExits[vault].front(); + positionTicket = uint160(Packing.extract_32_20(packed, 0)); + shares = uint96(Packing.extract_32_12(packed, 20)); + } + + /** + * @dev Stores the sub-vault exit data + * @param subVaultsExits The mapping of sub-vault exits queues + * @param vault The address of the sub-vault + * @param positionTicket The position ticket of the sub-vault + * @param shares The shares to be exited from the sub-vault + * @param front Whether to insert the exit data at the front of the queue + */ + function pushSubVaultExit( + mapping(address vault => DoubleEndedQueue.Bytes32Deque) storage subVaultsExits, + address vault, + uint160 positionTicket, + uint96 shares, + bool front + ) internal { + if (shares == 0) revert Errors.InvalidShares(); + bytes32 packed = Packing.pack_20_12(bytes20(positionTicket), bytes12(shares)); + if (front) { + subVaultsExits[vault].pushFront(packed); + } else { + subVaultsExits[vault].pushBack(packed); + } + } + + /** + * @dev Removes the sub-vault exit data + * @param subVaultsExits The mapping of sub-vault exits queues + * @param vault The address of the sub-vault + * @return positionTicket The position ticket of the sub-vault + * @return shares The shares to be exited from the sub-vault + */ + function popSubVaultExit( + mapping(address vault => DoubleEndedQueue.Bytes32Deque) storage subVaultsExits, + address vault + ) internal returns (uint160 positionTicket, uint96 shares) { + bytes32 packed = subVaultsExits[vault].popFront(); + positionTicket = uint160(Packing.extract_32_20(packed, 0)); + shares = uint96(Packing.extract_32_12(packed, 20)); + } +} diff --git a/contracts/libraries/SubVaultUtils.sol b/contracts/libraries/SubVaultUtils.sol new file mode 100644 index 00000000..a60d8bb8 --- /dev/null +++ b/contracts/libraries/SubVaultUtils.sol @@ -0,0 +1,340 @@ +// 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 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 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); + } + + /** + * @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 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); + } + } +} diff --git a/contracts/tokens/EthOsTokenRedeemer.sol b/contracts/tokens/EthOsTokenRedeemer.sol index a32d88fc..52cb93d1 100644 --- a/contracts/tokens/EthOsTokenRedeemer.sol +++ b/contracts/tokens/EthOsTokenRedeemer.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.22; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IEthOsTokenRedeemer} from "../interfaces/IEthOsTokenRedeemer.sol"; import {OsTokenRedeemer} from "./OsTokenRedeemer.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /** * @title EthOsTokenRedeemer @@ -15,14 +15,21 @@ import {OsTokenRedeemer} from "./OsTokenRedeemer.sol"; contract EthOsTokenRedeemer is IEthOsTokenRedeemer, ReentrancyGuard, OsTokenRedeemer { /** * @dev Constructor + * @param vaultsRegistry_ The address of the VaultsRegistry contract * @param osToken_ The address of the OsToken contract * @param osTokenVaultController_ The address of the OsTokenVaultController contract * @param owner_ The address of the owner * @param exitQueueUpdateDelay_ The delay in seconds for exit queue updates */ - constructor(address osToken_, address osTokenVaultController_, address owner_, uint256 exitQueueUpdateDelay_) + constructor( + address vaultsRegistry_, + address osToken_, + address osTokenVaultController_, + address owner_, + uint256 exitQueueUpdateDelay_ + ) ReentrancyGuard() - OsTokenRedeemer(osToken_, osTokenVaultController_, owner_, exitQueueUpdateDelay_) + OsTokenRedeemer(vaultsRegistry_, osToken_, osTokenVaultController_, owner_, exitQueueUpdateDelay_) {} /// @inheritdoc IEthOsTokenRedeemer @@ -31,8 +38,8 @@ contract EthOsTokenRedeemer is IEthOsTokenRedeemer, ReentrancyGuard, OsTokenRede } /// @inheritdoc OsTokenRedeemer - function _availableAssets() internal view override returns (uint256) { - return address(this).balance; + function _getAssets(address account) internal view override returns (uint256) { + return account.balance; } /// @inheritdoc OsTokenRedeemer diff --git a/contracts/tokens/EthOsTokenVaultEscrow.sol b/contracts/tokens/EthOsTokenVaultEscrow.sol index 7d06665c..415b29dc 100644 --- a/contracts/tokens/EthOsTokenVaultEscrow.sol +++ b/contracts/tokens/EthOsTokenVaultEscrow.sol @@ -39,12 +39,7 @@ contract EthOsTokenVaultEscrow is ReentrancyGuard, OsTokenVaultEscrow { ) ReentrancyGuard() OsTokenVaultEscrow( - osTokenVaultController, - osTokenConfig, - initialOwner, - _authenticator, - _liqThresholdPercent, - _liqBonusPercent + osTokenVaultController, osTokenConfig, initialOwner, _authenticator, _liqThresholdPercent, _liqBonusPercent ) {} diff --git a/contracts/tokens/GnoOsTokenRedeemer.sol b/contracts/tokens/GnoOsTokenRedeemer.sol index ed0cde0c..591343e1 100644 --- a/contracts/tokens/GnoOsTokenRedeemer.sol +++ b/contracts/tokens/GnoOsTokenRedeemer.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.22; +import {IGnoOsTokenRedeemer} from "../interfaces/IGnoOsTokenRedeemer.sol"; +import {OsTokenRedeemer} from "./OsTokenRedeemer.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IGnoOsTokenRedeemer} from "../interfaces/IGnoOsTokenRedeemer.sol"; -import {OsTokenRedeemer} from "./OsTokenRedeemer.sol"; /** * @title GnoOsTokenRedeemer @@ -19,6 +19,7 @@ contract GnoOsTokenRedeemer is IGnoOsTokenRedeemer, OsTokenRedeemer { /** * @dev Constructor * @param gnoToken_ The address of the GNO token contract + * @param vaultsRegistry_ The address of the VaultsRegistry contract * @param osToken_ The address of the OsToken contract * @param osTokenVaultController_ The address of the OsTokenVaultController contract * @param owner_ The address of the owner @@ -26,11 +27,12 @@ contract GnoOsTokenRedeemer is IGnoOsTokenRedeemer, OsTokenRedeemer { */ constructor( address gnoToken_, + address vaultsRegistry_, address osToken_, address osTokenVaultController_, address owner_, uint256 exitQueueUpdateDelay_ - ) OsTokenRedeemer(osToken_, osTokenVaultController_, owner_, exitQueueUpdateDelay_) { + ) OsTokenRedeemer(vaultsRegistry_, osToken_, osTokenVaultController_, owner_, exitQueueUpdateDelay_) { _gnoToken = IERC20(gnoToken_); } @@ -50,8 +52,8 @@ contract GnoOsTokenRedeemer is IGnoOsTokenRedeemer, OsTokenRedeemer { } /// @inheritdoc OsTokenRedeemer - function _availableAssets() internal view override returns (uint256) { - return _gnoToken.balanceOf(address(this)); + function _getAssets(address account) internal view override returns (uint256) { + return _gnoToken.balanceOf(account); } /// @inheritdoc OsTokenRedeemer diff --git a/contracts/tokens/GnoOsTokenVaultEscrow.sol b/contracts/tokens/GnoOsTokenVaultEscrow.sol index 3cbfb924..0ce3b8e0 100644 --- a/contracts/tokens/GnoOsTokenVaultEscrow.sol +++ b/contracts/tokens/GnoOsTokenVaultEscrow.sol @@ -34,12 +34,7 @@ contract GnoOsTokenVaultEscrow is OsTokenVaultEscrow { address gnoToken ) OsTokenVaultEscrow( - osTokenVaultController, - osTokenConfig, - initialOwner, - _authenticator, - _liqThresholdPercent, - _liqBonusPercent + osTokenVaultController, osTokenConfig, initialOwner, _authenticator, _liqThresholdPercent, _liqBonusPercent ) { _gnoToken = IERC20(gnoToken); diff --git a/contracts/tokens/OsTokenFlashLoans.sol b/contracts/tokens/OsTokenFlashLoans.sol index d914dbdd..81d43fa1 100644 --- a/contracts/tokens/OsTokenFlashLoans.sol +++ b/contracts/tokens/OsTokenFlashLoans.sol @@ -27,6 +27,7 @@ contract OsTokenFlashLoans is ReentrancyGuard, IOsTokenFlashLoans { } /// @inheritdoc IOsTokenFlashLoans + // slither-disable-start reentrancy-balance function flashLoan(uint256 osTokenShares, bytes memory userData) external override nonReentrant { // check if not more than max flash loan amount requested if (osTokenShares == 0 || osTokenShares > _maxFlashLoanAmount) { @@ -56,4 +57,5 @@ contract OsTokenFlashLoans is ReentrancyGuard, IOsTokenFlashLoans { // emit event emit OsTokenFlashLoan(msg.sender, osTokenShares); } + // slither-disable-end reentrancy-balance } diff --git a/contracts/tokens/OsTokenRedeemer.sol b/contracts/tokens/OsTokenRedeemer.sol index c78cc672..5bee73a3 100644 --- a/contracts/tokens/OsTokenRedeemer.sol +++ b/contracts/tokens/OsTokenRedeemer.sol @@ -2,19 +2,22 @@ pragma solidity ^0.8.22; +import {Multicall} from "../base/Multicall.sol"; +import {IMetaVault} from "../interfaces/IMetaVault.sol"; +import {IOsTokenRedeemer} from "../interfaces/IOsTokenRedeemer.sol"; +import {IOsTokenVaultController} from "../interfaces/IOsTokenVaultController.sol"; +import {IVaultOsToken} from "../interfaces/IVaultOsToken.sol"; +import {IVaultSubVaults} from "../interfaces/IVaultSubVaults.sol"; +import {IVaultsRegistry} from "../interfaces/IVaultsRegistry.sol"; +import {Errors} from "../libraries/Errors.sol"; +import {ExitQueue} from "../libraries/ExitQueue.sol"; +import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +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 {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import {IVaultOsToken} from "../interfaces/IVaultOsToken.sol"; -import {IOsTokenVaultController} from "../interfaces/IOsTokenVaultController.sol"; -import {IOsTokenRedeemer} from "../interfaces/IOsTokenRedeemer.sol"; -import {Errors} from "../libraries/Errors.sol"; -import {ExitQueue} from "../libraries/ExitQueue.sol"; -import {Multicall} from "../base/Multicall.sol"; /** * @title OsTokenRedeemer @@ -22,6 +25,7 @@ import {Multicall} from "../base/Multicall.sol"; * @notice This contract is used to redeem OsTokens for the underlying asset. */ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { + IVaultsRegistry private immutable _vaultsRegistry; IERC20 private immutable _osToken; IOsTokenVaultController private immutable _osTokenVaultController; @@ -62,29 +66,34 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { uint256 public override exitQueueTimestamp; RedeemablePositions private _redeemablePositions; - RedeemablePositions private _pendingRedeemablePositions; ExitQueue.History private _exitQueue; /** * @dev Constructor + * @param vaultsRegistry_ The address of the VaultsRegistry contract * @param osToken_ The address of the OsToken contract * @param osTokenVaultController_ The address of the OsTokenVaultController contract * @param owner_ The address of the owner * @param exitQueueUpdateDelay_ The delay in seconds for exit queue updates */ - constructor(address osToken_, address osTokenVaultController_, address owner_, uint256 exitQueueUpdateDelay_) - Ownable(owner_) - { + constructor( + address vaultsRegistry_, + address osToken_, + address osTokenVaultController_, + address owner_, + uint256 exitQueueUpdateDelay_ + ) Ownable(owner_) { if (exitQueueUpdateDelay_ > type(uint64).max) { revert Errors.InvalidDelay(); } + _vaultsRegistry = IVaultsRegistry(vaultsRegistry_); _osToken = IERC20(osToken_); _osTokenVaultController = IOsTokenVaultController(osTokenVaultController_); exitQueueUpdateDelay = exitQueueUpdateDelay_; } /// @inheritdoc IOsTokenRedeemer - function getExitQueueData() external view override returns (uint256, uint256, uint256) { + function getExitQueueData() public view override returns (uint256, uint256, uint256) { return ( queuedShares, unclaimedAssets + redeemedAssets + swappedAssets, @@ -93,15 +102,15 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } /// @inheritdoc IOsTokenRedeemer - function redeemablePositions() external view override returns (bytes32 merkleRoot, string memory ipfsHash) { - merkleRoot = _redeemablePositions.merkleRoot; - ipfsHash = _redeemablePositions.ipfsHash; + function getExitQueueCumulativeTickets() external view override returns (uint256) { + (uint256 _queuedShares,, uint256 totalTickets) = getExitQueueData(); + return totalTickets + _queuedShares; } /// @inheritdoc IOsTokenRedeemer - function pendingRedeemablePositions() external view override returns (bytes32 merkleRoot, string memory ipfsHash) { - merkleRoot = _pendingRedeemablePositions.merkleRoot; - ipfsHash = _pendingRedeemablePositions.ipfsHash; + function redeemablePositions() external view override returns (bytes32 merkleRoot, string memory ipfsHash) { + merkleRoot = _redeemablePositions.merkleRoot; + ipfsHash = _redeemablePositions.ipfsHash; } /// @inheritdoc IOsTokenRedeemer @@ -152,73 +161,49 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } /// @inheritdoc IOsTokenRedeemer - function proposeRedeemablePositions(RedeemablePositions calldata newPositions) external override { - if (msg.sender != positionsManager) { - revert Errors.AccessDenied(); - } - if (newPositions.merkleRoot == bytes32(0) || bytes(newPositions.ipfsHash).length == 0) { - revert Errors.InvalidRedeemablePositions(); - } - + function getExitQueueMissingAssets(uint256 targetCumulativeTickets) + external + view + override + returns (uint256 missingAssets) + { // SLOAD to memory - RedeemablePositions memory pendingPositions = _pendingRedeemablePositions; - if (pendingPositions.merkleRoot != bytes32(0) || bytes(pendingPositions.ipfsHash).length != 0) { - revert Errors.RedeemablePositionsProposed(); - } + (uint256 _queuedShares, uint256 _unclaimedAssets, uint256 totalTickets) = getExitQueueData(); - // SLOAD to memory - RedeemablePositions memory currentPositions = _redeemablePositions; - if (newPositions.merkleRoot == currentPositions.merkleRoot || bytes(pendingPositions.ipfsHash).length != 0) { - revert Errors.ValueNotChanged(); + // check whether already covered + if (totalTickets >= targetCumulativeTickets) { + return 0; } - // update state - _pendingRedeemablePositions = newPositions; + // calculate the amount of tickets that need to be covered + uint256 totalTicketsToCover = targetCumulativeTickets - totalTickets; - // emit event - emit RedeemablePositionsProposed(newPositions.merkleRoot, newPositions.ipfsHash); + // calculate missing assets + missingAssets = _osTokenVaultController.convertToAssets(Math.min(totalTicketsToCover, _queuedShares)); + + // check whether there is enough available assets + uint256 availableAssets = _getAssets(address(this)) - _unclaimedAssets; + return availableAssets >= missingAssets ? 0 : missingAssets - availableAssets; } /// @inheritdoc IOsTokenRedeemer - function acceptRedeemablePositions() external override onlyOwner { - // SLOAD to memory - RedeemablePositions memory newPositions = _pendingRedeemablePositions; + function setRedeemablePositions(RedeemablePositions calldata newPositions) external override onlyOwner { if (newPositions.merkleRoot == bytes32(0) || bytes(newPositions.ipfsHash).length == 0) { revert Errors.InvalidRedeemablePositions(); } + // SLOAD to memory + RedeemablePositions memory currentPositions = _redeemablePositions; + if (newPositions.merkleRoot == currentPositions.merkleRoot) { + revert Errors.ValueNotChanged(); + } + // update state nonce += 1; _redeemablePositions = newPositions; - delete _pendingRedeemablePositions; // emit event - emit RedeemablePositionsAccepted(newPositions.merkleRoot, newPositions.ipfsHash); - } - - /// @inheritdoc IOsTokenRedeemer - function denyRedeemablePositions() external override onlyOwner { - // SLOAD to memory - RedeemablePositions memory newPositions = _pendingRedeemablePositions; - if (newPositions.merkleRoot == bytes32(0)) { - return; - } - delete _pendingRedeemablePositions; - - // emit event - emit RedeemablePositionsDenied(newPositions.merkleRoot, newPositions.ipfsHash); - } - - /// @inheritdoc IOsTokenRedeemer - function removeRedeemablePositions() external override onlyOwner { - // SLOAD to memory - RedeemablePositions memory positions = _redeemablePositions; - if (positions.merkleRoot == bytes32(0)) { - return; - } - - delete _redeemablePositions; - emit RedeemablePositionsRemoved(positions.merkleRoot, positions.ipfsHash); + emit RedeemablePositionsUpdated(newPositions.merkleRoot, newPositions.ipfsHash); } /// @inheritdoc IOsTokenRedeemer @@ -280,11 +265,51 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { emit ExitedAssetsClaimed(msg.sender, positionTicket, newPositionTicket, exitedAssets); } + /// @inheritdoc IOsTokenRedeemer + function redeemSubVaultsAssets(address metaVault, uint256 assetsToRedeem) + external + override + returns (uint256 totalRedeemedAssets) + { + if (msg.sender != positionsManager) { + revert Errors.AccessDenied(); + } + + if (!_isMetaVault(metaVault)) { + revert Errors.InvalidVault(); + } + + return IMetaVault(metaVault).redeemSubVaultsAssets(assetsToRedeem); + } + + /// @inheritdoc IOsTokenRedeemer + function redeemSubVaultOsToken(address subVault, uint256 osTokenShares) external override returns (uint256) { + if (!_vaultsRegistry.vaults(subVault) || !_isMetaVault(msg.sender)) { + revert Errors.AccessDenied(); + } + if (osTokenShares == 0) { + revert Errors.InvalidShares(); + } + + uint256 _vaultAssetsBefore = _getAssets(msg.sender); + + // redeem osToken shares from sub vault to meta vault + IVaultOsToken(subVault).redeemOsToken(osTokenShares, msg.sender, msg.sender); + + // calculate redeemed assets + return _getAssets(msg.sender) - _vaultAssetsBefore; + } + + /// @inheritdoc IOsTokenRedeemer function redeemOsTokenPositions( OsTokenPosition[] memory positions, bytes32[] calldata proof, bool[] calldata proofFlags ) external override { + if (msg.sender != positionsManager) { + revert Errors.AccessDenied(); + } + // SLOAD to memory uint256 _queuedShares = queuedShares; uint256 positionsCount = positions.length; @@ -318,8 +343,12 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { // update state if (position.sharesToRedeem > 0) { - _queuedShares -= position.sharesToRedeem; - leafToProcessedShares[leaf] = processedPositionShares + position.sharesToRedeem; + unchecked { + // position.sharesToRedeem <= _queuedShares checked above + _queuedShares -= position.sharesToRedeem; + // cannot realistically overflow + leafToProcessedShares[leaf] = processedPositionShares + position.sharesToRedeem; + } } unchecked { @@ -338,7 +367,7 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } // redeem positions - uint256 availableAssetsBefore = _availableAssets(); + uint256 availableAssetsBefore = _getAssets(address(this)); for (uint256 i = 0; i < positionsCount;) { OsTokenPosition memory position = positions[i]; if (position.sharesToRedeem > 0) { @@ -353,7 +382,7 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } // calculate processed assets and shares - uint256 processedAssets = _availableAssets() - availableAssetsBefore; + uint256 processedAssets = _getAssets(address(this)) - availableAssetsBefore; uint256 processedShares = queuedShares - _queuedShares; // update state @@ -371,6 +400,7 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { revert Errors.TooEarlyUpdate(); } + // update state uint256 processedShares = swappedShares + redeemedShares; uint256 processedAssets = swappedAssets + redeemedAssets; swappedShares = 0; @@ -420,10 +450,30 @@ abstract contract OsTokenRedeemer is Ownable2Step, Multicall, IOsTokenRedeemer { } /** - * @dev Internal function that must be implemented to return the available assets for exit - * @return The amount of available assets for exit + * @dev Internal function to check whether the caller is a meta vault + * @param vault The address of the vault to check + * @return True if the caller is a meta vault, false otherwise + */ + function _isMetaVault(address vault) private view returns (bool) { + // must be a registered vault + if (!_vaultsRegistry.vaults(vault)) { + return false; + } + + // must be a meta vault + try IVaultSubVaults(vault).getSubVaults() { + return true; + } catch { + return false; + } + } + + /** + * @dev Internal function that must be implemented to return the account assets + * @param account The address of the account + * @return The amount of assets in the vault */ - function _availableAssets() internal view virtual returns (uint256); + function _getAssets(address account) internal view virtual returns (uint256); /** * @dev Internal function for transferring assets to the receiver diff --git a/contracts/tokens/OsTokenVaultEscrow.sol b/contracts/tokens/OsTokenVaultEscrow.sol index 0c0a4812..79ed71e6 100644 --- a/contracts/tokens/OsTokenVaultEscrow.sol +++ b/contracts/tokens/OsTokenVaultEscrow.sol @@ -106,9 +106,8 @@ abstract contract OsTokenVaultEscrow is Ownable2Step, Multicall, IOsTokenVaultEs if (position.owner == address(0)) revert Errors.InvalidPosition(); // claim exited assets - (uint256 leftTickets,, uint256 exitedAssets) = IVaultEnterExit(vault).calculateExitedAssets( - address(this), exitPositionTicket, timestamp, uint256(exitQueueIndex) - ); + (uint256 leftTickets,, uint256 exitedAssets) = IVaultEnterExit(vault) + .calculateExitedAssets(address(this), exitPositionTicket, timestamp, uint256(exitQueueIndex)); // the exit request must be fully processed (1 ticket could be a rounding error) if (leftTickets > 1) revert Errors.ExitRequestNotProcessed(); IVaultEnterExit(vault).claimExitedAssets(exitPositionTicket, timestamp, uint256(exitQueueIndex)); diff --git a/contracts/validators/DepositDataRegistry.sol b/contracts/validators/DepositDataRegistry.sol index 8b119ccd..aea029c2 100644 --- a/contracts/validators/DepositDataRegistry.sol +++ b/contracts/validators/DepositDataRegistry.sol @@ -96,13 +96,11 @@ contract DepositDataRegistry is Multicall, IDepositDataRegistry { bytes32 depositDataRoot = depositDataRoots[vault]; // check matches merkle root and next validator index - if ( - !MerkleProof.verifyCalldata( + if (!MerkleProof.verifyCalldata( proof, depositDataRoot, keccak256(bytes.concat(keccak256(abi.encode(keeperParams.validators, currentIndex)))) - ) - ) { + )) { revert Errors.InvalidProof(); } diff --git a/contracts/validators/ValidatorsChecker.sol b/contracts/validators/ValidatorsChecker.sol index 47a37c03..e7fc7cd9 100644 --- a/contracts/validators/ValidatorsChecker.sol +++ b/contracts/validators/ValidatorsChecker.sol @@ -72,12 +72,16 @@ abstract contract ValidatorsChecker is Multicall, IValidatorsChecker { } /// @inheritdoc IValidatorsChecker - function getExitQueueMissingAssets(address vault, uint256 withdrawingAssets, uint256 targetCumulativeTickets) - external - view - override - returns (uint256 missingAssets) - { + function getExitQueueMissingAssets( + address vault, + uint256 withdrawingAssets, + uint256 redemptionAssets, + uint256 targetCumulativeTickets + ) external view override returns (uint256 missingAssets) { + // start with redemption assets as missing assets + missingAssets = redemptionAssets; + + // fetch vault exit queue state ( uint128 queuedShares, uint128 unclaimedAssets, @@ -85,26 +89,23 @@ abstract contract ValidatorsChecker is Multicall, IValidatorsChecker { uint128 totalExitingAssets, uint256 totalTickets ) = IVaultState(vault).getExitQueueData(); - // check whether already covered - if (totalTickets >= targetCumulativeTickets) { - return 0; - } // calculate the amount of tickets that need to be covered - uint256 totalTicketsToCover = targetCumulativeTickets - totalTickets; + uint256 totalTicketsToCover; + if (totalTickets < targetCumulativeTickets) { + totalTicketsToCover = targetCumulativeTickets - totalTickets; + } // calculate missing assets from legacy exits - uint256 ticketsToCover; if (totalExitingTickets > 0) { - ticketsToCover = Math.min(totalTicketsToCover, totalExitingTickets); - missingAssets = Math.mulDiv(ticketsToCover, totalExitingAssets, totalExitingTickets); - totalTicketsToCover -= ticketsToCover; + uint256 legacyTicketsToCover = Math.min(totalTicketsToCover, totalExitingTickets); + missingAssets += Math.mulDiv(legacyTicketsToCover, totalExitingAssets, totalExitingTickets); + totalTicketsToCover -= legacyTicketsToCover; } // calculate missing assets from queued shares if (totalTicketsToCover > 0 && queuedShares > 0) { - ticketsToCover = Math.min(totalTicketsToCover, queuedShares); - missingAssets += IVaultState(vault).convertToAssets(ticketsToCover); + missingAssets += IVaultState(vault).convertToAssets(Math.min(totalTicketsToCover, queuedShares)); } // check whether there is enough available assets @@ -207,8 +208,9 @@ abstract contract ValidatorsChecker is Multicall, IValidatorsChecker { uint256 endIndex; for (uint256 i = 0; i < validatorsCount;) { endIndex += validatorLength; - leaves[params.proofIndexes[i]] = - keccak256(bytes.concat(keccak256(abi.encode(params.validators[startIndex:endIndex], currentIndex)))); + leaves[params.proofIndexes[i]] = keccak256( + bytes.concat(keccak256(abi.encode(params.validators[startIndex:endIndex], currentIndex))) + ); startIndex = endIndex; unchecked { diff --git a/contracts/vaults/base/MetaVault.sol b/contracts/vaults/base/MetaVault.sol new file mode 100644 index 00000000..2e4a0c39 --- /dev/null +++ b/contracts/vaults/base/MetaVault.sol @@ -0,0 +1,179 @@ +// 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/EthGenesisVault.sol b/contracts/vaults/ethereum/EthGenesisVault.sol index 4708af13..d47c42c8 100644 --- a/contracts/vaults/ethereum/EthGenesisVault.sol +++ b/contracts/vaults/ethereum/EthGenesisVault.sol @@ -47,13 +47,7 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault { } /// @inheritdoc IEthVault - function initialize(bytes calldata) - external - payable - virtual - override(IEthVault, EthVault) - reinitializer(_version) - { + function initialize(bytes calldata) external payable virtual override(IEthVault, EthVault) reinitializer(_version) { if (admin == address(0)) { revert Errors.UpgradeFailed(); } diff --git a/contracts/vaults/ethereum/custom/EthMetaVault.sol b/contracts/vaults/ethereum/EthMetaVault.sol similarity index 57% rename from contracts/vaults/ethereum/custom/EthMetaVault.sol rename to contracts/vaults/ethereum/EthMetaVault.sol index 3db29134..30ad96e9 100644 --- a/contracts/vaults/ethereum/custom/EthMetaVault.sol +++ b/contracts/vaults/ethereum/EthMetaVault.sol @@ -2,71 +2,50 @@ pragma solidity ^0.8.22; +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 {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 {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {IVaultEthStaking} from "../../../interfaces/IVaultEthStaking.sol"; -import {IEthMetaVaultFactory} from "../../../interfaces/IEthMetaVaultFactory.sol"; -import {IKeeperRewards} from "../../../interfaces/IKeeperRewards.sol"; -import {IEthMetaVault} from "../../../interfaces/IEthMetaVault.sol"; -import {Errors} from "../../../libraries/Errors.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 {Multicall} from "../../../base/Multicall.sol"; /** * @title EthMetaVault * @author StakeWise - * @notice Defines the Meta Vault that delegates stake to the sub vaults on Ethereum + * @notice Defines the Meta Vault functionality on Ethereum */ -contract EthMetaVault is - VaultImmutables, - Initializable, - VaultAdmin, - VaultVersion, - VaultFee, - VaultState, - VaultEnterExit, - VaultOsToken, - VaultSubVaults, - Multicall, - IEthMetaVault -{ +contract EthMetaVault is Initializable, MetaVault, IEthMetaVault { using EnumerableSet for EnumerableSet.AddressSet; - uint8 private constant _version = 5; + 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 EthMetaVault contract + * @param args The arguments for initializing the MetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(EthMetaVaultConstructorArgs memory args) - VaultImmutables(args.keeper, args.vaultsRegistry) - VaultEnterExit(args.exitingAssetsClaimDelay) - VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) - VaultSubVaults(args.curatorsRegistry) - { + constructor(MetaVaultConstructorArgs memory args) MetaVault(args) { _disableInitializers(); } /// @inheritdoc IEthMetaVault function initialize(bytes calldata params) external payable virtual override reinitializer(_version) { - __EthMetaVault_init(IEthMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (EthMetaVaultInitParams))); - } + // if admin is already set, it's an upgrade from version 5 to 6 + if (admin != address(0)) { + return; + } - /// @inheritdoc IVaultState - function isStateUpdateRequired() public view override(IVaultState, VaultState, VaultSubVaults) returns (bool) { - return super.isStateUpdateRequired(); + __EthMetaVault_init(IEthMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (MetaVaultInitParams))); } /// @inheritdoc IEthMetaVault @@ -85,24 +64,6 @@ contract EthMetaVault is _deposit(msg.sender, msg.value, address(0)); } - // @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 IEthMetaVault function updateStateAndDeposit( address receiver, @@ -154,16 +115,6 @@ contract EthMetaVault is return _version; } - /// @inheritdoc VaultImmutables - function _checkHarvested() internal view override(VaultImmutables, VaultSubVaults) { - super._checkHarvested(); - } - - /// @inheritdoc VaultImmutables - function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { - return super._isCollateralized(); - } - /// @inheritdoc VaultSubVaults function _depositToVault(address vault, uint256 assets) internal override returns (uint256) { return IVaultEthStaking(vault).deposit{value: assets}(address(this), address(0)); @@ -182,14 +133,10 @@ contract EthMetaVault is /** * @dev Initializes the EthMetaVault contract * @param admin The address of the admin of the Vault - * @param params The parameters for initializing the EthMetaVault contract + * @param params The parameters for initializing the MetaVault contract */ - function __EthMetaVault_init(address admin, EthMetaVaultInitParams 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); + function __EthMetaVault_init(address admin, MetaVaultInitParams memory params) internal onlyInitializing { + __MetaVault_init(admin, params); // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 if (msg.value < _securityDeposit) revert Errors.InvalidSecurityDeposit(); diff --git a/contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol b/contracts/vaults/ethereum/EthMetaVaultFactory.sol similarity index 56% rename from contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol rename to contracts/vaults/ethereum/EthMetaVaultFactory.sol index a7e19a1f..f048f151 100644 --- a/contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol +++ b/contracts/vaults/ethereum/EthMetaVaultFactory.sol @@ -3,18 +3,17 @@ pragma solidity ^0.8.22; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; -import {IEthMetaVaultFactory} from "../../../interfaces/IEthMetaVaultFactory.sol"; -import {IEthMetaVault} from "../../../interfaces/IEthMetaVault.sol"; -import {IVaultsRegistry} from "../../../interfaces/IVaultsRegistry.sol"; -import {Errors} from "../../../libraries/Errors.sol"; +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 * @author StakeWise * @notice Factory for deploying Ethereum meta Vaults */ -contract EthMetaVaultFactory is Ownable2Step, IEthMetaVaultFactory { +contract EthMetaVaultFactory is IEthMetaVaultFactory { IVaultsRegistry internal immutable _vaultsRegistry; /// @inheritdoc IEthMetaVaultFactory @@ -25,30 +24,21 @@ contract EthMetaVaultFactory is Ownable2Step, IEthMetaVaultFactory { /** * @dev Constructor - * @param initialOwner The address of the contract owner * @param _implementation The implementation address of Vault * @param vaultsRegistry The address of the VaultsRegistry contract */ - constructor(address initialOwner, address _implementation, IVaultsRegistry vaultsRegistry) Ownable(initialOwner) { + constructor(address _implementation, IVaultsRegistry vaultsRegistry) { implementation = _implementation; _vaultsRegistry = vaultsRegistry; } /// @inheritdoc IEthMetaVaultFactory - function createVault(address admin, bytes calldata params) - external - payable - override - onlyOwner - returns (address vault) - { - if (admin == address(0)) revert Errors.ZeroAddress(); - + function createVault(bytes calldata params) external payable override returns (address vault) { // create vault vault = address(new ERC1967Proxy(implementation, "")); // set admin so that it can be initialized in the Vault - vaultAdmin = admin; + vaultAdmin = msg.sender; // initialize Vault IEthMetaVault(vault).initialize{value: msg.value}(params); @@ -60,6 +50,6 @@ contract EthMetaVaultFactory is Ownable2Step, IEthMetaVaultFactory { _vaultsRegistry.addVault(vault); // emit event - emit MetaVaultCreated(msg.sender, admin, vault, params); + emit MetaVaultCreated(msg.sender, vault, params); } } diff --git a/contracts/vaults/ethereum/EthPrivMetaVault.sol b/contracts/vaults/ethereum/EthPrivMetaVault.sol new file mode 100644 index 00000000..badd1eee --- /dev/null +++ b/contracts/vaults/ethereum/EthPrivMetaVault.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IEthMetaVaultFactory} from "../../interfaces/IEthMetaVaultFactory.sol"; +import {IEthPrivMetaVault} from "../../interfaces/IEthPrivMetaVault.sol"; +import {IVaultOsToken, VaultOsToken} from "../modules/VaultOsToken.sol"; +import {IVaultVersion} from "../modules/VaultVersion.sol"; +import {VaultWhitelist} from "../modules/VaultWhitelist.sol"; +import {EthMetaVault, IEthMetaVault} from "./EthMetaVault.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/** + * @title EthPrivMetaVault + * @author StakeWise + * @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; + + /** + * @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 EthMetaVault contract + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(MetaVaultConstructorArgs memory args) EthMetaVault(args) { + _disableInitializers(); + } + + /// @inheritdoc IEthMetaVault + function initialize(bytes calldata params) + external + payable + virtual + override(IEthMetaVault, EthMetaVault) + reinitializer(_version) + { + // initialize deployed vault + address _admin = IEthMetaVaultFactory(msg.sender).vaultAdmin(); + __EthMetaVault_init(_admin, abi.decode(params, (MetaVaultInitParams))); + // whitelister is initially set to admin address + __VaultWhitelist_init(_admin); + } + + /// @inheritdoc IEthMetaVault + function deposit(address receiver, address referrer) + public + payable + virtual + override(IEthMetaVault, EthMetaVault) + returns (uint256 shares) + { + _checkWhitelist(msg.sender); + _checkWhitelist(receiver); + return super.deposit(receiver, referrer); + } + + /// @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)) { + return; + } + _checkWhitelist(msg.sender); + _deposit(msg.sender, msg.value, address(0)); + } + + /// @inheritdoc IVaultOsToken + function mintOsToken(address receiver, uint256 osTokenShares, address referrer) + public + virtual + override(IVaultOsToken, VaultOsToken) + returns (uint256 assets) + { + _checkWhitelist(msg.sender); + return super.mintOsToken(receiver, osTokenShares, referrer); + } + + /// @inheritdoc IVaultVersion + function vaultId() public pure virtual override(IVaultVersion, EthMetaVault) returns (bytes32) { + return keccak256("EthPrivMetaVault"); + } + + /// @inheritdoc IVaultVersion + function version() public pure virtual override(IVaultVersion, EthMetaVault) returns (uint8) { + return _version; + } + + /** + * @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/EthVaultFactory.sol b/contracts/vaults/ethereum/EthVaultFactory.sol index 787aadb7..c5c13b6d 100644 --- a/contracts/vaults/ethereum/EthVaultFactory.sol +++ b/contracts/vaults/ethereum/EthVaultFactory.sol @@ -36,12 +36,7 @@ contract EthVaultFactory is IEthVaultFactory { } /// @inheritdoc IEthVaultFactory - function createVault(bytes calldata params, bool isOwnMevEscrow) - external - payable - override - returns (address vault) - { + function createVault(bytes calldata params, bool isOwnMevEscrow) external payable override returns (address vault) { // create vault vault = address(new ERC1967Proxy(implementation, "")); diff --git a/contracts/vaults/gnosis/GnoErc20Vault.sol b/contracts/vaults/gnosis/GnoErc20Vault.sol index 4c9aa517..007cd59c 100644 --- a/contracts/vaults/gnosis/GnoErc20Vault.sol +++ b/contracts/vaults/gnosis/GnoErc20Vault.sol @@ -165,9 +165,7 @@ contract GnoErc20Vault is * @param ownMevEscrow The address of the MEV escrow owned by the Vault. Zero address if shared MEV escrow is used. * @param params The decoded parameters for initializing the GnoErc20Vault contract */ - function __GnoErc20Vault_init(address admin, address ownMevEscrow, GnoErc20VaultInitParams memory params) - internal - { + function __GnoErc20Vault_init(address admin, address ownMevEscrow, GnoErc20VaultInitParams memory params) internal { __VaultAdmin_init(admin, params.metadataIpfsHash); // fee recipient is initially set to admin address __VaultFee_init(admin, params.feePercent); diff --git a/contracts/vaults/gnosis/custom/GnoMetaVault.sol b/contracts/vaults/gnosis/GnoMetaVault.sol similarity index 51% rename from contracts/vaults/gnosis/custom/GnoMetaVault.sol rename to contracts/vaults/gnosis/GnoMetaVault.sol index 9e086796..e1429170 100644 --- a/contracts/vaults/gnosis/custom/GnoMetaVault.sol +++ b/contracts/vaults/gnosis/GnoMetaVault.sol @@ -2,46 +2,26 @@ pragma solidity ^0.8.22; +import {IGnoMetaVault} from "../../interfaces/IGnoMetaVault.sol"; +import {IGnoMetaVaultFactory} from "../../interfaces/IGnoMetaVaultFactory.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 {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {IGnoMetaVaultFactory} from "../../../interfaces/IGnoMetaVaultFactory.sol"; -import {IVaultGnoStaking} from "../../../interfaces/IVaultGnoStaking.sol"; -import {IKeeperRewards} from "../../../interfaces/IKeeperRewards.sol"; -import {IGnoMetaVault} from "../../../interfaces/IGnoMetaVault.sol"; -import {Errors} from "../../../libraries/Errors.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 {IVaultSubVaults, VaultSubVaults} from "../../modules/VaultSubVaults.sol"; -import {Multicall} from "../../../base/Multicall.sol"; /** * @title GnoMetaVault * @author StakeWise - * @notice Defines the Meta Vault that delegates stake to the sub vaults on Gnosis + * @notice Defines the Meta Vault functionality on Gnosis */ -contract GnoMetaVault is - VaultImmutables, - Initializable, - VaultAdmin, - VaultVersion, - VaultFee, - VaultState, - VaultEnterExit, - VaultOsToken, - VaultSubVaults, - Multicall, - IGnoMetaVault -{ - using EnumerableSet for EnumerableSet.AddressSet; - - uint8 private constant _version = 3; +contract GnoMetaVault is Initializable, MetaVault, IGnoMetaVault { + uint8 private constant _version = 4; uint256 private constant _securityDeposit = 1e9; IERC20 private immutable _gnoToken; @@ -50,22 +30,23 @@ contract GnoMetaVault is * @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 GnoMetaVault contract + * @param gnoToken The address of the GNO token contract + * @param args The arguments for initializing the MetaVault contract */ /// @custom:oz-upgrades-unsafe-allow constructor - constructor(GnoMetaVaultConstructorArgs memory args) - VaultImmutables(args.keeper, args.vaultsRegistry) - VaultEnterExit(args.exitingAssetsClaimDelay) - VaultOsToken(args.osTokenVaultController, args.osTokenConfig, args.osTokenVaultEscrow) - VaultSubVaults(args.curatorsRegistry) - { - _gnoToken = IERC20(args.gnoToken); + constructor(address gnoToken, MetaVaultConstructorArgs memory args) MetaVault(args) { + _gnoToken = IERC20(gnoToken); _disableInitializers(); } /// @inheritdoc IGnoMetaVault - function initialize(bytes calldata params) external payable virtual override reinitializer(_version) { - __GnoMetaVault_init(IGnoMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (GnoMetaVaultInitParams))); + 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)) { + return; + } + + __GnoMetaVault_init(IGnoMetaVaultFactory(msg.sender).vaultAdmin(), abi.decode(params, (MetaVaultInitParams))); } /// @inheritdoc IGnoMetaVault @@ -73,7 +54,6 @@ contract GnoMetaVault is public virtual override - nonReentrant returns (uint256 shares) { // withdraw GNO tokens from the user @@ -81,11 +61,6 @@ contract GnoMetaVault is shares = _deposit(receiver, assets, referrer); } - /// @inheritdoc IVaultState - function isStateUpdateRequired() public view override(IVaultState, VaultState, VaultSubVaults) returns (bool) { - return super.isStateUpdateRequired(); - } - /// @inheritdoc IVaultSubVaults function addSubVault(address vault) public virtual override(IVaultSubVaults, VaultSubVaults) { super.addSubVault(vault); @@ -100,24 +75,6 @@ contract GnoMetaVault is _gnoToken.approve(vault, 0); } - // @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 IGnoMetaVault function donateAssets(uint256 amount) external override nonReentrant { if (amount == 0) { @@ -139,16 +96,6 @@ contract GnoMetaVault is return _version; } - /// @inheritdoc VaultImmutables - function _checkHarvested() internal view override(VaultImmutables, VaultSubVaults) { - super._checkHarvested(); - } - - /// @inheritdoc VaultImmutables - function _isCollateralized() internal view virtual override(VaultImmutables, VaultSubVaults) returns (bool) { - return super._isCollateralized(); - } - /// @inheritdoc VaultSubVaults function _depositToVault(address vault, uint256 assets) internal override returns (uint256) { return IVaultGnoStaking(vault).deposit(assets, address(this), address(0)); @@ -167,14 +114,10 @@ contract GnoMetaVault is /** * @dev Initializes the GnoMetaVault contract * @param admin The address of the admin of the Vault - * @param params The parameters for initializing the GnoMetaVault contract + * @param params The parameters for initializing the MetaVault contract */ - function __GnoMetaVault_init(address admin, GnoMetaVaultInitParams 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); + function __GnoMetaVault_init(address admin, MetaVaultInitParams memory params) internal onlyInitializing { + __MetaVault_init(admin, params); _deposit(address(this), _securityDeposit, address(0)); // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 diff --git a/contracts/vaults/gnosis/custom/GnoMetaVaultFactory.sol b/contracts/vaults/gnosis/GnoMetaVaultFactory.sol similarity index 67% rename from contracts/vaults/gnosis/custom/GnoMetaVaultFactory.sol rename to contracts/vaults/gnosis/GnoMetaVaultFactory.sol index b3ff1ba8..8b51ef67 100644 --- a/contracts/vaults/gnosis/custom/GnoMetaVaultFactory.sol +++ b/contracts/vaults/gnosis/GnoMetaVaultFactory.sol @@ -5,18 +5,17 @@ pragma solidity ^0.8.22; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.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"; +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 * @author StakeWise * @notice Factory for deploying Gnosis meta Vaults */ -contract GnoMetaVaultFactory is Ownable2Step, IGnoMetaVaultFactory { +contract GnoMetaVaultFactory is IGnoMetaVaultFactory { uint256 private constant _securityDeposit = 1e9; IVaultsRegistry internal immutable _vaultsRegistry; @@ -31,23 +30,18 @@ contract GnoMetaVaultFactory is Ownable2Step, IGnoMetaVaultFactory { /** * @dev Constructor - * @param initialOwner The address of the contract owner * @param _implementation The implementation address of Vault * @param vaultsRegistry The address of the VaultsRegistry contract * @param gnoToken The address of the GNO token contract */ - constructor(address initialOwner, address _implementation, IVaultsRegistry vaultsRegistry, address gnoToken) - Ownable(initialOwner) - { + constructor(address _implementation, IVaultsRegistry vaultsRegistry, address gnoToken) { implementation = _implementation; _vaultsRegistry = vaultsRegistry; _gnoToken = IERC20(gnoToken); } /// @inheritdoc IGnoMetaVaultFactory - function createVault(address admin, bytes calldata params) external override onlyOwner returns (address vault) { - if (admin == address(0)) revert Errors.ZeroAddress(); - + function createVault(bytes calldata params) external override returns (address vault) { // transfer GNO security deposit to the factory // see https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3706 SafeERC20.safeTransferFrom(_gnoToken, msg.sender, address(this), _securityDeposit); @@ -59,7 +53,7 @@ contract GnoMetaVaultFactory is Ownable2Step, IGnoMetaVaultFactory { _gnoToken.approve(vault, _securityDeposit); // set admin so that it can be initialized in the Vault - vaultAdmin = admin; + vaultAdmin = msg.sender; // initialize Vault IGnoMetaVault(vault).initialize(params); @@ -71,6 +65,6 @@ contract GnoMetaVaultFactory is Ownable2Step, IGnoMetaVaultFactory { _vaultsRegistry.addVault(vault); // emit event - emit MetaVaultCreated(msg.sender, admin, vault, params); + emit MetaVaultCreated(msg.sender, vault, params); } } diff --git a/contracts/vaults/gnosis/GnoPrivMetaVault.sol b/contracts/vaults/gnosis/GnoPrivMetaVault.sol new file mode 100644 index 00000000..fe72671a --- /dev/null +++ b/contracts/vaults/gnosis/GnoPrivMetaVault.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {IGnoMetaVaultFactory} from "../../interfaces/IGnoMetaVaultFactory.sol"; +import {IGnoPrivMetaVault} from "../../interfaces/IGnoPrivMetaVault.sol"; +import {IVaultOsToken, VaultOsToken} from "../modules/VaultOsToken.sol"; +import {IVaultVersion, VaultVersion} from "../modules/VaultVersion.sol"; +import {VaultWhitelist} from "../modules/VaultWhitelist.sol"; +import {GnoMetaVault, IGnoMetaVault} from "./GnoMetaVault.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title GnoPrivMetaVault + * @author StakeWise + * @notice Defines the Meta Vault functionality with whitelist on Gnosis + */ +contract GnoPrivMetaVault is Initializable, GnoMetaVault, VaultWhitelist, IGnoPrivMetaVault { + // slither-disable-next-line shadowing-state + uint8 private constant _version = 4; + + /** + * @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 + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address gnoToken, MetaVaultConstructorArgs memory args) GnoMetaVault(gnoToken, args) { + _disableInitializers(); + } + + /// @inheritdoc IGnoMetaVault + function initialize(bytes calldata params) + external + virtual + override(IGnoMetaVault, GnoMetaVault) + reinitializer(_version) + { + // initialize deployed vault + address _admin = IGnoMetaVaultFactory(msg.sender).vaultAdmin(); + __GnoMetaVault_init(_admin, abi.decode(params, (MetaVaultInitParams))); + // whitelister is initially set to admin address + __VaultWhitelist_init(_admin); + } + + /// @inheritdoc IGnoMetaVault + function deposit(uint256 assets, address receiver, address referrer) + public + virtual + override(IGnoMetaVault, GnoMetaVault) + returns (uint256 shares) + { + _checkWhitelist(msg.sender); + _checkWhitelist(receiver); + return super.deposit(assets, receiver, referrer); + } + + /// @inheritdoc IVaultOsToken + function mintOsToken(address receiver, uint256 osTokenShares, address referrer) + public + virtual + override(IVaultOsToken, VaultOsToken) + returns (uint256 assets) + { + _checkWhitelist(msg.sender); + return super.mintOsToken(receiver, osTokenShares, referrer); + } + + /// @inheritdoc IVaultVersion + function vaultId() public pure virtual override(IVaultVersion, GnoMetaVault) returns (bytes32) { + return keccak256("GnoPrivMetaVault"); + } + + /// @inheritdoc IVaultVersion + function version() public pure virtual override(IVaultVersion, GnoMetaVault) returns (uint8) { + return _version; + } + + /** + * @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/modules/VaultEnterExit.sol b/contracts/vaults/modules/VaultEnterExit.sol index 1acdd044..4e91d4cd 100644 --- a/contracts/vaults/modules/VaultEnterExit.sol +++ b/contracts/vaults/modules/VaultEnterExit.sol @@ -40,12 +40,7 @@ abstract contract VaultEnterExit is VaultImmutables, Initializable, VaultState, } /// @inheritdoc IVaultEnterExit - function enterExitQueue(uint256 shares, address receiver) - public - virtual - override - returns (uint256 positionTicket) - { + function enterExitQueue(uint256 shares, address receiver) public virtual override returns (uint256 positionTicket) { return _enterExitQueue(msg.sender, shares, receiver); } diff --git a/contracts/vaults/modules/VaultGnoStaking.sol b/contracts/vaults/modules/VaultGnoStaking.sol index b13c966f..88b6872c 100644 --- a/contracts/vaults/modules/VaultGnoStaking.sol +++ b/contracts/vaults/modules/VaultGnoStaking.sol @@ -94,13 +94,14 @@ abstract contract VaultGnoStaking is depositData.depositAmount /= 32; // deposit GNO tokens to the validators registry - IGnoValidatorsRegistry(_validatorsRegistry).deposit( - depositData.publicKey, - depositData.withdrawalCredentials, - depositData.signature, - depositData.depositDataRoot, - depositData.depositAmount - ); + IGnoValidatorsRegistry(_validatorsRegistry) + .deposit( + depositData.publicKey, + depositData.withdrawalCredentials, + depositData.signature, + depositData.depositDataRoot, + depositData.depositAmount + ); // will revert if not enough assets availableAssets -= depositData.depositAmount; diff --git a/contracts/vaults/modules/VaultOsToken.sol b/contracts/vaults/modules/VaultOsToken.sol index 7c3e44e4..af80f443 100644 --- a/contracts/vaults/modules/VaultOsToken.sol +++ b/contracts/vaults/modules/VaultOsToken.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 private immutable _osTokenVaultController; + IOsTokenVaultController internal immutable _osTokenVaultController; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - IOsTokenConfig private immutable _osTokenConfig; + IOsTokenConfig internal immutable _osTokenConfig; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IOsTokenVaultEscrow private immutable _osTokenVaultEscrow; @@ -97,11 +97,7 @@ abstract contract VaultOsToken is VaultImmutables, VaultState, VaultEnterExit, I } /// @inheritdoc IVaultOsToken - function transferOsTokenPositionToEscrow(uint256 osTokenShares) - external - override - returns (uint256 positionTicket) - { + function transferOsTokenPositionToEscrow(uint256 osTokenShares) external override returns (uint256 positionTicket) { // check whether vault assets are up to date _checkHarvested(); diff --git a/contracts/vaults/modules/VaultSubVaults.sol b/contracts/vaults/modules/VaultSubVaults.sol index c48708bf..f916e43d 100644 --- a/contracts/vaults/modules/VaultSubVaults.sol +++ b/contracts/vaults/modules/VaultSubVaults.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.22; -import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; 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"; @@ -18,6 +18,8 @@ import {ICuratorsRegistry} from "../../interfaces/ICuratorsRegistry.sol"; import {IVaultVersion} from "../../interfaces/IVaultVersion.sol"; import {ExitQueue} from "../../libraries/ExitQueue.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"; @@ -38,8 +40,6 @@ abstract contract VaultSubVaults is using EnumerableSet for EnumerableSet.AddressSet; using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - uint256 private constant _maxSubVaults = 50; - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address private immutable _curatorsRegistry; @@ -51,14 +51,17 @@ abstract contract VaultSubVaults is EnumerableSet.AddressSet internal _subVaults; mapping(address vault => DoubleEndedQueue.Bytes32Deque) private _subVaultsExits; - mapping(address vault => SubVaultState state) private _subVaultsStates; + mapping(address vault => SubVaultState state) internal _subVaultsStates; /// @inheritdoc IVaultSubVaults uint128 public override subVaultsRewardsNonce; - uint128 private _subVaultsTotalAssets; + uint128 internal _subVaultsTotalAssets; uint256 private _totalProcessedExitQueueTickets; - uint256 private _ejectingSubVaultShares; + uint256 internal _ejectingSubVaultShares; + + /// @inheritdoc IVaultSubVaults + address public override pendingMetaSubVault; /** * @dev Constructor @@ -90,82 +93,72 @@ abstract contract VaultSubVaults is /// @inheritdoc IVaultSubVaults function addSubVault(address vault) public virtual override { _checkAdmin(); - // 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 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); } - // check whether the vault is not exceeding the limit - uint256 subVaultsCount = _subVaults.length(); - if (subVaultsCount >= _maxSubVaults) { - revert Errors.CapacityExceeded(); + } + + /// @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(); } - // check whether vault is collateralized - if (!_isSubVaultCollateralized(vault)) { - revert Errors.NotCollateralized(); + + if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { + revert Errors.InvalidVault(); } - // 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(); + // check sub-vault validity + SubVaultUtils.validateSubVault(_subVaults, _vaultsRegistry, _keeper, metaSubVault); + + // update state + delete pendingMetaSubVault; + _addSubVault(metaSubVault); + } + + /// @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(); } - // check harvested - uint256 vaultNonce = _getSubVaultRewardsNonce(vault); - uint256 lastSubVaultsRewardsNonce = subVaultsRewardsNonce; - if (subVaultsCount == 0) { - subVaultsRewardsNonce = SafeCast.toUint128(vaultNonce); - emit RewardsNonceUpdated(vaultNonce); - } else if (vaultNonce != lastSubVaultsRewardsNonce) { - revert Errors.NotHarvested(); + if (metaSubVault == address(0) || pendingMetaSubVault != metaSubVault) { + revert Errors.InvalidVault(); } - // add the vault to the list of sub vaults - _subVaults.add(vault); - emit SubVaultAdded(msg.sender, vault); + // update state + delete pendingMetaSubVault; + + // emit event + emit MetaSubVaultRejected(msg.sender, metaSubVault); } /// @inheritdoc IVaultSubVaults function ejectSubVault(address vault) public virtual override { _checkAdmin(); - if (ejectingSubVault != address(0)) { - revert Errors.EjectingVault(); - } - if (!_subVaults.contains(vault)) { - revert Errors.AlreadyRemoved(); - } - if (_subVaults.length() == 1) { - revert Errors.EmptySubVaults(); - } + (bool ejected, uint128 ejectingShares) = + SubVaultUtils.ejectSubVault(_subVaults, _subVaultsStates, _subVaultsExits, ejectingSubVault, vault); - // check the vault state - 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 - _pushSubVaultExit(vault, SafeCast.toUint160(positionTicket), SafeCast.toUint96(state.stakedShares), false); - state.queuedShares += state.stakedShares; - } - - // update state - if (state.queuedShares > 0) { + if (ejected) { + emit SubVaultEjected(msg.sender, vault); + } else { ejectingSubVault = vault; - _ejectingSubVaultShares = state.stakedShares; - state.stakedShares = 0; - _subVaultsStates[vault] = state; + _ejectingSubVaultShares = ejectingShares; emit SubVaultEjecting(msg.sender, vault); - } else { - // no shares left - _subVaultsExits[vault].clear(); - // remove the vault from the list of sub vaults - _subVaults.remove(vault); - emit SubVaultEjected(msg.sender, vault); } } @@ -239,53 +232,26 @@ abstract contract VaultSubVaults is /// @inheritdoc IVaultSubVaults function claimSubVaultsExitedAssets(SubVaultExitRequest[] calldata exitRequests) external override { - uint256 exitRequestsLength = exitRequests.length; // SLOAD to memory - uint256 subVaultsTotalAssets = _subVaultsTotalAssets; address _ejectingSubVault = ejectingSubVault; - for (uint256 i = 0; i < exitRequestsLength;) { - SubVaultExitRequest calldata exitRequest = exitRequests[i]; - SubVaultState memory subVaultState = _subVaultsStates[exitRequest.vault]; - (uint256 positionTicket, uint256 positionShares) = _popSubVaultExit(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 - _pushSubVaultExit( - 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; + uint256 totalExitedAssets = + SubVaultUtils.claimSubVaultsExitedAssets(_subVaultsStates, _subVaultsExits, exitRequests); - // claim exited assets from the vault - IVaultEnterExit(exitRequest.vault).claimExitedAssets( - positionTicket, exitRequest.timestamp, exitRequest.exitQueueIndex - ); - if (_ejectingSubVault == exitRequest.vault && subVaultState.queuedShares == 0) { + 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[exitRequest.vault].clear(); - _subVaults.remove(exitRequest.vault); - emit SubVaultEjected(msg.sender, exitRequest.vault); - } - - unchecked { - // cannot realistically overflow - ++i; + _subVaultsExits[_ejectingSubVault].clear(); + _subVaults.remove(_ejectingSubVault); + emit SubVaultEjected(msg.sender, _ejectingSubVault); } } - // update sub vaults total assets - _subVaultsTotalAssets = SafeCast.toUint128(subVaultsTotalAssets); + + // update sub vaults total assets + _subVaultsTotalAssets -= SafeCast.toUint128(totalExitedAssets); } /// @inheritdoc IVaultState @@ -305,26 +271,9 @@ abstract contract VaultSubVaults is _checkSubVaultsExitClaims(vaults); // calculate new total assets and save balances in each sub vault + uint256[] memory balances; uint256 newSubVaultsTotalAssets; - uint256[] memory balances = new uint256[](vaultsLength); - for (uint256 i = 0; i < vaultsLength;) { - address vault = vaults[i]; - SubVaultState memory vaultState = _subVaultsStates[vault]; - uint256 vaultTotalShares = vaultState.stakedShares + vaultState.queuedShares; - if (vaultTotalShares > 0) { - newSubVaultsTotalAssets += IVaultState(vault).convertToAssets(vaultTotalShares); - } - - if (vaultState.stakedShares > 0) { - balances[i] = IVaultState(vault).convertToAssets(vaultState.stakedShares); - } else { - balances[i] = 0; - } - unchecked { - // cannot realistically overflow - ++i; - } - } + (balances, newSubVaultsTotalAssets) = SubVaultUtils.getSubVaultsBalances(_subVaultsStates, vaults, true); // store new sub vaults total assets delta int256 totalAssetsDelta = SafeCast.toInt256(newSubVaultsTotalAssets) - SafeCast.toInt256(_subVaultsTotalAssets); @@ -357,6 +306,25 @@ 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 @@ -421,8 +389,12 @@ abstract contract VaultSubVaults is uint256 positionTicket = IVaultEnterExit(exitRequest.vault).enterExitQueue(vaultShares, address(this)); // save exit request - _pushSubVaultExit( - exitRequest.vault, SafeCast.toUint160(positionTicket), SafeCast.toUint96(vaultShares), false + SubVaultExits.pushSubVaultExit( + _subVaultsExits, + exitRequest.vault, + SafeCast.toUint160(positionTicket), + SafeCast.toUint96(vaultShares), + false ); // update state @@ -451,7 +423,7 @@ abstract contract VaultSubVaults is uint256 vaultsLength = vaults.length; for (uint256 i = 0; i < vaultsLength;) { address vault = vaults[i]; - (uint256 positionTicket, uint256 exitShares) = _peekSubVaultExit(vault); + (uint256 positionTicket, uint256 exitShares) = SubVaultExits.peekSubVaultExit(_subVaultsExits, vault); if (positionTicket == 0 && exitShares == 0) { // no queue positions unchecked { @@ -544,50 +516,6 @@ abstract contract VaultSubVaults is ejectingSubVaultShares - IVaultState(_ejectingSubVault).convertToShares(processedAssets); } - /** - * @dev Fetches the sub-vault exit data - * @param vault The address of the sub-vault - * @return positionTicket The position ticket of the sub-vault - * @return shares The shares to be exited from the sub-vault - */ - function _peekSubVaultExit(address vault) private view returns (uint160 positionTicket, uint96 shares) { - if (_subVaultsExits[vault].empty()) { - return (0, 0); - } - bytes32 packed = _subVaultsExits[vault].front(); - positionTicket = uint160(Packing.extract_32_20(packed, 0)); - shares = uint96(Packing.extract_32_12(packed, 20)); - } - - /** - * @dev Stores the sub-vault exit data - * @param vault The address of the sub-vault - * @param positionTicket The position ticket of the sub-vault - * @param shares The shares to be exited from the sub-vault - * @param front Whether to insert the exit data at the front of the queue - */ - function _pushSubVaultExit(address vault, uint160 positionTicket, uint96 shares, bool front) private { - if (shares == 0) revert Errors.InvalidShares(); - bytes32 packed = Packing.pack_20_12(bytes20(positionTicket), bytes12(shares)); - if (front) { - _subVaultsExits[vault].pushFront(packed); - } else { - _subVaultsExits[vault].pushBack(packed); - } - } - - /** - * @dev Removes the sub-vault exit data - * @param vault The address of the sub-vault - * @return positionTicket The position ticket of the sub-vault - * @return shares The shares to be exited from the sub-vault - */ - function _popSubVaultExit(address vault) private returns (uint160 positionTicket, uint96 shares) { - bytes32 packed = _subVaultsExits[vault].popFront(); - positionTicket = uint160(Packing.extract_32_20(packed, 0)); - shares = uint96(Packing.extract_32_12(packed, 20)); - } - /// @inheritdoc VaultImmutables function _checkHarvested() internal view virtual override { if (isStateUpdateRequired()) { @@ -600,19 +528,6 @@ abstract contract VaultSubVaults is return _subVaults.length() > 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 subVault) private view returns (bool) { - try IVaultSubVaults(subVault).isCollateralized() returns (bool collateralized) { - return collateralized; - } 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 @@ -649,6 +564,19 @@ abstract contract VaultSubVaults is emit SubVaultsCuratorUpdated(msg.sender, 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 + */ + function _isMetaVault(address vault) private view returns (bool) { + try IVaultSubVaults(vault).getSubVaults() { + return true; + } catch { + return false; + } + } + /** * @dev Internal function to deposit assets to the vault * @param vault The address of the vault @@ -672,5 +600,5 @@ abstract contract VaultSubVaults is * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[50] private __gap; + uint256[49] private __gap; } diff --git a/contracts/vaults/modules/VaultValidators.sol b/contracts/vaults/modules/VaultValidators.sol index b43db359..1057f82e 100644 --- a/contracts/vaults/modules/VaultValidators.sol +++ b/contracts/vaults/modules/VaultValidators.sol @@ -101,11 +101,9 @@ abstract contract VaultValidators is _checkHarvested(); // check access - if ( - !_isValidatorsManager( + if (!_isValidatorsManager( keeperParams.validators, keeperParams.validatorsRegistryRoot, validatorsManagerSignature - ) - ) { + )) { revert Errors.AccessDenied(); } @@ -249,9 +247,8 @@ abstract contract VaultValidators is // migrate deposit data variables to DepositDataRegistry contract if (__deprecated__validatorsRoot != bytes32(0)) { - IDepositDataRegistry(_depositDataRegistry).migrate( - __deprecated__validatorsRoot, __deprecated__validatorIndex, validatorsManager - ); + IDepositDataRegistry(_depositDataRegistry) + .migrate(__deprecated__validatorsRoot, __deprecated__validatorIndex, validatorsManager); delete __deprecated__validatorIndex; delete __deprecated__validatorsRoot; delete validatorsManager; diff --git a/script/ExecuteGovernorTxs.s.sol b/script/ExecuteGovernorTxs.s.sol index 7507c40f..dbd3421a 100644 --- a/script/ExecuteGovernorTxs.s.sol +++ b/script/ExecuteGovernorTxs.s.sol @@ -2,11 +2,11 @@ pragma solidity ^0.8.22; -import {console} from "forge-std/console.sol"; -import {stdJson} from "forge-std/StdJson.sol"; +import {Network} from "./Network.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {Network} from "./Network.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {console} from "forge-std/console.sol"; contract ExecuteGovernorTxs is Network { using stdJson for string; diff --git a/script/Network.sol b/script/Network.sol index 7451951f..334b6b5f 100644 --- a/script/Network.sol +++ b/script/Network.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.22; -import {Script} from "forge-std/Script.sol"; -import {stdJson} from "forge-std/StdJson.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IOsTokenConfig} from "../contracts/interfaces/IOsTokenConfig.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; -import {IOsTokenConfig} from "../contracts/interfaces/IOsTokenConfig.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; /** * @title Network @@ -40,6 +40,7 @@ abstract contract Network is Script { address erc20VaultFactory; address privErc20VaultFactory; address blocklistErc20VaultFactory; + address metaVaultFactory; address sharedMevEscrow; address osToken; address osTokenConfig; @@ -50,6 +51,10 @@ abstract contract Network is Script { address legacyPoolEscrow; address legacyRewardToken; address merkleDistributor; + address curatorsRegistry; + address balancedCurator; + address consolidationsChecker; + address rewardSplitterFactory; } struct Factory { @@ -110,6 +115,7 @@ abstract contract Network is Script { deployment.erc20VaultFactory = deploymentData.readAddress(".Erc20VaultFactory"); deployment.privErc20VaultFactory = deploymentData.readAddress(".PrivErc20VaultFactory"); deployment.blocklistErc20VaultFactory = deploymentData.readAddress(".BlocklistErc20VaultFactory"); + deployment.metaVaultFactory = deploymentData.readAddress(".MetaVaultFactory"); deployment.sharedMevEscrow = deploymentData.readAddress(".SharedMevEscrow"); deployment.osToken = deploymentData.readAddress(".OsToken"); deployment.osTokenConfig = deploymentData.readAddress(".OsTokenConfig"); @@ -121,6 +127,10 @@ abstract contract Network is Script { deployment.legacyRewardToken = deploymentData.readAddress(".LegacyRewardToken"); deployment.merkleDistributor = deploymentData.readAddress(".MerkleDistributor"); deployment.foxVault = isGnosisNetwork() ? address(0) : deploymentData.readAddress(".FoxVault"); + deployment.curatorsRegistry = deploymentData.readAddress(".CuratorsRegistry"); + deployment.balancedCurator = deploymentData.readAddress(".BalancedCurator"); + deployment.consolidationsChecker = deploymentData.readAddress(".ConsolidationsChecker"); + deployment.rewardSplitterFactory = deploymentData.readAddress(".RewardSplitterFactory"); _deployment = deployment; return deployment; @@ -147,12 +157,7 @@ abstract contract Network is Script { Deployment memory deployment = getDeploymentData(); if (removePrevVaultFactories) { - _governorCalls.push(_serializeRemoveFactory(deployment.vaultFactory)); - _governorCalls.push(_serializeRemoveFactory(deployment.privVaultFactory)); - _governorCalls.push(_serializeRemoveFactory(deployment.blocklistVaultFactory)); - _governorCalls.push(_serializeRemoveFactory(deployment.erc20VaultFactory)); - _governorCalls.push(_serializeRemoveFactory(deployment.privErc20VaultFactory)); - _governorCalls.push(_serializeRemoveFactory(deployment.blocklistErc20VaultFactory)); + _governorCalls.push(_serializeRemoveFactory(deployment.metaVaultFactory)); } _governorCalls.push(_serializeSetOsTokenRedeemer(deployment.osTokenConfig, osTokenRedeemer)); @@ -177,15 +182,9 @@ abstract contract Network is Script { vm.writeJson(output, getUpgradesFilePath()); } - function generateAddressesJson( - Factory[] memory newFactories, - address validatorsChecker, - address consolidationsChecker, - address rewardSplitterFactory, - address curatorsRegistry, - address balancedCurator, - address osTokenRedeemer - ) internal { + function generateAddressesJson(Factory[] memory newFactories, address validatorsChecker, address osTokenRedeemer) + internal + { Deployment memory deployment = getDeploymentData(); string memory json = "addresses"; @@ -203,9 +202,17 @@ abstract contract Network is Script { vm.serializeAddress(json, "LegacyPoolEscrow", deployment.legacyPoolEscrow); vm.serializeAddress(json, "LegacyRewardToken", deployment.legacyRewardToken); vm.serializeAddress(json, "MerkleDistributor", deployment.merkleDistributor); - vm.serializeAddress(json, "CuratorsRegistry", curatorsRegistry); - vm.serializeAddress(json, "BalancedCurator", balancedCurator); - vm.serializeAddress(json, "OsTokenRedeemer", osTokenRedeemer); + vm.serializeAddress(json, "CuratorsRegistry", deployment.curatorsRegistry); + vm.serializeAddress(json, "BalancedCurator", deployment.balancedCurator); + vm.serializeAddress(json, "ConsolidationsChecker", deployment.consolidationsChecker); + + vm.serializeAddress(json, "RewardSplitterFactory", deployment.rewardSplitterFactory); + vm.serializeAddress(json, "VaultFactory", deployment.vaultFactory); + vm.serializeAddress(json, "PrivVaultFactory", deployment.privVaultFactory); + vm.serializeAddress(json, "BlocklistVaultFactory", deployment.blocklistVaultFactory); + vm.serializeAddress(json, "Erc20VaultFactory", deployment.erc20VaultFactory); + vm.serializeAddress(json, "PrivErc20VaultFactory", deployment.privErc20VaultFactory); + vm.serializeAddress(json, "BlocklistErc20VaultFactory", deployment.blocklistErc20VaultFactory); for (uint256 i = 0; i < newFactories.length; i++) { Factory memory factory = newFactories[i]; @@ -216,10 +223,8 @@ abstract contract Network is Script { vm.serializeAddress(json, "FoxVault", deployment.foxVault); } - vm.serializeAddress(json, "ValidatorsChecker", validatorsChecker); - vm.serializeAddress(json, "ConsolidationsChecker", consolidationsChecker); - string memory output = vm.serializeAddress(json, "RewardSplitterFactory", rewardSplitterFactory); - + vm.serializeAddress(json, "OsTokenRedeemer", osTokenRedeemer); + 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 3dbf97e2..34062715 100644 --- a/script/UpgradeEthNetwork.s.sol +++ b/script/UpgradeEthNetwork.s.sol @@ -2,49 +2,29 @@ pragma solidity ^0.8.22; -import {console} from "forge-std/console.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IEthVault} from "../contracts/interfaces/IEthVault.sol"; -import {IEthErc20Vault} from "../contracts/interfaces/IEthErc20Vault.sol"; -import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; +import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; -import {ConsolidationsChecker} from "../contracts/validators/ConsolidationsChecker.sol"; -import {EthBlocklistErc20Vault} from "../contracts/vaults/ethereum/EthBlocklistErc20Vault.sol"; -import {EthBlocklistVault} from "../contracts/vaults/ethereum/EthBlocklistVault.sol"; -import {EthErc20Vault} from "../contracts/vaults/ethereum/EthErc20Vault.sol"; -import {EthGenesisVault} from "../contracts/vaults/ethereum/EthGenesisVault.sol"; -import {EthPrivErc20Vault} from "../contracts/vaults/ethereum/EthPrivErc20Vault.sol"; -import {EthPrivVault} from "../contracts/vaults/ethereum/EthPrivVault.sol"; -import {EthVault} from "../contracts/vaults/ethereum/EthVault.sol"; -import {EthMetaVault, IEthMetaVault} from "../contracts/vaults/ethereum/custom/EthMetaVault.sol"; -import {EthVaultFactory} from "../contracts/vaults/ethereum/EthVaultFactory.sol"; -import {EthMetaVaultFactory} from "../contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol"; -import {EthValidatorsChecker} from "../contracts/validators/EthValidatorsChecker.sol"; -import {CuratorsRegistry, ICuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; -import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; -import {EthRewardSplitter} from "../contracts/misc/EthRewardSplitter.sol"; -import {RewardSplitterFactory} from "../contracts/misc/RewardSplitterFactory.sol"; +import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; import {EthOsTokenRedeemer} from "../contracts/tokens/EthOsTokenRedeemer.sol"; +import {EthValidatorsChecker} from "../contracts/validators/EthValidatorsChecker.sol"; +import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; +import {EthMetaVaultFactory} from "../contracts/vaults/ethereum/EthMetaVaultFactory.sol"; +import {EthPrivMetaVault} from "../contracts/vaults/ethereum/EthPrivMetaVault.sol"; import {Network} from "./Network.sol"; +import {console} from "forge-std/console.sol"; contract UpgradeEthNetwork is Network { - address public metaVaultFactoryOwner; address public osTokenRedeemerOwner; address public validatorsRegistry; uint256 public osTokenRedeemerExitQueueUpdateDelay; - address public consolidationsChecker; address public validatorsChecker; - address public rewardSplitterFactory; - address public curatorsRegistry; - address public balancedCurator; address public osTokenRedeemer; address[] public vaultImpls; Factory[] public vaultFactories; function run() external { - metaVaultFactoryOwner = vm.envAddress("META_VAULT_FACTORY_OWNER"); osTokenRedeemerOwner = vm.envAddress("OS_TOKEN_REDEEMER_OWNER"); osTokenRedeemerExitQueueUpdateDelay = vm.envUint("OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY"); validatorsRegistry = vm.envAddress("VALIDATORS_REGISTRY"); @@ -57,7 +37,6 @@ contract UpgradeEthNetwork is Network { vm.startBroadcast(privateKey); // Deploy common contracts - consolidationsChecker = address(new ConsolidationsChecker(deployment.keeper)); validatorsChecker = address( new EthValidatorsChecker( validatorsRegistry, @@ -67,18 +46,11 @@ contract UpgradeEthNetwork is Network { deployment.legacyPoolEscrow ) ); - address rewardsSplitterImpl = address(new EthRewardSplitter()); - rewardSplitterFactory = address(new RewardSplitterFactory(rewardsSplitterImpl)); - // deploy curators - curatorsRegistry = address(new CuratorsRegistry()); - balancedCurator = address(new BalancedCurator()); - ICuratorsRegistry(curatorsRegistry).addCurator(balancedCurator); - ICuratorsRegistry(curatorsRegistry).initialize(Ownable(deployment.vaultsRegistry).owner()); - - // deploy OsToken redeemer + // Deploy OsToken redeemer osTokenRedeemer = address( new EthOsTokenRedeemer( + deployment.vaultsRegistry, deployment.osToken, deployment.osTokenVaultController, osTokenRedeemerOwner, @@ -92,59 +64,22 @@ contract UpgradeEthNetwork is Network { generateGovernorTxJson(vaultImpls, vaultFactories, osTokenRedeemer); generateUpgradesJson(vaultImpls); - generateAddressesJson( - vaultFactories, - validatorsChecker, - consolidationsChecker, - rewardSplitterFactory, - curatorsRegistry, - balancedCurator, - osTokenRedeemer - ); + generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer); } function _deployImplementations() internal { // constructors for implementations - IEthVault.EthVaultConstructorArgs memory vaultArgs = _getEthVaultConstructorArgs(); - IEthErc20Vault.EthErc20VaultConstructorArgs memory erc20VaultArgs = _getEthErc20VaultConstructorArgs(); - IEthMetaVault.EthMetaVaultConstructorArgs memory metaVaultArgs = _getEthMetaVaultConstructorArgs(); - Deployment memory deployment = getDeploymentData(); - - // update exited assets claim delay for public vaults - vaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; - erc20VaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; + IMetaVault.MetaVaultConstructorArgs memory metaVaultArgs = _getMetaVaultConstructorArgs(); - // deploy genesis vault - EthGenesisVault ethGenesisVault = - new EthGenesisVault(vaultArgs, deployment.legacyPoolEscrow, deployment.legacyRewardToken); - - // deploy public vaults - EthVault ethVault = new EthVault(vaultArgs); - EthErc20Vault ethErc20Vault = new EthErc20Vault(erc20VaultArgs); - - // deploy blocklist vaults - EthBlocklistVault ethBlocklistVault = new EthBlocklistVault(vaultArgs); - EthBlocklistErc20Vault ethBlocklistErc20Vault = new EthBlocklistErc20Vault(erc20VaultArgs); - - // update exited assets claim delay for private vaults - vaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; - erc20VaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; - - // deploy private vaults - EthPrivVault ethPrivVault = new EthPrivVault(vaultArgs); - EthPrivErc20Vault ethPrivErc20Vault = new EthPrivErc20Vault(erc20VaultArgs); - - // deploy MetaVault + // deploy meta vaults + metaVaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; EthMetaVault ethMetaVault = new EthMetaVault(metaVaultArgs); - vaultImpls.push(address(ethGenesisVault)); - vaultImpls.push(address(ethVault)); - vaultImpls.push(address(ethErc20Vault)); - vaultImpls.push(address(ethBlocklistVault)); - vaultImpls.push(address(ethBlocklistErc20Vault)); - vaultImpls.push(address(ethPrivVault)); - vaultImpls.push(address(ethPrivErc20Vault)); + metaVaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; + EthPrivMetaVault ethPrivMetaVault = new EthPrivMetaVault(metaVaultArgs); + vaultImpls.push(address(ethMetaVault)); + vaultImpls.push(address(ethPrivMetaVault)); } function _deployFactories() internal { @@ -153,84 +88,24 @@ contract UpgradeEthNetwork is Network { address vaultImpl = vaultImpls[i]; bytes32 vaultId = IVaultVersion(vaultImpl).vaultId(); - // skip factory creation for EthGenesisVault or EthFoxVault - if (vaultId == keccak256("EthGenesisVault") || vaultId == keccak256("EthFoxVault")) { - continue; - } - - address factory; + address factory = address(new EthMetaVaultFactory(vaultImpl, IVaultsRegistry(deployment.vaultsRegistry))); if (vaultId == keccak256("EthMetaVault")) { - factory = address( - new EthMetaVaultFactory( - metaVaultFactoryOwner, vaultImpl, IVaultsRegistry(deployment.vaultsRegistry) - ) - ); vaultFactories.push(Factory({name: "MetaVaultFactory", factory: factory})); - continue; - } - - factory = address(new EthVaultFactory(vaultImpl, IVaultsRegistry(deployment.vaultsRegistry))); - if (vaultId == keccak256("EthVault")) { - vaultFactories.push(Factory({name: "VaultFactory", factory: factory})); - } else if (vaultId == keccak256("EthErc20Vault")) { - vaultFactories.push(Factory({name: "Erc20VaultFactory", factory: factory})); - } else if (vaultId == keccak256("EthBlocklistVault")) { - vaultFactories.push(Factory({name: "BlocklistVaultFactory", factory: factory})); - } else if (vaultId == keccak256("EthPrivVault")) { - vaultFactories.push(Factory({name: "PrivVaultFactory", factory: factory})); - } else if (vaultId == keccak256("EthBlocklistErc20Vault")) { - vaultFactories.push(Factory({name: "BlocklistErc20VaultFactory", factory: factory})); - } else if (vaultId == keccak256("EthPrivErc20Vault")) { - vaultFactories.push(Factory({name: "PrivErc20VaultFactory", factory: factory})); + } else if (vaultId == keccak256("EthPrivMetaVault")) { + vaultFactories.push(Factory({name: "PrivMetaVaultFactory", factory: factory})); } } } - function _getEthVaultConstructorArgs() internal returns (IEthVault.EthVaultConstructorArgs memory) { - Deployment memory deployment = getDeploymentData(); - return IEthVault.EthVaultConstructorArgs({ - keeper: deployment.keeper, - vaultsRegistry: deployment.vaultsRegistry, - validatorsRegistry: validatorsRegistry, - validatorsWithdrawals: VALIDATORS_WITHDRAWALS, - validatorsConsolidations: VALIDATORS_CONSOLIDATIONS, - consolidationsChecker: consolidationsChecker, - osTokenVaultController: deployment.osTokenVaultController, - osTokenConfig: deployment.osTokenConfig, - osTokenVaultEscrow: deployment.osTokenVaultEscrow, - sharedMevEscrow: deployment.sharedMevEscrow, - depositDataRegistry: deployment.depositDataRegistry, - exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY - }); - } - - function _getEthErc20VaultConstructorArgs() internal returns (IEthErc20Vault.EthErc20VaultConstructorArgs memory) { - Deployment memory deployment = getDeploymentData(); - return IEthErc20Vault.EthErc20VaultConstructorArgs({ - keeper: deployment.keeper, - vaultsRegistry: deployment.vaultsRegistry, - validatorsRegistry: validatorsRegistry, - validatorsWithdrawals: VALIDATORS_WITHDRAWALS, - validatorsConsolidations: VALIDATORS_CONSOLIDATIONS, - consolidationsChecker: consolidationsChecker, - osTokenVaultController: deployment.osTokenVaultController, - osTokenConfig: deployment.osTokenConfig, - osTokenVaultEscrow: deployment.osTokenVaultEscrow, - sharedMevEscrow: deployment.sharedMevEscrow, - depositDataRegistry: deployment.depositDataRegistry, - exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY - }); - } - - function _getEthMetaVaultConstructorArgs() internal returns (IEthMetaVault.EthMetaVaultConstructorArgs memory) { + function _getMetaVaultConstructorArgs() internal returns (IMetaVault.MetaVaultConstructorArgs memory) { Deployment memory deployment = getDeploymentData(); - return IEthMetaVault.EthMetaVaultConstructorArgs({ + return IMetaVault.MetaVaultConstructorArgs({ keeper: deployment.keeper, vaultsRegistry: deployment.vaultsRegistry, osTokenVaultController: deployment.osTokenVaultController, osTokenConfig: deployment.osTokenConfig, osTokenVaultEscrow: deployment.osTokenVaultEscrow, - curatorsRegistry: curatorsRegistry, + curatorsRegistry: deployment.curatorsRegistry, exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY }); } diff --git a/script/UpgradeGnoNetwork.s.sol b/script/UpgradeGnoNetwork.s.sol index b4e1a00f..cbe6e6ca 100644 --- a/script/UpgradeGnoNetwork.s.sol +++ b/script/UpgradeGnoNetwork.s.sol @@ -2,54 +2,32 @@ pragma solidity ^0.8.22; -import {console} from "forge-std/console.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IGnoVault} from "../contracts/interfaces/IGnoVault.sol"; -import {IGnoErc20Vault} from "../contracts/interfaces/IGnoErc20Vault.sol"; -import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; +import {IMetaVault} from "../contracts/interfaces/IMetaVault.sol"; import {IVaultVersion} from "../contracts/interfaces/IVaultVersion.sol"; -import {ConsolidationsChecker} from "../contracts/validators/ConsolidationsChecker.sol"; -import {GnoValidatorsChecker} from "../contracts/validators/GnoValidatorsChecker.sol"; -import {GnoRewardSplitter} from "../contracts/misc/GnoRewardSplitter.sol"; -import {RewardSplitterFactory} from "../contracts/misc/RewardSplitterFactory.sol"; -import {GnoGenesisVault} from "../contracts/vaults/gnosis/GnoGenesisVault.sol"; -import {GnoVault} from "../contracts/vaults/gnosis/GnoVault.sol"; -import {GnoErc20Vault} from "../contracts/vaults/gnosis/GnoErc20Vault.sol"; -import {GnoBlocklistVault} from "../contracts/vaults/gnosis/GnoBlocklistVault.sol"; -import {GnoBlocklistErc20Vault} from "../contracts/vaults/gnosis/GnoBlocklistErc20Vault.sol"; -import {GnoPrivVault} from "../contracts/vaults/gnosis/GnoPrivVault.sol"; -import {GnoPrivErc20Vault} from "../contracts/vaults/gnosis/GnoPrivErc20Vault.sol"; -import {IGnoMetaVault, GnoMetaVault} from "../contracts/vaults/gnosis/custom/GnoMetaVault.sol"; -import {GnoVaultFactory} from "../contracts/vaults/gnosis/GnoVaultFactory.sol"; -import {GnoMetaVaultFactory} from "../contracts/vaults/gnosis/custom/GnoMetaVaultFactory.sol"; -import {CuratorsRegistry, ICuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; -import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; +import {IVaultsRegistry} from "../contracts/interfaces/IVaultsRegistry.sol"; import {GnoOsTokenRedeemer} from "../contracts/tokens/GnoOsTokenRedeemer.sol"; +import {GnoValidatorsChecker} from "../contracts/validators/GnoValidatorsChecker.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"; +import {console} from "forge-std/console.sol"; contract UpgradeGnoNetwork is Network { - address public metaVaultFactoryOwner; address public osTokenRedeemerOwner; address public validatorsRegistry; address public gnoToken; uint256 public osTokenRedeemerExitQueueUpdateDelay; - address public consolidationsChecker; address public validatorsChecker; - address public rewardSplitterFactory; - address public tokensConverterFactory; - address public curatorsRegistry; - address public balancedCurator; address public osTokenRedeemer; address[] public vaultImpls; Factory[] public vaultFactories; function run() external { - metaVaultFactoryOwner = vm.envAddress("META_VAULT_FACTORY_OWNER"); osTokenRedeemerOwner = vm.envAddress("OS_TOKEN_REDEEMER_OWNER"); osTokenRedeemerExitQueueUpdateDelay = vm.envUint("OS_TOKEN_REDEEMER_EXIT_QUEUE_UPDATE_DELAY"); - tokensConverterFactory = vm.envAddress("TOKENS_CONVERTER_FACTORY"); validatorsRegistry = vm.envAddress("VALIDATORS_REGISTRY"); gnoToken = vm.envAddress("GNO_TOKEN"); uint256 privateKey = vm.envUint("PRIVATE_KEY"); @@ -61,7 +39,6 @@ contract UpgradeGnoNetwork is Network { vm.startBroadcast(privateKey); // Deploy common contracts - consolidationsChecker = address(new ConsolidationsChecker(deployment.keeper)); validatorsChecker = address( new GnoValidatorsChecker( validatorsRegistry, @@ -72,19 +49,12 @@ contract UpgradeGnoNetwork is Network { gnoToken ) ); - address rewardsSplitterImpl = address(new GnoRewardSplitter(gnoToken)); - rewardSplitterFactory = address(new RewardSplitterFactory(rewardsSplitterImpl)); - - // deploy curators - curatorsRegistry = address(new CuratorsRegistry()); - balancedCurator = address(new BalancedCurator()); - ICuratorsRegistry(curatorsRegistry).addCurator(balancedCurator); - ICuratorsRegistry(curatorsRegistry).initialize(Ownable(deployment.vaultsRegistry).owner()); - // deploy OsToken redeemer + // Deploy OsToken redeemer osTokenRedeemer = address( new GnoOsTokenRedeemer( gnoToken, + deployment.vaultsRegistry, deployment.osToken, deployment.osTokenVaultController, osTokenRedeemerOwner, @@ -98,59 +68,22 @@ contract UpgradeGnoNetwork is Network { generateGovernorTxJson(vaultImpls, vaultFactories, osTokenRedeemer); generateUpgradesJson(vaultImpls); - generateAddressesJson( - vaultFactories, - validatorsChecker, - consolidationsChecker, - rewardSplitterFactory, - curatorsRegistry, - balancedCurator, - osTokenRedeemer - ); + generateAddressesJson(vaultFactories, validatorsChecker, osTokenRedeemer); } function _deployImplementations() internal { // constructors for implementations - IGnoVault.GnoVaultConstructorArgs memory vaultArgs = _getGnoVaultConstructorArgs(); - IGnoErc20Vault.GnoErc20VaultConstructorArgs memory erc20VaultArgs = _getGnoErc20VaultConstructorArgs(); - IGnoMetaVault.GnoMetaVaultConstructorArgs memory metaVaultArgs = _getGnoMetaVaultConstructorArgs(); - Deployment memory deployment = getDeploymentData(); + IMetaVault.MetaVaultConstructorArgs memory metaVaultArgs = _getMetaVaultConstructorArgs(); - // update exited assets claim delay for public vaults - vaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; - erc20VaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; + // deploy meta vaults + metaVaultArgs.exitingAssetsClaimDelay = PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY; + GnoMetaVault gnoMetaVault = new GnoMetaVault(gnoToken, metaVaultArgs); - // deploy genesis vault - GnoGenesisVault gnoGenesisVault = - new GnoGenesisVault(vaultArgs, deployment.legacyPoolEscrow, deployment.legacyRewardToken); + metaVaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; + GnoPrivMetaVault gnoPrivMetaVault = new GnoPrivMetaVault(gnoToken, metaVaultArgs); - // deploy public vaults - GnoVault gnoVault = new GnoVault(vaultArgs); - GnoErc20Vault gnoErc20Vault = new GnoErc20Vault(erc20VaultArgs); - - // deploy blocklist vaults - GnoBlocklistVault gnoBlocklistVault = new GnoBlocklistVault(vaultArgs); - GnoBlocklistErc20Vault gnoBlocklistErc20Vault = new GnoBlocklistErc20Vault(erc20VaultArgs); - - // update exited assets claim delay for private vaults - vaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; - erc20VaultArgs.exitingAssetsClaimDelay = PRIVATE_VAULT_EXITED_ASSETS_CLAIM_DELAY; - - // deploy private vaults - GnoPrivVault gnoPrivVault = new GnoPrivVault(vaultArgs); - GnoPrivErc20Vault gnoPrivErc20Vault = new GnoPrivErc20Vault(erc20VaultArgs); - - // deploy MetaVault - GnoMetaVault gnoMetaVault = new GnoMetaVault(metaVaultArgs); - - vaultImpls.push(address(gnoGenesisVault)); - vaultImpls.push(address(gnoVault)); - vaultImpls.push(address(gnoErc20Vault)); - vaultImpls.push(address(gnoBlocklistVault)); - vaultImpls.push(address(gnoBlocklistErc20Vault)); - vaultImpls.push(address(gnoPrivVault)); - vaultImpls.push(address(gnoPrivErc20Vault)); vaultImpls.push(address(gnoMetaVault)); + vaultImpls.push(address(gnoPrivMetaVault)); } function _deployFactories() internal { @@ -159,89 +92,25 @@ contract UpgradeGnoNetwork is Network { address vaultImpl = vaultImpls[i]; bytes32 vaultId = IVaultVersion(vaultImpl).vaultId(); - // skip factory creation for GnoGenesisVault - if (vaultId == keccak256("GnoGenesisVault")) { - continue; - } - - address factory; + address factory = + address(new GnoMetaVaultFactory(vaultImpl, IVaultsRegistry(deployment.vaultsRegistry), gnoToken)); if (vaultId == keccak256("GnoMetaVault")) { - factory = address( - new GnoMetaVaultFactory( - metaVaultFactoryOwner, vaultImpl, IVaultsRegistry(deployment.vaultsRegistry), gnoToken - ) - ); vaultFactories.push(Factory({name: "MetaVaultFactory", factory: factory})); - continue; - } - - factory = address(new GnoVaultFactory(vaultImpl, IVaultsRegistry(deployment.vaultsRegistry), gnoToken)); - if (vaultId == keccak256("GnoVault")) { - vaultFactories.push(Factory({name: "VaultFactory", factory: address(factory)})); - } else if (vaultId == keccak256("GnoErc20Vault")) { - vaultFactories.push(Factory({name: "Erc20VaultFactory", factory: address(factory)})); - } else if (vaultId == keccak256("GnoBlocklistVault")) { - vaultFactories.push(Factory({name: "BlocklistVaultFactory", factory: address(factory)})); - } else if (vaultId == keccak256("GnoPrivVault")) { - vaultFactories.push(Factory({name: "PrivVaultFactory", factory: address(factory)})); - } else if (vaultId == keccak256("GnoBlocklistErc20Vault")) { - vaultFactories.push(Factory({name: "BlocklistErc20VaultFactory", factory: address(factory)})); - } else if (vaultId == keccak256("GnoPrivErc20Vault")) { - vaultFactories.push(Factory({name: "PrivErc20VaultFactory", factory: address(factory)})); + } else if (vaultId == keccak256("GnoPrivMetaVault")) { + vaultFactories.push(Factory({name: "PrivMetaVaultFactory", factory: factory})); } } } - function _getGnoVaultConstructorArgs() internal returns (IGnoVault.GnoVaultConstructorArgs memory) { - Deployment memory deployment = getDeploymentData(); - return IGnoVault.GnoVaultConstructorArgs({ - keeper: deployment.keeper, - vaultsRegistry: deployment.vaultsRegistry, - validatorsRegistry: validatorsRegistry, - validatorsWithdrawals: VALIDATORS_WITHDRAWALS, - validatorsConsolidations: VALIDATORS_CONSOLIDATIONS, - consolidationsChecker: consolidationsChecker, - osTokenVaultController: deployment.osTokenVaultController, - osTokenConfig: deployment.osTokenConfig, - osTokenVaultEscrow: deployment.osTokenVaultEscrow, - sharedMevEscrow: deployment.sharedMevEscrow, - depositDataRegistry: deployment.depositDataRegistry, - gnoToken: gnoToken, - tokensConverterFactory: tokensConverterFactory, - exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY - }); - } - - function _getGnoErc20VaultConstructorArgs() internal returns (IGnoErc20Vault.GnoErc20VaultConstructorArgs memory) { - Deployment memory deployment = getDeploymentData(); - return IGnoErc20Vault.GnoErc20VaultConstructorArgs({ - keeper: deployment.keeper, - vaultsRegistry: deployment.vaultsRegistry, - validatorsRegistry: validatorsRegistry, - validatorsWithdrawals: VALIDATORS_WITHDRAWALS, - validatorsConsolidations: VALIDATORS_CONSOLIDATIONS, - consolidationsChecker: consolidationsChecker, - osTokenVaultController: deployment.osTokenVaultController, - osTokenConfig: deployment.osTokenConfig, - osTokenVaultEscrow: deployment.osTokenVaultEscrow, - sharedMevEscrow: deployment.sharedMevEscrow, - depositDataRegistry: deployment.depositDataRegistry, - gnoToken: gnoToken, - tokensConverterFactory: tokensConverterFactory, - exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY - }); - } - - function _getGnoMetaVaultConstructorArgs() internal returns (IGnoMetaVault.GnoMetaVaultConstructorArgs memory) { + function _getMetaVaultConstructorArgs() internal returns (IMetaVault.MetaVaultConstructorArgs memory) { Deployment memory deployment = getDeploymentData(); - return IGnoMetaVault.GnoMetaVaultConstructorArgs({ + return IMetaVault.MetaVaultConstructorArgs({ keeper: deployment.keeper, vaultsRegistry: deployment.vaultsRegistry, osTokenVaultController: deployment.osTokenVaultController, osTokenConfig: deployment.osTokenConfig, osTokenVaultEscrow: deployment.osTokenVaultEscrow, - curatorsRegistry: curatorsRegistry, - gnoToken: gnoToken, + curatorsRegistry: deployment.curatorsRegistry, exitingAssetsClaimDelay: PUBLIC_VAULT_EXITED_ASSETS_CLAIM_DELAY }); } diff --git a/snapshots/ConsolidationsCheckerTest.json b/snapshots/ConsolidationsCheckerTest.json index da841d2e..6d56cf6e 100644 --- a/snapshots/ConsolidationsCheckerTest.json +++ b/snapshots/ConsolidationsCheckerTest.json @@ -1,12 +1,12 @@ { - "ConsolidationsCheckerTest_test_verifySignatures_differentValidatorData": "23484", - "ConsolidationsCheckerTest_test_verifySignatures_differentVault": "21375", - "ConsolidationsCheckerTest_test_verifySignatures_emptySignatures": "17460", - "ConsolidationsCheckerTest_test_verifySignatures_exactMinimumSignatures": "38203", - "ConsolidationsCheckerTest_test_verifySignatures_moreThanMinimumSignatures": "38216", - "ConsolidationsCheckerTest_test_verifySignatures_nonOracleSigner": "29123", - "ConsolidationsCheckerTest_test_verifySignatures_repeatedSigner": "25705", - "ConsolidationsCheckerTest_test_verifySignatures_success": "38205", - "ConsolidationsCheckerTest_test_verifySignatures_tooFewSignatures": "13654", - "ConsolidationsCheckerTest_test_verifySignatures_unsortedSignatures": "25701" + "ConsolidationsCheckerTest_test_verifySignatures_differentValidatorData": "23314", + "ConsolidationsCheckerTest_test_verifySignatures_differentVault": "21205", + "ConsolidationsCheckerTest_test_verifySignatures_emptySignatures": "17545", + "ConsolidationsCheckerTest_test_verifySignatures_exactMinimumSignatures": "37705", + "ConsolidationsCheckerTest_test_verifySignatures_moreThanMinimumSignatures": "37718", + "ConsolidationsCheckerTest_test_verifySignatures_nonOracleSigner": "28794", + "ConsolidationsCheckerTest_test_verifySignatures_repeatedSigner": "25331", + "ConsolidationsCheckerTest_test_verifySignatures_success": "37707", + "ConsolidationsCheckerTest_test_verifySignatures_tooFewSignatures": "13679", + "ConsolidationsCheckerTest_test_verifySignatures_unsortedSignatures": "25327" } \ No newline at end of file diff --git a/snapshots/DepositDataRegistryTest.json b/snapshots/DepositDataRegistryTest.json index 186c7c64..4c42af58 100644 --- a/snapshots/DepositDataRegistryTest.json +++ b/snapshots/DepositDataRegistryTest.json @@ -1,9 +1,9 @@ { - "DepositDataRegistryTest_test_registerValidator_succeedsWith0x01Validator": "272487", - "DepositDataRegistryTest_test_registerValidator_succeedsWith0x02Validator": "295393", - "DepositDataRegistryTest_test_registerValidators_successWith0x01Validators": "313965", - "DepositDataRegistryTest_test_registerValidators_successWith0x02Validators": "338034", - "DepositDataRegistryTest_test_setDepositDataManager_succeeds": "66206", - "DepositDataRegistryTest_test_setDepositDataRoot_succeeds": "65127", - "DepositDataRegistryTest_test_updateVaultState_succeeds": "128302" + "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" } \ No newline at end of file diff --git a/snapshots/EthBlocklistErc20VaultTest.json b/snapshots/EthBlocklistErc20VaultTest.json index 2bd80124..940d521b 100644 --- a/snapshots/EthBlocklistErc20VaultTest.json +++ b/snapshots/EthBlocklistErc20VaultTest.json @@ -1,8 +1,8 @@ { - "EthBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "89924", + "EthBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "89912", "EthBlocklistErc20VaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "84302", "EthBlocklistErc20VaultTest_test_canMintOsTokenAsNonBlockedUser": "161501", "EthBlocklistErc20VaultTest_test_deploysCorrectly": "625440", "EthBlocklistErc20VaultTest_test_transfer": "61693", - "EthBlocklistErc20VaultTest_test_upgradesCorrectly": "112276" + "EthBlocklistErc20VaultTest_test_upgradesCorrectly": "119538" } \ No newline at end of file diff --git a/snapshots/EthBlocklistVaultTest.json b/snapshots/EthBlocklistVaultTest.json index bdb38bdd..7c46536f 100644 --- a/snapshots/EthBlocklistVaultTest.json +++ b/snapshots/EthBlocklistVaultTest.json @@ -1,7 +1,7 @@ { - "EthBlocklistVaultTest_test_canDepositAsNonBlockedUser": "87971", + "EthBlocklistVaultTest_test_canDepositAsNonBlockedUser": "87959", "EthBlocklistVaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "82360", "EthBlocklistVaultTest_test_canMintOsTokenAsNonBlockedUser": "161528", "EthBlocklistVaultTest_test_deploysCorrectly": "550241", - "EthBlocklistVaultTest_test_upgradesCorrectly": "111607" + "EthBlocklistVaultTest_test_upgradesCorrectly": "118869" } \ No newline at end of file diff --git a/snapshots/EthErc20VaultTest.json b/snapshots/EthErc20VaultTest.json index 2b8c33c2..d27828c7 100644 --- a/snapshots/EthErc20VaultTest.json +++ b/snapshots/EthErc20VaultTest.json @@ -2,14 +2,14 @@ "EthErc20VaultTest_test_canTransferFromSharesWithHighLtv": "90077", "EthErc20VaultTest_test_cannotTransferFromSharesWithLowLtv": "96608", "EthErc20VaultTest_test_deploysCorrectly": "455018", - "EthErc20VaultTest_test_depositAndMintOsToken": "201247", - "EthErc20VaultTest_test_depositViaReceiveFallback_emitsTransfer": "75451", - "EthErc20VaultTest_test_deposit_emitsTransfer": "78913", + "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": "177077", - "EthErc20VaultTest_test_updateStateAndDepositAndMintOsToken": "227535", - "EthErc20VaultTest_test_upgradesCorrectly": "112150", + "EthErc20VaultTest_test_updateExitQueue_emitsTransfer": "177065", + "EthErc20VaultTest_test_updateStateAndDepositAndMintOsToken": "230047", + "EthErc20VaultTest_test_upgradesCorrectly": "119424", "EthErc20VaultTest_test_withdrawValidator_unknown": "55951", "EthErc20VaultTest_test_withdrawValidator_validatorsManager": "74092" } \ No newline at end of file diff --git a/snapshots/EthFoxVaultTest.json b/snapshots/EthFoxVaultTest.json index 4b7f8ac4..9883cf17 100644 --- a/snapshots/EthFoxVaultTest.json +++ b/snapshots/EthFoxVaultTest.json @@ -1,8 +1,8 @@ { - "EthFoxVaultTest_test_canDepositAsNonBlockedUser": "87565", - "EthFoxVaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "81932", - "EthFoxVaultTest_test_ejectUser": "107257", - "EthFoxVaultTest_test_ejectUserWithNoShares": "62862", + "EthFoxVaultTest_test_canDepositAsNonBlockedUser": "90053", + "EthFoxVaultTest_test_canDepositUsingReceiveAsNotBlockedUser": "84432", + "EthFoxVaultTest_test_ejectUser": "109757", + "EthFoxVaultTest_test_ejectUserWithNoShares": "65362", "EthFoxVaultTest_test_withdrawValidator_unknown": "55886", "EthFoxVaultTest_test_withdrawValidator_validatorsManager": "73964" } \ No newline at end of file diff --git a/snapshots/EthGenesisVaultTest.json b/snapshots/EthGenesisVaultTest.json index 2b6602e7..a8226ddb 100644 --- a/snapshots/EthGenesisVaultTest.json +++ b/snapshots/EthGenesisVaultTest.json @@ -1,8 +1,7 @@ { - "EthGenesisVaultTest_test_claimsPoolEscrowAssets": "77662", - "EthGenesisVaultTest_test_fallback_acceptsEtherFromPoolEscrow": "33277", - "EthGenesisVaultTest_test_fallback_acceptsEtherFromUser": "77540", - "EthGenesisVaultTest_test_migrate_works": "201753", - "EthGenesisVaultTest_test_upgradesCorrectly": "91142", - "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "803339" + "EthGenesisVaultTest_test_claimsPoolEscrowAssets": "78175", + "EthGenesisVaultTest_test_fallback_acceptsEtherFromPoolEscrow": "35807", + "EthGenesisVaultTest_test_fallback_acceptsEtherFromUser": "79346", + "EthGenesisVaultTest_test_migrate_works": "204956", + "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "819842" } \ No newline at end of file diff --git a/snapshots/EthMetaVaultTest.json b/snapshots/EthMetaVaultTest.json index 166cc670..520a8996 100644 --- a/snapshots/EthMetaVaultTest.json +++ b/snapshots/EthMetaVaultTest.json @@ -1,9 +1,17 @@ { - "EthMetaVaultTest_test_deposit": "83013", - "EthMetaVaultTest_test_depositAndMintOsToken": "179864", - "EthMetaVaultTest_test_depositViaFallback": "79631", - "EthMetaVaultTest_test_isStateUpdateRequired_true": "138057", - "EthMetaVaultTest_test_updateStateAndDeposit": "188941", - "EthMetaVaultTest_test_updateStateAndDepositAndMintOsToken": "200835", - "EthMetaVaultTest_test_userClaimExitedAssets": "51730" + "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" } \ No newline at end of file diff --git a/snapshots/EthOsTokenRedeemerTest.json b/snapshots/EthOsTokenRedeemerTest.json index 389fe399..03035fdf 100644 --- a/snapshots/EthOsTokenRedeemerTest.json +++ b/snapshots/EthOsTokenRedeemerTest.json @@ -1,17 +1,16 @@ { - "EthOsTokenRedeemerTest_test_acceptRedeemablePositions_success": "94939", - "EthOsTokenRedeemerTest_test_denyRedeemablePositions_success": "30268", - "EthOsTokenRedeemerTest_test_enterExitQueue_success": "107164", - "EthOsTokenRedeemerTest_test_permitOsToken_success": "82596", - "EthOsTokenRedeemerTest_test_processExitQueue_nothingToProcess": "31498", - "EthOsTokenRedeemerTest_test_processExitQueue_success": "99215", - "EthOsTokenRedeemerTest_test_proposeRedeemablePositions_success": "83218", - "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_multiplePositions": "292793", - "EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "199173", - "EthOsTokenRedeemerTest_test_removeRedeemablePositions_success": "30304", - "EthOsTokenRedeemerTest_test_setPositionsManager": "35796", - "EthOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "101315", - "test_claimExitedAssets_fullWithdrawal": "50998", - "test_claimExitedAssets_noPosition": "30156", - "test_claimExitedAssets_partialWithdrawal": "75835" + "EthOsTokenRedeemerTest_test_enterExitQueue_success": "109662", + "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_setPositionsManager": "35834", + "EthOsTokenRedeemerTest_test_setRedeemablePositions_success": "100561", + "EthOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "101264", + "test_claimExitedAssets_fullWithdrawal": "53564", + "test_claimExitedAssets_noPosition": "30222", + "test_claimExitedAssets_partialWithdrawal": "75901", + "test_redeemSubVaultsAssets_concurrentMultipleSubVaults": "675026", + "test_redeemSubVaultsAssets_largeAmountAcrossAllSubVaults": "816491" } \ No newline at end of file diff --git a/snapshots/EthOsTokenVaultEscrowTest.json b/snapshots/EthOsTokenVaultEscrowTest.json index dbd93706..33bc1d77 100644 --- a/snapshots/EthOsTokenVaultEscrowTest.json +++ b/snapshots/EthOsTokenVaultEscrowTest.json @@ -1,35 +1,35 @@ { "EthOsTokenVaultEscrowTest_test_claimExitedAssets_insufficientShares": "66982", - "EthOsTokenVaultEscrowTest_test_claimExitedAssets_minimalAmount": "92563", + "EthOsTokenVaultEscrowTest_test_claimExitedAssets_minimalAmount": "95063", "EthOsTokenVaultEscrowTest_test_claimExitedAssets_noProcessedAssets": "91451", "EthOsTokenVaultEscrowTest_test_claimExitedAssets_nonExistentPosition": "50879", "EthOsTokenVaultEscrowTest_test_claimExitedAssets_notOwner": "65015", - "EthOsTokenVaultEscrowTest_test_claimExitedAssets_withFeeAccrual": "101458", - "EthOsTokenVaultEscrowTest_test_processExitedAssets_claimExitedAssets": "77940", + "EthOsTokenVaultEscrowTest_test_claimExitedAssets_withFeeAccrual": "103958", + "EthOsTokenVaultEscrowTest_test_processExitedAssets_claimExitedAssets": "80440", "EthOsTokenVaultEscrowTest_test_processExitedAssets_exitRequestNotProcessed": "49774", "EthOsTokenVaultEscrowTest_test_processExitedAssets_invalidPosition": "33255", - "EthOsTokenVaultEscrowTest_test_processExitedAssets_partialClaim": "93137", + "EthOsTokenVaultEscrowTest_test_processExitedAssets_partialClaim": "95637", "EthOsTokenVaultEscrowTest_test_processExitedAssets_success": "71278", - "EthOsTokenVaultEscrowTest_test_register_accessDenied": "31199", - "EthOsTokenVaultEscrowTest_test_register_directCall": "81100", + "EthOsTokenVaultEscrowTest_test_register_accessDenied": "33699", + "EthOsTokenVaultEscrowTest_test_register_directCall": "83600", "EthOsTokenVaultEscrowTest_test_register_fullFlow": "163659", - "EthOsTokenVaultEscrowTest_test_register_invalidShares": "31242", - "EthOsTokenVaultEscrowTest_test_register_zeroAddress": "30886", + "EthOsTokenVaultEscrowTest_test_register_invalidShares": "33742", + "EthOsTokenVaultEscrowTest_test_register_zeroAddress": "33386", "EthOsTokenVaultEscrowTest_test_setAuthenticator_onlyOwner": "30174", - "EthOsTokenVaultEscrowTest_test_setAuthenticator_success": "33014", - "EthOsTokenVaultEscrowTest_test_setAuthenticator_valueNotChanged": "29509", + "EthOsTokenVaultEscrowTest_test_setAuthenticator_success": "35514", + "EthOsTokenVaultEscrowTest_test_setAuthenticator_valueNotChanged": "32009", "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidBonus_high": "27548", - "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidBonus_low": "27626", + "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidBonus_low": "30126", "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidThreshold_max": "27337", - "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidThreshold_zero": "27470", + "EthOsTokenVaultEscrowTest_test_updateLiqConfig_invalidThreshold_zero": "29970", "EthOsTokenVaultEscrowTest_test_updateLiqConfig_onlyOwner": "30318", - "EthOsTokenVaultEscrowTest_test_updateLiqConfig_success": "38745", - "OsTokenLiquidationTest_test_liquidateOsToken_invalidHealthFactor": "49969", + "EthOsTokenVaultEscrowTest_test_updateLiqConfig_success": "41245", + "OsTokenLiquidationTest_test_liquidateOsToken_invalidHealthFactor": "52469", "OsTokenLiquidationTest_test_liquidateOsToken_invalidPosition": "41078", "OsTokenLiquidationTest_test_liquidateOsToken_invalidReceivedAssets": "41999", "OsTokenLiquidationTest_test_liquidateOsToken_partialLiquidation": "94624", "OsTokenLiquidationTest_test_liquidateOsToken_success": "97295", "OsTokenLiquidationTest_test_liquidateOsToken_zeroAddress": "24032", "OsTokenLiquidationTest_test_redeemOsToken_notRedeemer": "27478", - "OsTokenLiquidationTest_test_redeemOsToken_success": "118722" + "OsTokenLiquidationTest_test_redeemOsToken_success": "118710" } \ No newline at end of file diff --git a/snapshots/EthPrivErc20VaultTest.json b/snapshots/EthPrivErc20VaultTest.json index eb18d641..74df5a12 100644 --- a/snapshots/EthPrivErc20VaultTest.json +++ b/snapshots/EthPrivErc20VaultTest.json @@ -1,9 +1,9 @@ { "EthPrivErc20VaultTest_test_canDepositAndMintOsTokenAsWhitelistedUser": "206655", - "EthPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "81407", + "EthPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "81395", "EthPrivErc20VaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "77693", "EthPrivErc20VaultTest_test_canMintOsTokenAsWhitelistedUser": "161601", "EthPrivErc20VaultTest_test_deploysCorrectly": "625462", "EthPrivErc20VaultTest_test_transfer": "59709", - "EthPrivErc20VaultTest_test_upgradesCorrectly": "112296" + "EthPrivErc20VaultTest_test_upgradesCorrectly": "119559" } \ No newline at end of file diff --git a/snapshots/EthPrivMetaVaultTest.json b/snapshots/EthPrivMetaVaultTest.json new file mode 100644 index 00000000..dd76de74 --- /dev/null +++ b/snapshots/EthPrivMetaVaultTest.json @@ -0,0 +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" +} \ No newline at end of file diff --git a/snapshots/EthPrivVaultTest.json b/snapshots/EthPrivVaultTest.json index c580c2f1..e860566b 100644 --- a/snapshots/EthPrivVaultTest.json +++ b/snapshots/EthPrivVaultTest.json @@ -1,11 +1,11 @@ { - "EthPrivVaultTest_test_canDepositAsWhitelistedUser": "79443", + "EthPrivVaultTest_test_canDepositAsWhitelistedUser": "79431", "EthPrivVaultTest_test_canDepositUsingReceiveAsWhitelistedUser": "75857", "EthPrivVaultTest_test_canMintOsTokenAsWhitelistedUser": "161628", - "EthPrivVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "138187", + "EthPrivVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser": "140687", "EthPrivVaultTest_test_deploysCorrectly": "550259", "EthPrivVaultTest_test_depositAndMintOsTokenAsWhitelistedUser": "200570", "EthPrivVaultTest_test_setWhitelister": "36608", "EthPrivVaultTest_test_updateWhitelist": "54543", - "EthPrivVaultTest_test_upgradesCorrectly": "111722" + "EthPrivVaultTest_test_upgradesCorrectly": "118985" } \ No newline at end of file diff --git a/snapshots/EthRewardSplitterTest.json b/snapshots/EthRewardSplitterTest.json index 2c83421a..6d3d62db 100644 --- a/snapshots/EthRewardSplitterTest.json +++ b/snapshots/EthRewardSplitterTest.json @@ -1,14 +1,14 @@ { - "EthRewardSplitter_claimExitedAssetsOnBehalf": "749608", - "EthRewardSplitter_claimVaultTokens": "754243", - "EthRewardSplitter_decreaseShares": "87633", - "EthRewardSplitter_enterExitQueue": "166850", - "EthRewardSplitter_enterExitQueueMaxWithdrawal": "128878", - "EthRewardSplitter_enterExitQueueOnBehalf": "947262", + "EthRewardSplitter_claimExitedAssetsOnBehalf": "767084", + "EthRewardSplitter_claimVaultTokens": "771719", + "EthRewardSplitter_decreaseShares": "90133", + "EthRewardSplitter_enterExitQueue": "171850", + "EthRewardSplitter_enterExitQueueMaxWithdrawal": "131378", + "EthRewardSplitter_enterExitQueueOnBehalf": "964738", "EthRewardSplitter_increaseShares": "73128", "EthRewardSplitter_receiveEth": "31194", "EthRewardSplitter_syncRewards": "77682", - "EthRewardSplitter_syncRewardsDetailed": "75182", + "EthRewardSplitter_syncRewardsDetailed": "77682", "EthRewardSplitter_test_setClaimer": "66269", - "EthRewardSplitter_updateVaultState": "132625" + "EthRewardSplitter_updateVaultState": "132613" } \ No newline at end of file diff --git a/snapshots/EthVaultTest.json b/snapshots/EthVaultTest.json index 04ae719f..395ae7c5 100644 --- a/snapshots/EthVaultTest.json +++ b/snapshots/EthVaultTest.json @@ -1,10 +1,10 @@ { "EthVaultTest_test_deploysCorrectly": "379895", - "EthVaultTest_test_depositAndMintOsToken": "199548", - "EthVaultTest_test_exitQueue_works": "96643", - "EthVaultTest_test_fallbackDeposit": "75127", - "EthVaultTest_test_updateStateAndDepositAndMintOsToken": "228158", - "EthVaultTest_test_upgradesCorrectly": "111566", + "EthVaultTest_test_depositAndMintOsToken": "202060", + "EthVaultTest_test_exitQueue_works": "99131", + "EthVaultTest_test_fallbackDeposit": "77627", + "EthVaultTest_test_updateStateAndDepositAndMintOsToken": "228182", + "EthVaultTest_test_upgradesCorrectly": "118828", "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 afabf55d..2f80b1dd 100644 --- a/snapshots/GnoBlocklistErc20VaultTest.json +++ b/snapshots/GnoBlocklistErc20VaultTest.json @@ -1,7 +1,7 @@ { - "GnoBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "160088", + "GnoBlocklistErc20VaultTest_test_canDepositAsNonBlockedUser": "160108", "GnoBlocklistErc20VaultTest_test_canMintOsTokenAsNonBlockedUser": "161610", - "GnoBlocklistErc20VaultTest_test_deploysCorrectly": "953673", + "GnoBlocklistErc20VaultTest_test_deploysCorrectly": "953762", "GnoBlocklistErc20VaultTest_test_transfer": "61693", - "GnoBlocklistErc20VaultTest_test_upgradesCorrectly": "335109" + "GnoBlocklistErc20VaultTest_test_upgradesCorrectly": "339872" } \ No newline at end of file diff --git a/snapshots/GnoBlocklistVaultTest.json b/snapshots/GnoBlocklistVaultTest.json index d5257935..27856b9d 100644 --- a/snapshots/GnoBlocklistVaultTest.json +++ b/snapshots/GnoBlocklistVaultTest.json @@ -1,6 +1,6 @@ { - "GnoBlocklistVaultTest_test_canDepositAsNonBlockedUser": "158104", + "GnoBlocklistVaultTest_test_canDepositAsNonBlockedUser": "158124", "GnoBlocklistVaultTest_test_canMintOsTokenAsNonBlockedUser": "161570", - "GnoBlocklistVaultTest_test_deploysCorrectly": "702298", - "GnoBlocklistVaultTest_test_upgradesCorrectly": "334405" + "GnoBlocklistVaultTest_test_deploysCorrectly": "702387", + "GnoBlocklistVaultTest_test_upgradesCorrectly": "339168" } \ No newline at end of file diff --git a/snapshots/GnoErc20VaultTest.json b/snapshots/GnoErc20VaultTest.json index e324c666..7581c144 100644 --- a/snapshots/GnoErc20VaultTest.json +++ b/snapshots/GnoErc20VaultTest.json @@ -1,11 +1,11 @@ { "GnoErc20VaultTest_test_canTransferFromSharesWithHighLtv": "90100", "GnoErc20VaultTest_test_cannotTransferFromSharesWithLowLtv": "96637", - "GnoErc20VaultTest_test_deploysCorrectly": "753241", - "GnoErc20VaultTest_test_deposit_emitsTransfer": "98232", + "GnoErc20VaultTest_test_deploysCorrectly": "753330", + "GnoErc20VaultTest_test_deposit_emitsTransfer": "100732", "GnoErc20VaultTest_test_enterExitQueue_emitsTransfer": "89780", "GnoErc20VaultTest_test_redeem_emitsEvent": "77069", - "GnoErc20VaultTest_test_upgradesCorrectly": "335083", + "GnoErc20VaultTest_test_upgradesCorrectly": "339858", "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 1644737d..3ac51c48 100644 --- a/snapshots/GnoGenesisVaultTest.json +++ b/snapshots/GnoGenesisVaultTest.json @@ -1,5 +1,5 @@ { - "GnoGenesisVaultTest_test_migrate_works": "204216", - "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "867236", - "GnoGenesisVaultTest_test_upgradesCorrectly": "5823320" + "GnoGenesisVaultTest_test_migrate_works": "206716", + "GnoGenesisVaultTest_test_pullWithdrawals_claimEscrowAssets": "879736", + "GnoGenesisVaultTest_test_upgradesCorrectly": "5830554" } \ No newline at end of file diff --git a/snapshots/GnoMetaVaultTest.json b/snapshots/GnoMetaVaultTest.json index 0ab977f6..ffe00ffa 100644 --- a/snapshots/GnoMetaVaultTest.json +++ b/snapshots/GnoMetaVaultTest.json @@ -1,9 +1,9 @@ { - "GnoMetaVaultTest_test_addSubVault": "154448", - "GnoMetaVaultTest_test_claimExitedAssets": "67870", - "GnoMetaVaultTest_test_deposit": "103288", - "GnoMetaVaultTest_test_depositToSubVaults": "326938", - "GnoMetaVaultTest_test_ejectSubVault": "211758", - "GnoMetaVaultTest_test_enterExitQueue": "86712", - "GnoMetaVaultTest_test_updateState": "149972" + "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" } \ No newline at end of file diff --git a/snapshots/GnoOsTokenRedeemerTest.json b/snapshots/GnoOsTokenRedeemerTest.json index 84c531eb..1bb452bb 100644 --- a/snapshots/GnoOsTokenRedeemerTest.json +++ b/snapshots/GnoOsTokenRedeemerTest.json @@ -1,6 +1,6 @@ { - "GnoOsTokenRedeemerTest_test_claimExitedAssets_fullWithdrawal": "80429", + "GnoOsTokenRedeemerTest_test_claimExitedAssets_fullWithdrawal": "80495", "GnoOsTokenRedeemerTest_test_permitGnoToken_success": "91926", - "GnoOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "250801", - "GnoOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "138041" + "GnoOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition": "252803", + "GnoOsTokenRedeemerTest_test_swapAssetsToOsTokenShares_success": "137991" } \ No newline at end of file diff --git a/snapshots/GnoOsTokenVaultEscrowTest.json b/snapshots/GnoOsTokenVaultEscrowTest.json index 8f353a4e..7e35444f 100644 --- a/snapshots/GnoOsTokenVaultEscrowTest.json +++ b/snapshots/GnoOsTokenVaultEscrowTest.json @@ -1,5 +1,5 @@ { "GnoOsTokenVaultEscrowTest_test_transferAssets_claim": "140340", - "GnoOsTokenVaultEscrowTest_test_transferAssets_process": "873932", + "GnoOsTokenVaultEscrowTest_test_transferAssets_process": "893932", "GnoOsTokenVaultEscrowTest_test_transferAssets_transfer": "163157" } \ No newline at end of file diff --git a/snapshots/GnoPrivErc20VaultTest.json b/snapshots/GnoPrivErc20VaultTest.json index 381fca97..2a1d7fa1 100644 --- a/snapshots/GnoPrivErc20VaultTest.json +++ b/snapshots/GnoPrivErc20VaultTest.json @@ -1,7 +1,7 @@ { - "GnoPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "156017", + "GnoPrivErc20VaultTest_test_canDepositAsWhitelistedUser": "156037", "GnoPrivErc20VaultTest_test_canMintOsTokenAsWhitelistedUser": "161543", - "GnoPrivErc20VaultTest_test_deploysCorrectly": "953717", + "GnoPrivErc20VaultTest_test_deploysCorrectly": "953806", "GnoPrivErc20VaultTest_test_transfer": "59709", - "GnoPrivErc20VaultTest_test_upgradesCorrectly": "335246" + "GnoPrivErc20VaultTest_test_upgradesCorrectly": "340008" } \ No newline at end of file diff --git a/snapshots/GnoPrivMetaVaultTest.json b/snapshots/GnoPrivMetaVaultTest.json new file mode 100644 index 00000000..433487f0 --- /dev/null +++ b/snapshots/GnoPrivMetaVaultTest.json @@ -0,0 +1,8 @@ +{ + "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 6a30f899..ff248b50 100644 --- a/snapshots/GnoPrivVaultTest.json +++ b/snapshots/GnoPrivVaultTest.json @@ -1,8 +1,8 @@ { - "GnoPrivVaultTest_test_canDepositAsWhitelistedUser": "154120", + "GnoPrivVaultTest_test_canDepositAsWhitelistedUser": "154140", "GnoPrivVaultTest_test_canMintOsTokenAsWhitelistedUser": "161480", - "GnoPrivVaultTest_test_deploysCorrectly": "702342", + "GnoPrivVaultTest_test_deploysCorrectly": "702431", "GnoPrivVaultTest_test_setWhitelister": "36608", "GnoPrivVaultTest_test_updateWhitelist": "54521", - "GnoPrivVaultTest_test_upgradesCorrectly": "334541" + "GnoPrivVaultTest_test_upgradesCorrectly": "339304" } \ No newline at end of file diff --git a/snapshots/GnoRewardSplitterTest.json b/snapshots/GnoRewardSplitterTest.json index 31c8eead..28733f70 100644 --- a/snapshots/GnoRewardSplitterTest.json +++ b/snapshots/GnoRewardSplitterTest.json @@ -1,14 +1,14 @@ { - "GnoRewardSplitter_claimExitedAssets": "92309", - "GnoRewardSplitter_claimExitedAssetsOnBehalf": "806894", - "GnoRewardSplitter_claimVaultTokens": "838126", - "GnoRewardSplitter_decreaseShares": "96041", - "GnoRewardSplitter_enterExitQueue": "184229", - "GnoRewardSplitter_enterExitQueueMaxWithdrawal": "128900", - "GnoRewardSplitter_enterExitQueueOnBehalf": "1011528", - "GnoRewardSplitter_increaseShares": "70672", + "GnoRewardSplitter_claimExitedAssets": "94809", + "GnoRewardSplitter_claimExitedAssetsOnBehalf": "824394", + "GnoRewardSplitter_claimVaultTokens": "855626", + "GnoRewardSplitter_decreaseShares": "101041", + "GnoRewardSplitter_enterExitQueue": "186729", + "GnoRewardSplitter_enterExitQueueMaxWithdrawal": "131400", + "GnoRewardSplitter_enterExitQueueOnBehalf": "1029028", + "GnoRewardSplitter_increaseShares": "73172", "GnoRewardSplitter_setClaimer": "66291", "GnoRewardSplitter_syncRewards": "77704", - "GnoRewardSplitter_syncRewardsDetailed": "75204", + "GnoRewardSplitter_syncRewardsDetailed": "77704", "GnoRewardSplitter_updateVaultState": "165440" } \ No newline at end of file diff --git a/snapshots/GnoVaultExitQueueTest.json b/snapshots/GnoVaultExitQueueTest.json index c8efe3e0..d054d0a7 100644 --- a/snapshots/GnoVaultExitQueueTest.json +++ b/snapshots/GnoVaultExitQueueTest.json @@ -1,7 +1,7 @@ { "GnoVaultExitQueueTest_test_ExitingAssetsPenalized_event": "194845", - "GnoVaultExitQueueTest_test_claim_position1_after_upgrade": "6621623", - "GnoVaultExitQueueTest_test_claim_position2_before_upgrade": "123260", - "GnoVaultExitQueueTest_test_claim_position3_after_upgrade": "160251", - "GnoVaultExitQueueTest_test_claim_position4_after_upgrade": "155429" + "GnoVaultExitQueueTest_test_claim_position1_after_upgrade": "6653963", + "GnoVaultExitQueueTest_test_claim_position2_before_upgrade": "125766", + "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 121303d3..0ddb8366 100644 --- a/snapshots/GnoVaultTest.json +++ b/snapshots/GnoVaultTest.json @@ -1,7 +1,7 @@ { - "GnoVaultTest_test_deploysCorrectly": "678256", - "GnoVaultTest_test_exitQueue_works": "96643", - "GnoVaultTest_test_upgradesCorrectly": "334443", + "GnoVaultTest_test_deploysCorrectly": "678345", + "GnoVaultTest_test_exitQueue_works": "99131", + "GnoVaultTest_test_upgradesCorrectly": "339206", "GnoVaultTest_test_withdrawValidator_unknown": "55950", "GnoVaultTest_test_withdrawValidator_validatorsManager": "74091" } \ No newline at end of file diff --git a/snapshots/KeeperOraclesTest.json b/snapshots/KeeperOraclesTest.json index a1d30fcf..4ded9d36 100644 --- a/snapshots/KeeperOraclesTest.json +++ b/snapshots/KeeperOraclesTest.json @@ -1,12 +1,12 @@ { - "KeeperOraclesTest_test_addOracle_alreadyAdded": "28022", - "KeeperOraclesTest_test_addOracle_maxOraclesExceeded": "33511", - "KeeperOraclesTest_test_addOracle_onlyOwner": "32780", - "KeeperOraclesTest_test_addOracle_success": "55972", - "KeeperOraclesTest_test_removeOracle_alreadyRemoved": "32141", - "KeeperOraclesTest_test_removeOracle_onlyOwner": "26396", - "KeeperOraclesTest_test_removeOracle_success": "34154", + "KeeperOraclesTest_test_addOracle_alreadyAdded": "28034", + "KeeperOraclesTest_test_addOracle_maxOraclesExceeded": "36011", + "KeeperOraclesTest_test_addOracle_onlyOwner": "32792", + "KeeperOraclesTest_test_addOracle_success": "58484", + "KeeperOraclesTest_test_removeOracle_alreadyRemoved": "34653", + "KeeperOraclesTest_test_removeOracle_onlyOwner": "26408", + "KeeperOraclesTest_test_removeOracle_success": "36666", "KeeperOraclesTest_test_updateConfig_onlyOwner": "31726", "KeeperOraclesTest_test_updateConfig_success": "32787", - "KeeperOraclesTest_test_verifySignatures_throughKeeperRewards": "234771" + "KeeperOraclesTest_test_verifySignatures_throughKeeperRewards": "242271" } \ No newline at end of file diff --git a/snapshots/KeeperRewardsTest.json b/snapshots/KeeperRewardsTest.json index 9192df01..78e34a09 100644 --- a/snapshots/KeeperRewardsTest.json +++ b/snapshots/KeeperRewardsTest.json @@ -1,20 +1,20 @@ { "KeeperRewardsTest_test_canHarvest": "12938", "KeeperRewardsTest_test_harvest": "130843", - "KeeperRewardsTest_test_harvestWithPenalties": "92536", - "KeeperRewardsTest_test_harvest_alreadyHarvested": "47972", + "KeeperRewardsTest_test_harvestWithPenalties": "95048", + "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": "135325", - "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_2": "581737", - "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_3": "581747", - "KeeperRewardsTest_test_setRewardsMinOracles": "35248", + "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_1": "135313", + "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_2": "606725", + "KeeperRewardsTest_test_multipleRewardUpdatesAndHarvests_round_3": "606771", + "KeeperRewardsTest_test_setRewardsMinOracles": "37748", "KeeperRewardsTest_test_setRewardsMinOracles_tooMany": "29542", - "KeeperRewardsTest_test_setRewardsMinOracles_zero": "27531", - "KeeperRewardsTest_test_updateRewards": "98923", - "KeeperRewardsTest_test_updateRewards_invalidAvgRewardPerSecond": "30759", - "KeeperRewardsTest_test_updateRewards_tooEarly": "30750" + "KeeperRewardsTest_test_setRewardsMinOracles_zero": "30031", + "KeeperRewardsTest_test_updateRewards": "101423", + "KeeperRewardsTest_test_updateRewards_invalidAvgRewardPerSecond": "33259", + "KeeperRewardsTest_test_updateRewards_tooEarly": "33250" } \ No newline at end of file diff --git a/snapshots/KeeperValidatorsTest.json b/snapshots/KeeperValidatorsTest.json index 14d1236d..30f8465b 100644 --- a/snapshots/KeeperValidatorsTest.json +++ b/snapshots/KeeperValidatorsTest.json @@ -1,15 +1,15 @@ { - "KeeperValidatorsTest_test_approveValidators_accessDenied": "139566", + "KeeperValidatorsTest_test_approveValidators_accessDenied": "139546", "KeeperValidatorsTest_test_approveValidators_invalidDeadline": "31828", - "KeeperValidatorsTest_test_approveValidators_invalidRegistry": "134465", - "KeeperValidatorsTest_test_approveValidators_success": "179045", - "KeeperValidatorsTest_test_setValidatorsMinOracles_success": "35099", + "KeeperValidatorsTest_test_approveValidators_invalidRegistry": "134445", + "KeeperValidatorsTest_test_approveValidators_success": "179025", + "KeeperValidatorsTest_test_setValidatorsMinOracles_success": "37599", "KeeperValidatorsTest_test_setValidatorsMinOracles_tooHigh": "29460", - "KeeperValidatorsTest_test_setValidatorsMinOracles_unauthorized": "27745", - "KeeperValidatorsTest_test_setValidatorsMinOracles_zero": "27382", - "KeeperValidatorsTest_test_updateExitSignatures_duplicateUpdate": "45635", - "KeeperValidatorsTest_test_updateExitSignatures_expiredDeadline": "34338", + "KeeperValidatorsTest_test_setValidatorsMinOracles_unauthorized": "30245", + "KeeperValidatorsTest_test_setValidatorsMinOracles_zero": "29882", + "KeeperValidatorsTest_test_updateExitSignatures_duplicateUpdate": "45623", + "KeeperValidatorsTest_test_updateExitSignatures_expiredDeadline": "36838", "KeeperValidatorsTest_test_updateExitSignatures_invalidVault": "31986", - "KeeperValidatorsTest_test_updateExitSignatures_notCollateralized": "34313", - "KeeperValidatorsTest_test_updateExitSignatures_success": "68010" + "KeeperValidatorsTest_test_updateExitSignatures_notCollateralized": "34301", + "KeeperValidatorsTest_test_updateExitSignatures_success": "70498" } \ No newline at end of file diff --git a/snapshots/OsTokenConfigTest.json b/snapshots/OsTokenConfigTest.json index d687b7ca..e072ef42 100644 --- a/snapshots/OsTokenConfigTest.json +++ b/snapshots/OsTokenConfigTest.json @@ -1,7 +1,7 @@ { - "OsTokenConfigForkTest_test_setRedeemer": "32972", - "OsTokenConfigForkTest_test_setRedeemer_notOwner": "29710", - "OsTokenConfigForkTest_test_setRedeemer_sameValue": "29381", + "OsTokenConfigForkTest_test_setRedeemer": "35472", + "OsTokenConfigForkTest_test_setRedeemer_notOwner": "32210", + "OsTokenConfigForkTest_test_setRedeemer_sameValue": "31881", "OsTokenConfigForkTest_test_updateConfig_disabledLiquidations": "56787", "OsTokenConfigForkTest_test_updateConfig_forVault": "55186", "OsTokenConfigForkTest_test_updateConfig_invalidDisabledLiquidations": "33162", @@ -11,7 +11,7 @@ "OsTokenConfigForkTest_test_updateConfig_invalidLiqThresholdPercent_tooHigh": "28640", "OsTokenConfigForkTest_test_updateConfig_invalidLiqThresholdPercent_zero": "33104", "OsTokenConfigForkTest_test_updateConfig_invalidLtvPercent_tooHigh": "28671", - "OsTokenConfigForkTest_test_updateConfig_invalidLtvPercent_zero": "30479", + "OsTokenConfigForkTest_test_updateConfig_invalidLtvPercent_zero": "32979", "OsTokenConfigForkTest_test_updateConfig_notOwner": "33393", "OsTokenConfigTest_test_updateDefaultConfig_success": "37692" } \ No newline at end of file diff --git a/snapshots/OsTokenTest.json b/snapshots/OsTokenTest.json index 95b2c256..7291565c 100644 --- a/snapshots/OsTokenTest.json +++ b/snapshots/OsTokenTest.json @@ -1,19 +1,19 @@ { - "OsTokenTest_test_burn": "36742", + "OsTokenTest_test_burn": "39242", "OsTokenTest_test_burn_onlyController": "27956", - "OsTokenTest_test_controllerIntegration_burn": "46843", - "OsTokenTest_test_controllerIntegration_mint": "105994", - "OsTokenTest_test_erc20_transfer": "54454", - "OsTokenTest_test_erc20_transferFrom": "112658", + "OsTokenTest_test_controllerIntegration_burn": "51843", + "OsTokenTest_test_controllerIntegration_mint": "106082", + "OsTokenTest_test_erc20_transfer": "54466", + "OsTokenTest_test_erc20_transferFrom": "117670", "OsTokenTest_test_fullDepositFlow": "197995", - "OsTokenTest_test_mint": "58400", + "OsTokenTest_test_mint": "60900", "OsTokenTest_test_mint_onlyController": "30281", - "OsTokenTest_test_permit": "77632", + "OsTokenTest_test_permit": "80144", "OsTokenTest_test_permit_expiredDeadline": "32235", - "OsTokenTest_test_permit_invalidSignature": "53951", + "OsTokenTest_test_permit_invalidSignature": "56463", "OsTokenTest_test_permit_zeroAddress": "56033", - "OsTokenTest_test_setController_add": "53103", + "OsTokenTest_test_setController_add": "55603", "OsTokenTest_test_setController_onlyOwner": "32836", - "OsTokenTest_test_setController_remove": "39137", - "OsTokenTest_test_setController_zeroAddress": "27570" + "OsTokenTest_test_setController_remove": "41637", + "OsTokenTest_test_setController_zeroAddress": "30070" } \ No newline at end of file diff --git a/snapshots/VaultEnterExitTest.json b/snapshots/VaultEnterExitTest.json index 7024d403..e2c63547 100644 --- a/snapshots/VaultEnterExitTest.json +++ b/snapshots/VaultEnterExitTest.json @@ -1,25 +1,25 @@ { "VaultEnterExitTest_test_calculateExitedAssets_invalidPosition": "18600", - "VaultEnterExitTest_test_claimExitedAssets": "730355", - "VaultEnterExitTest_test_claimExitedAssets_insufficientDelay": "41424", + "VaultEnterExitTest_test_claimExitedAssets": "752879", + "VaultEnterExitTest_test_claimExitedAssets_insufficientDelay": "43924", "VaultEnterExitTest_test_claimExitedAssets_invalidCheckpoint": "36507", "VaultEnterExitTest_test_deposit_exceedingCapacity": "47943", - "VaultEnterExitTest_test_deposit_success_basic": "80660", - "VaultEnterExitTest_test_deposit_success_differentReceiver": "80563", + "VaultEnterExitTest_test_deposit_success_basic": "83160", + "VaultEnterExitTest_test_deposit_success_differentReceiver": "83051", "VaultEnterExitTest_test_deposit_success_multipleDeposits": "57463", - "VaultEnterExitTest_test_deposit_success_receiveFunction": "77118", - "VaultEnterExitTest_test_deposit_success_withReferrer": "76785", + "VaultEnterExitTest_test_deposit_success_receiveFunction": "79618", + "VaultEnterExitTest_test_deposit_success_withReferrer": "76797", "VaultEnterExitTest_test_deposit_zeroAddress": "49918", - "VaultEnterExitTest_test_deposit_zeroAmount": "43483", - "VaultEnterExitTest_test_enterExitQueue_afterValidatorExit": "91855", - "VaultEnterExitTest_test_enterExitQueue_basicFlow": "91843", - "VaultEnterExitTest_test_enterExitQueue_directRedemption": "81906", - "VaultEnterExitTest_test_enterExitQueue_invalidParams_tooManyShares": "62322", - "VaultEnterExitTest_test_enterExitQueue_invalidParams_zeroAddress": "45832", - "VaultEnterExitTest_test_enterExitQueue_invalidParams_zeroShares": "31356", + "VaultEnterExitTest_test_deposit_zeroAmount": "43471", + "VaultEnterExitTest_test_enterExitQueue_afterValidatorExit": "94343", + "VaultEnterExitTest_test_enterExitQueue_basicFlow": "94331", + "VaultEnterExitTest_test_enterExitQueue_directRedemption": "84394", + "VaultEnterExitTest_test_enterExitQueue_invalidParams_tooManyShares": "64810", + "VaultEnterExitTest_test_enterExitQueue_invalidParams_zeroAddress": "48332", + "VaultEnterExitTest_test_enterExitQueue_invalidParams_zeroShares": "31344", "VaultEnterExitTest_test_enterExitQueue_multiUser_sender2": "74995", - "VaultEnterExitTest_test_enterExitQueue_multiUser_user1": "91843", - "VaultEnterExitTest_test_enterExitQueue_multipleUpdates": "89746", - "VaultEnterExitTest_test_enterExitQueue_partialExit": "96643", + "VaultEnterExitTest_test_enterExitQueue_multiUser_user1": "94331", + "VaultEnterExitTest_test_enterExitQueue_multipleUpdates": "92234", + "VaultEnterExitTest_test_enterExitQueue_partialExit": "99131", "VaultEnterExitTest_test_rescueAssets": "46295" } \ No newline at end of file diff --git a/snapshots/VaultEthStakingTest.json b/snapshots/VaultEthStakingTest.json index dc29fcf5..cf1c5e76 100644 --- a/snapshots/VaultEthStakingTest.json +++ b/snapshots/VaultEthStakingTest.json @@ -1,18 +1,18 @@ { - "VaultEthStakingTest_test_deposit": "82825", - "VaultEthStakingTest_test_depositAndMintOsToken": "200035", + "VaultEthStakingTest_test_deposit": "85325", + "VaultEthStakingTest_test_depositAndMintOsToken": "200047", "VaultEthStakingTest_test_fundValidators_invalid": "57140", "VaultEthStakingTest_test_fundValidators_valid": "140479", "VaultEthStakingTest_test_harvestAssets": "130845", "VaultEthStakingTest_test_invalidSecurityDeposit": "308363", - "VaultEthStakingTest_test_receive": "75018", + "VaultEthStakingTest_test_receive": "77518", "VaultEthStakingTest_test_receiveFromMevEscrow_fail": "36570", "VaultEthStakingTest_test_receiveFromMevEscrow_success": "37826", - "VaultEthStakingTest_test_registerValidators_01prefix": "243841", - "VaultEthStakingTest_test_registerValidators_02prefix": "500994", - "VaultEthStakingTest_test_transferVaultAssets": "51751", - "VaultEthStakingTest_test_updateStateAndDeposit": "164173", - "VaultEthStakingTest_test_updateStateAndDepositAndMintOsToken": "254268", - "VaultEthStakingTest_test_validatorMinMaxEffectiveBalance": "199916", + "VaultEthStakingTest_test_registerValidators_01prefix": "244809", + "VaultEthStakingTest_test_registerValidators_02prefix": "500922", + "VaultEthStakingTest_test_transferVaultAssets": "54251", + "VaultEthStakingTest_test_updateStateAndDeposit": "166673", + "VaultEthStakingTest_test_updateStateAndDepositAndMintOsToken": "254280", + "VaultEthStakingTest_test_validatorMinMaxEffectiveBalance": "199908", "VaultEthStakingTest_test_withdrawValidator_fullFlow": "74114" } \ No newline at end of file diff --git a/snapshots/VaultFeeTest.json b/snapshots/VaultFeeTest.json index 50c82e89..399199f7 100644 --- a/snapshots/VaultFeeTest.json +++ b/snapshots/VaultFeeTest.json @@ -1,12 +1,12 @@ { - "VaultFeeTest_test_feeCollection": "121931", + "VaultFeeTest_test_feeCollection": "121919", "VaultFeeTest_test_feePercent_changeAffectsFutureRewards": "87733", "VaultFeeTest_test_setFeePercent_aboveMaximum": "40380", - "VaultFeeTest_test_setFeePercent_initialZeroToOne": "42312", + "VaultFeeTest_test_setFeePercent_initialZeroToOne": "44812", "VaultFeeTest_test_setFeePercent_maxIncrease": "38472", "VaultFeeTest_test_setFeePercent_notAdmin": "34556", "VaultFeeTest_test_setFeePercent_requiresHarvest": "42501", - "VaultFeeTest_test_setFeePercent_success": "44575", + "VaultFeeTest_test_setFeePercent_success": "47075", "VaultFeeTest_test_setFeePercent_tooSoon": "38137", "VaultFeeTest_test_setFeeRecipient_notAdmin": "36854", "VaultFeeTest_test_setFeeRecipient_requiresHarvest": "44796", diff --git a/snapshots/VaultGnoStakingTest.json b/snapshots/VaultGnoStakingTest.json index 1fc421b2..5f5bc848 100644 --- a/snapshots/VaultGnoStakingTest.json +++ b/snapshots/VaultGnoStakingTest.json @@ -2,12 +2,12 @@ "VaultGnoStakingCoverageTest_test_processTotalAssetsDelta_smallXdaiBalance": "175401", "VaultGnoStakingCoverageTest_test_registerValidator_topUp_invalid": "57128", "VaultGnoStakingCoverageTest_test_registerValidator_topUp_valid": "182150", - "VaultGnoStakingTest_test_deposit": "98637", + "VaultGnoStakingTest_test_deposit": "101137", "VaultGnoStakingTest_test_processTotalAssetsDelta": "303158", - "VaultGnoStakingTest_test_pullWithdrawals": "90421", + "VaultGnoStakingTest_test_pullWithdrawals": "92921", "VaultGnoStakingTest_test_receive_xDai": "213139", - "VaultGnoStakingTest_test_transferVaultAssets": "87452", - "VaultGnoStakingTest_test_vaultGnoStaking_init": "678256", + "VaultGnoStakingTest_test_transferVaultAssets": "89952", + "VaultGnoStakingTest_test_vaultGnoStaking_init": "678313", "VaultGnoStakingTest_test_withdrawValidator_fullFlow": "74091", "test_registerValidators_succeeds_0x01": "286528", "test_registerValidators_succeeds_0x02": "630112" diff --git a/snapshots/VaultOsTokenTest.json b/snapshots/VaultOsTokenTest.json index 8fb25fa6..44d8888e 100644 --- a/snapshots/VaultOsTokenTest.json +++ b/snapshots/VaultOsTokenTest.json @@ -1,27 +1,27 @@ { - "VaultOsTokenTest_test_burnOsToken_afterFeeSync": "73489", - "VaultOsTokenTest_test_burnOsToken_allShares": "68187", - "VaultOsTokenTest_test_burnOsToken_basic": "72987", - "VaultOsTokenTest_test_burnOsToken_exceedingShares": "48701", + "VaultOsTokenTest_test_burnOsToken_afterFeeSync": "76001", + "VaultOsTokenTest_test_burnOsToken_allShares": "70699", + "VaultOsTokenTest_test_burnOsToken_basic": "75499", + "VaultOsTokenTest_test_burnOsToken_exceedingShares": "48725", "VaultOsTokenTest_test_burnOsToken_improvesLTV": "70999", "VaultOsTokenTest_test_burnOsToken_invalidPosition": "66210", - "VaultOsTokenTest_test_burnOsToken_multipleBurns": "72987", + "VaultOsTokenTest_test_burnOsToken_multipleBurns": "75499", "VaultOsTokenTest_test_burnOsToken_zeroShares": "37846", "VaultOsTokenTest_test_enterExitQueue_ltvViolation": "118315", "VaultOsTokenTest_test_liquidateOsToken_basic": "120719", "VaultOsTokenTest_test_liquidateOsToken_bonus": "125219", "VaultOsTokenTest_test_liquidateOsToken_invalidReceivedAssets": "75759", - "VaultOsTokenTest_test_liquidateOsToken_liquidationDisabled": "69199", - "VaultOsTokenTest_test_liquidateOsToken_partialLiquidation": "122719", - "VaultOsTokenTest_test_mintOsToken_basic": "162098", - "VaultOsTokenTest_test_mintOsToken_feeSync": "105554", + "VaultOsTokenTest_test_liquidateOsToken_liquidationDisabled": "69187", + "VaultOsTokenTest_test_liquidateOsToken_partialLiquidation": "125219", + "VaultOsTokenTest_test_mintOsToken_basic": "162110", + "VaultOsTokenTest_test_mintOsToken_feeSync": "108078", "VaultOsTokenTest_test_mintOsToken_ltvValidation": "144971", - "VaultOsTokenTest_test_mintOsToken_maxAmount": "159979", - "VaultOsTokenTest_test_mintOsToken_multipleReceivers": "119965", - "VaultOsTokenTest_test_mintOsToken_notCollateralized": "41953", + "VaultOsTokenTest_test_mintOsToken_maxAmount": "162479", + "VaultOsTokenTest_test_mintOsToken_multipleReceivers": "119989", + "VaultOsTokenTest_test_mintOsToken_notCollateralized": "41965", "VaultOsTokenTest_test_mintOsToken_notHarvested": "47615", "VaultOsTokenTest_test_mintOsToken_repeatedMinting": "105670", - "VaultOsTokenTest_test_mintOsToken_zeroAddressReceiver": "86812", + "VaultOsTokenTest_test_mintOsToken_zeroAddressReceiver": "86836", "VaultOsTokenTest_test_mintOsToken_zeroShares": "89092", "VaultOsTokenTest_test_redeemOsToken_afterFeeSync": "134490", "VaultOsTokenTest_test_redeemOsToken_afterStateUpdate_fail": "50939", @@ -29,20 +29,20 @@ "VaultOsTokenTest_test_redeemOsToken_basic": "133988", "VaultOsTokenTest_test_redeemOsToken_fullPosition": "133988", "VaultOsTokenTest_test_redeemOsToken_goodHealthFactor": "134071", - "VaultOsTokenTest_test_redeemOsToken_insufficientShares": "112652", - "VaultOsTokenTest_test_redeemOsToken_nonExistentPosition": "87008", + "VaultOsTokenTest_test_redeemOsToken_insufficientShares": "112664", + "VaultOsTokenTest_test_redeemOsToken_nonExistentPosition": "86996", "VaultOsTokenTest_test_redeemOsToken_onlyRedeemer": "36360", - "VaultOsTokenTest_test_redeemOsToken_zeroAddressReceiver": "40673", - "VaultOsTokenTest_test_redeemOsToken_zeroShares": "90254", + "VaultOsTokenTest_test_redeemOsToken_zeroAddressReceiver": "40685", + "VaultOsTokenTest_test_redeemOsToken_zeroShares": "90242", "VaultOsTokenTest_test_redeemVsLiquidate": "134071", "VaultOsTokenTest_test_test_liquidateOsToken_notHarvested": "43548", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_basic": "165145", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_basic": "167657", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_claim": "107765", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_maxAmount": "165157", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_moreThanOwned": "44795", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_maxAmount": "167657", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_moreThanOwned": "44819", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_noPosition": "43502", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_notHarvested": "41330", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_partialTransfer": "173208", - "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_process": "721172", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_notHarvested": "41342", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_partialTransfer": "173220", + "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_process": "736184", "VaultOsTokenTest_test_transferOsTokenPositionToEscrow_zeroShares": "47545" } \ No newline at end of file diff --git a/snapshots/VaultSubVaultsTest.json b/snapshots/VaultSubVaultsTest.json index 036ccd5b..47f4c6bb 100644 --- a/snapshots/VaultSubVaultsTest.json +++ b/snapshots/VaultSubVaultsTest.json @@ -1,20 +1,24 @@ { - "VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_success": "115287", - "VaultSubVaultsTest_test_addSubVault_success": "126277", - "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_ejectionConsumesShares": "414205", - "VaultSubVaultsTest_test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets": "116896", - "VaultSubVaultsTest_test_depositToSubVaults_maxVaults": "3874609", - "VaultSubVaultsTest_test_depositToSubVaults_multipleSubVaults": "313640", - "VaultSubVaultsTest_test_depositToSubVaults_singleSubVault": "161455", - "VaultSubVaultsTest_test_depositToSubVaults_withMetaVaultSubVault": "368578", - "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_emptySubVault": "51327", - "VaultSubVaultsTest_test_ejectSubVault_metaVaultAsSubVault_withShares": "200486", - "VaultSubVaultsTest_test_setSubVaultsCurator_success": "51083", - "VaultSubVaultsTest_test_updateState_enterExitQueueMaxVaults": "7013881", - "VaultSubVaultsTest_test_updateState_withMetaVaultSubVault_success": "160403", - "test_depositToSubVaults_ejectingSubVault": "151889", - "test_ejectSubVault_emptySubVault": "61797", - "test_ejectSubVault_subVaultWithShares": "202108", - "test_updateState_newTotalAssets": "208042", - "test_updateState_unprocessedSubVaultExit": "198058" + "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" } \ No newline at end of file diff --git a/snapshots/VaultTokenTest.json b/snapshots/VaultTokenTest.json index c1bebe88..e738ddbc 100644 --- a/snapshots/VaultTokenTest.json +++ b/snapshots/VaultTokenTest.json @@ -1,22 +1,22 @@ { - "VaultTokenTest_test_approve": "56346", + "VaultTokenTest_test_approve": "58846", "VaultTokenTest_test_approveZeroAddress": "33703", - "VaultTokenTest_test_depositEmitsTransferEvent": "80993", - "VaultTokenTest_test_enterExitQueueEmitsTransferEvent": "94566", + "VaultTokenTest_test_depositEmitsTransferEvent": "83493", + "VaultTokenTest_test_enterExitQueueEmitsTransferEvent": "97066", "VaultTokenTest_test_invalidTokenMetaNameTooLong": "478037", "VaultTokenTest_test_invalidTokenMetaSymbolTooLong": "311105", - "VaultTokenTest_test_permit": "84690", - "VaultTokenTest_test_permitExpiredDeadline": "32817", - "VaultTokenTest_test_permitInvalidSigner": "61070", - "VaultTokenTest_test_permitZeroAddressSpender": "32351", - "VaultTokenTest_test_transfer": "63941", - "VaultTokenTest_test_transferFrom": "65201", + "VaultTokenTest_test_permit": "87202", + "VaultTokenTest_test_permitExpiredDeadline": "35305", + "VaultTokenTest_test_permitInvalidSigner": "63558", + "VaultTokenTest_test_permitZeroAddressSpender": "34863", + "VaultTokenTest_test_transfer": "66441", + "VaultTokenTest_test_transferFrom": "67701", "VaultTokenTest_test_transferFromMoreThanAllowance": "32480", "VaultTokenTest_test_transferFromMoreThanBalance": "37928", "VaultTokenTest_test_transferFromZeroAddress": "38535", - "VaultTokenTest_test_transferMoreThanBalance": "35862", - "VaultTokenTest_test_transferToZeroAddress": "31234", - "VaultTokenTest_test_transferWithOsTokenPosition": "94084", - "VaultTokenTest_test_unlimitedAllowance": "66861", - "VaultTokenTest_test_updateExitQueueBurnsShares": "146979" + "VaultTokenTest_test_transferMoreThanBalance": "38362", + "VaultTokenTest_test_transferToZeroAddress": "33734", + "VaultTokenTest_test_transferWithOsTokenPosition": "96584", + "VaultTokenTest_test_unlimitedAllowance": "69361", + "VaultTokenTest_test_updateExitQueueBurnsShares": "146991" } \ No newline at end of file diff --git a/snapshots/VaultValidatorsTest.json b/snapshots/VaultValidatorsTest.json index de12b7d6..3db88da5 100644 --- a/snapshots/VaultValidatorsTest.json +++ b/snapshots/VaultValidatorsTest.json @@ -1,43 +1,43 @@ { "VaultValidatorsTest_test_consolidateValidators_byManager": "79212", "VaultValidatorsTest_test_consolidateValidators_feeHandling": "86213", - "VaultValidatorsTest_test_consolidateValidators_invalidSignature": "69620", + "VaultValidatorsTest_test_consolidateValidators_invalidSignature": "72120", "VaultValidatorsTest_test_consolidateValidators_invalidValidatorsEmpty": "55608", "VaultValidatorsTest_test_consolidateValidators_invalidValidatorsLength": "57330", - "VaultValidatorsTest_test_consolidateValidators_multipleValidators": "99418", + "VaultValidatorsTest_test_consolidateValidators_multipleValidators": "99394", "VaultValidatorsTest_test_consolidateValidators_notManager": "57250", - "VaultValidatorsTest_test_consolidateValidators_untrackedDestination": "76208", - "VaultValidatorsTest_test_consolidateValidators_withOracleSignatures": "113484", - "VaultValidatorsTest_test_consolidateValidators_withSignature": "109193", + "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": "61201", + "VaultValidatorsTest_test_fundValidators_invalidSignature": "63689", "VaultValidatorsTest_test_fundValidators_invalidValidators": "45735", - "VaultValidatorsTest_test_fundValidators_multipleValidators": "147775", - "VaultValidatorsTest_test_fundValidators_nonExistingValidator": "57128", + "VaultValidatorsTest_test_fundValidators_multipleValidators": "144799", + "VaultValidatorsTest_test_fundValidators_nonExistingValidator": "57140", "VaultValidatorsTest_test_fundValidators_notHarvested": "44394", "VaultValidatorsTest_test_fundValidators_notManager": "48806", "VaultValidatorsTest_test_fundValidators_v1Validators": "52712", - "VaultValidatorsTest_test_fundValidators_withSignature": "135199", - "VaultValidatorsTest_test_registerValidators_byManager": "266735", + "VaultValidatorsTest_test_fundValidators_withSignature": "137699", + "VaultValidatorsTest_test_registerValidators_byManager": "267691", "VaultValidatorsTest_test_registerValidators_insufficientAssets": "41930", - "VaultValidatorsTest_test_registerValidators_invalidSignature": "206444", - "VaultValidatorsTest_test_registerValidators_invalidValidatorLength": "194386", - "VaultValidatorsTest_test_registerValidators_invalidValidators": "188451", - "VaultValidatorsTest_test_registerValidators_multipleValidators": "303619", - "VaultValidatorsTest_test_registerValidators_nonceIncrement": "145899", - "VaultValidatorsTest_test_registerValidators_notHarvested": "170709", - "VaultValidatorsTest_test_registerValidators_notManager": "193605", - "VaultValidatorsTest_test_registerValidators_v1Validators": "241336", - "VaultValidatorsTest_test_registerValidators_v2Validators": "264235", - "VaultValidatorsTest_test_registerValidators_withSignature": "299199", - "VaultValidatorsTest_test_setValidatorsManager_valueNotChanged": "34429", + "VaultValidatorsTest_test_registerValidators_invalidSignature": "206436", + "VaultValidatorsTest_test_registerValidators_invalidValidatorLength": "194366", + "VaultValidatorsTest_test_registerValidators_invalidValidators": "188419", + "VaultValidatorsTest_test_registerValidators_multipleValidators": "304623", + "VaultValidatorsTest_test_registerValidators_nonceIncrement": "148369", + "VaultValidatorsTest_test_registerValidators_notHarvested": "170713", + "VaultValidatorsTest_test_registerValidators_notManager": "193585", + "VaultValidatorsTest_test_registerValidators_v1Validators": "244804", + "VaultValidatorsTest_test_registerValidators_v2Validators": "267691", + "VaultValidatorsTest_test_registerValidators_withSignature": "300155", + "VaultValidatorsTest_test_setValidatorsManager_valueNotChanged": "36929", "VaultValidatorsTest_test_withdrawValidators_byManager": "74102", "VaultValidatorsTest_test_withdrawValidators_feeHandling": "81103", - "VaultValidatorsTest_test_withdrawValidators_invalidSignature": "68361", + "VaultValidatorsTest_test_withdrawValidators_invalidSignature": "70861", "VaultValidatorsTest_test_withdrawValidators_invalidValidatorsEmpty": "55082", "VaultValidatorsTest_test_withdrawValidators_invalidValidatorsLength": "56493", "VaultValidatorsTest_test_withdrawValidators_multipleValidators": "89825", "VaultValidatorsTest_test_withdrawValidators_notAuthorized": "56024", - "VaultValidatorsTest_test_withdrawValidators_withSignature": "104039" + "VaultValidatorsTest_test_withdrawValidators_withSignature": "106539" } \ No newline at end of file diff --git a/snapshots/VaultVersionTest.json b/snapshots/VaultVersionTest.json index 9fcd9c3d..b0ac9537 100644 --- a/snapshots/VaultVersionTest.json +++ b/snapshots/VaultVersionTest.json @@ -1,12 +1,12 @@ { "VaultVersionTest_test_reinitializeFails": "30923", - "VaultVersionTest_test_upgradeMultipleSteps": "58533", - "VaultVersionTest_test_upgradeNonAdminFails": "38860", - "VaultVersionTest_test_upgradeToDifferentVaultIdFails": "42364", - "VaultVersionTest_test_upgradeToNextVersion": "83669", - "VaultVersionTest_test_upgradeToSameVersionFails": "34605", + "VaultVersionTest_test_upgradeMultipleSteps": "61033", + "VaultVersionTest_test_upgradeNonAdminFails": "38872", + "VaultVersionTest_test_upgradeToDifferentVaultIdFails": "42352", + "VaultVersionTest_test_upgradeToNextVersion": "83681", + "VaultVersionTest_test_upgradeToSameVersionFails": "37105", "VaultVersionTest_test_upgradeToSkipVersionFails": "43074", "VaultVersionTest_test_upgradeToUnapprovedImplementationFails": "46371", "VaultVersionTest_test_upgradeToZeroAddressFails": "36640", - "VaultVersionTest_test_upgradeWithInvalidCallDataFails": "60718" + "VaultVersionTest_test_upgradeWithInvalidCallDataFails": "60730" } \ No newline at end of file diff --git a/snapshots/VaultsRegistryTest.json b/snapshots/VaultsRegistryTest.json index 1e58d9a1..bcc1bfd3 100644 --- a/snapshots/VaultsRegistryTest.json +++ b/snapshots/VaultsRegistryTest.json @@ -8,13 +8,13 @@ "VaultsRegistryTest_test_addVaultImpl_notOwner": "32404", "VaultsRegistryTest_test_addVault_asOwner": "59391", "VaultsRegistryTest_test_addVault_notFactoryOrOwner": "33976", - "VaultsRegistryTest_test_initialize": "53876", + "VaultsRegistryTest_test_initialize": "56376", "VaultsRegistryTest_test_initialize_alreadyInitialized": "27356", - "VaultsRegistryTest_test_initialize_zeroAddress": "24983", - "VaultsRegistryTest_test_removeFactory": "32212", + "VaultsRegistryTest_test_initialize_zeroAddress": "27483", + "VaultsRegistryTest_test_removeFactory": "34712", "VaultsRegistryTest_test_removeFactory_alreadyRemoved": "34077", "VaultsRegistryTest_test_removeFactory_notOwner": "25794", - "VaultsRegistryTest_test_removeVaultImpl": "32163", + "VaultsRegistryTest_test_removeVaultImpl": "34663", "VaultsRegistryTest_test_removeVaultImpl_alreadyRemoved": "34044", "VaultsRegistryTest_test_removeVaultImpl_notOwner": "25750" } \ No newline at end of file diff --git a/test/BalancedCurator.t.sol b/test/BalancedCurator.t.sol index 41f0eb93..96f8f3b4 100644 --- a/test/BalancedCurator.t.sol +++ b/test/BalancedCurator.t.sol @@ -75,7 +75,7 @@ contract BalancedCuratorTest is Test { // 100 ETH to distribute across 5 vaults, but one is ejecting uint256 assetsToDeposit = 100 ether; address[] memory vaults = subVaults; - address ejecting = makeAddr("unknown"); + address ejecting = makeAddr("Unknown"); // Should revert with EjectingVaultNotFound error vm.expectRevert(Errors.EjectingVaultNotFound.selector); diff --git a/test/ConsolidationsChecker.t.sol b/test/ConsolidationsChecker.t.sol index 6568714d..c67f8a28 100644 --- a/test/ConsolidationsChecker.t.sol +++ b/test/ConsolidationsChecker.t.sol @@ -30,8 +30,8 @@ contract ConsolidationsCheckerTest is Test, EthHelpers { consolidationsChecker = ConsolidationsChecker(address(contracts.consolidationsChecker)); // Set up test accounts - admin = makeAddr("admin"); - vault = makeAddr("vault"); + admin = makeAddr("Admin"); + vault = makeAddr("Vault"); // Store initial min oracles value _validatorsMinOraclesBefore = contracts.keeper.validatorsMinOracles(); @@ -412,7 +412,7 @@ contract ConsolidationsCheckerTest is Test, EthHelpers { _generateValidSignatures(vault, validatorsData, contracts.keeper.validatorsMinOracles()); // Create a different vault address - address differentVault = makeAddr("differentVault"); + address differentVault = makeAddr("DifferentVault"); // Try to verify signatures with different vault _startSnapshotGas("ConsolidationsCheckerTest_test_verifySignatures_differentVault"); diff --git a/test/CuratorsRegistry.t.sol b/test/CuratorsRegistry.t.sol index 725eb6ee..a60f2033 100644 --- a/test/CuratorsRegistry.t.sol +++ b/test/CuratorsRegistry.t.sol @@ -14,10 +14,10 @@ contract CuratorsRegistryTest is Test { function setUp() public { // Create accounts - owner = makeAddr("owner"); - newOwner = makeAddr("newOwner"); - curator = makeAddr("curator"); - nonOwner = makeAddr("nonOwner"); + owner = makeAddr("Owner"); + newOwner = makeAddr("NewOwner"); + curator = makeAddr("Curator"); + nonOwner = makeAddr("NonOwner"); // Deploy registry with owner as the deployer vm.prank(owner); diff --git a/test/DepositDataRegistry.t.sol b/test/DepositDataRegistry.t.sol index 24250334..71f2fb1a 100644 --- a/test/DepositDataRegistry.t.sol +++ b/test/DepositDataRegistry.t.sol @@ -38,7 +38,7 @@ contract DepositDataRegistryTest is Test, EthHelpers { depositDataRegistry = IDepositDataRegistry(_depositDataRegistry); // Create a valid vault (version >= 2) - admin = makeAddr("admin"); + admin = makeAddr("Admin"); validVault = _getOrCreateVault( VaultType.EthVault, admin, @@ -59,13 +59,13 @@ contract DepositDataRegistryTest is Test, EthHelpers { IEthVault(validVault).getExitQueueData(); exitingAssets = totalExitingAssets + IEthVault(validVault).convertToAssets(queuedShares) + unclaimedAssets; - invalidVault = makeAddr("invalidVault"); - nonAdmin = makeAddr("nonAdmin"); - newDepositDataManager = makeAddr("newDepositDataManager"); + invalidVault = makeAddr("InvalidVault"); + nonAdmin = makeAddr("NonAdmin"); + newDepositDataManager = makeAddr("NewDepositDataManager"); // Create or mock a vault with version < 2 // For this test, we'll simulate a vault with version 1 - lowVersionVault = makeAddr("lowVersionVault"); + lowVersionVault = makeAddr("LowVersionVault"); vm.mockCall(lowVersionVault, abi.encodeWithSelector(IVaultVersion.version.selector), abi.encode(uint8(1))); // Mock that lowVersionVault is registered in the vaults registry diff --git a/test/EthBlocklistErc20Vault.t.sol b/test/EthBlocklistErc20Vault.t.sol index cdf3a01e..7d11d7e4 100644 --- a/test/EthBlocklistErc20Vault.t.sol +++ b/test/EthBlocklistErc20Vault.t.sol @@ -30,11 +30,11 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - blocklistManager = makeAddr("blocklistManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + blocklistManager = makeAddr("BlocklistManager"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); diff --git a/test/EthBlocklistVault.t.sol b/test/EthBlocklistVault.t.sol index 11c94b03..697d09f3 100644 --- a/test/EthBlocklistVault.t.sol +++ b/test/EthBlocklistVault.t.sol @@ -31,11 +31,11 @@ contract EthBlocklistVaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - blocklistManager = makeAddr("blocklistManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + blocklistManager = makeAddr("BlocklistManager"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); diff --git a/test/EthErc20Vault.t.sol b/test/EthErc20Vault.t.sol index b44d645f..837060f5 100644 --- a/test/EthErc20Vault.t.sol +++ b/test/EthErc20Vault.t.sol @@ -31,10 +31,10 @@ contract EthErc20VaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); @@ -316,8 +316,11 @@ contract EthErc20VaultTest is Test, EthHelpers { // Check results assertGt(mintedAssets, 0, "Should have minted some osToken assets"); assertEq(vault.osTokenPositions(sender), osTokenShares, "Should have minted expected osToken shares"); - assertEq( - vault.balanceOf(sender), vault.convertToShares(depositAmount), "Should have received tokens for the deposit" + assertApproxEqAbs( + vault.balanceOf(sender), + vault.convertToShares(depositAmount), + 1, + "Should have received tokens for the deposit" ); } @@ -342,14 +345,17 @@ contract EthErc20VaultTest is Test, EthHelpers { // Check results assertGt(mintedAssets, 0, "Should have minted some osToken assets"); assertEq(vault.osTokenPositions(sender), osTokenShares, "Should have minted expected osToken shares"); - assertEq( - vault.balanceOf(sender), vault.convertToShares(depositAmount), "Should have received tokens for the deposit" + assertApproxEqAbs( + vault.balanceOf(sender), + vault.convertToShares(depositAmount), + 1, + "Should have received tokens for the deposit" ); } function test_withdrawValidator_validatorsManager() public { // 1. Set validators manager - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); @@ -369,12 +375,12 @@ contract EthErc20VaultTest is Test, EthHelpers { } function test_withdrawValidator_unknown() public { - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); // 1. Set unknown address - address unknown = makeAddr("unknown"); + address unknown = makeAddr("Unknown"); uint256 withdrawFee = 0.1 ether; vm.deal(unknown, withdrawFee); @@ -410,7 +416,7 @@ contract EthErc20VaultTest is Test, EthHelpers { require(success, "ETH transfer failed"); // Verify sender received the correct number of tokens - assertEq(vault.balanceOf(sender), shares, "Sender should have received tokens"); + assertApproxEqAbs(vault.balanceOf(sender), shares, 1, "Sender should have received tokens"); } function test_updateExitQueue_emitsTransfer() public { diff --git a/test/EthFoxVault.t.sol b/test/EthFoxVault.t.sol index 32c900c6..e6f94c7d 100644 --- a/test/EthFoxVault.t.sol +++ b/test/EthFoxVault.t.sol @@ -28,11 +28,11 @@ contract EthFoxVaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - blocklistManager = makeAddr("blocklistManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + blocklistManager = makeAddr("BlocklistManager"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); @@ -254,7 +254,7 @@ contract EthFoxVaultTest is Test, EthHelpers { function test_withdrawValidator_validatorsManager() public { // 1. Set validators manager - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); @@ -275,7 +275,7 @@ contract EthFoxVaultTest is Test, EthHelpers { function test_withdrawValidator_unknown() public { // 1. Set unknown address - address unknown = makeAddr("unknown"); + address unknown = makeAddr("Unknown"); uint256 withdrawFee = 0.1 ether; vm.deal(unknown, withdrawFee); diff --git a/test/EthGenesisVault.t.sol b/test/EthGenesisVault.t.sol index f9f0ab5b..4c7dd730 100644 --- a/test/EthGenesisVault.t.sol +++ b/test/EthGenesisVault.t.sol @@ -30,8 +30,8 @@ contract EthGenesisVaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); // Fund accounts with ETH for testing vm.deal(admin, 100 ether); @@ -61,61 +61,6 @@ contract EthGenesisVaultTest is Test, EthHelpers { IEthGenesisVault(_vault).initialize(initParams); } - function test_upgradesCorrectly() public { - // Get or create a vault - address vaultAddr = _getForkVault(VaultType.EthGenesisVault); - EthGenesisVault existingVault = EthGenesisVault(payable(vaultAddr)); - - vm.deal( - vaultAddr, - IVaultStateV4(address(existingVault)).totalExitingAssets() - + existingVault.convertToAssets(IVaultStateV4(address(existingVault)).queuedShares()) + vaultAddr.balance - ); - _depositToVault(address(existingVault), 40 ether, user, user); - _registerEthValidator(address(existingVault), 32 ether, true); - - vm.prank(user); - existingVault.enterExitQueue(10 ether, user); - - // Record initial state - 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(); - uint256 queuedSharesBefore = IVaultStateV4(address(existingVault)).queuedShares(); - uint256 totalExitingAssetsBefore = IVaultStateV4(address(existingVault)).totalExitingAssets(); - - assertEq(existingVault.vaultId(), keccak256("EthGenesisVault")); - assertEq(existingVault.version(), 4); - - address newImpl = _getOrCreateVaultImpl(VaultType.EthGenesisVault); - vm.deal(adminBefore, admin.balance + 1 ether); - - _startSnapshotGas("EthGenesisVaultTest_test_upgradesCorrectly"); - vm.prank(adminBefore); - existingVault.upgradeToAndCall(newImpl, "0x"); - _stopSnapshotGas(); - - (uint128 queuedSharesAfter,,, uint128 totalExitingAssetsAfter,) = existingVault.getExitQueueData(); - assertEq(existingVault.vaultId(), keccak256("EthGenesisVault")); - assertEq(existingVault.version(), 5); - assertEq(existingVault.admin(), adminBefore); - assertEq(existingVault.capacity(), initialCapacity); - assertEq(existingVault.feePercent(), initialFeePercent); - assertEq(existingVault.feeRecipient(), feeRecipient); - assertEq(existingVault.validatorsManager(), validatorsManager); - assertEq(queuedSharesAfter, queuedSharesBefore); - assertEq(existingVault.totalShares(), initialTotalShares); - assertEq(existingVault.totalAssets(), initialTotalAssets); - assertEq(totalExitingAssetsAfter, totalExitingAssetsBefore); - assertEq(existingVault.validatorsManagerNonce(), 0); - assertEq(existingVault.getShares(user), senderBalanceBefore); - } - function test_cannotInitializeTwice() public { // Get or create a vault address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); diff --git a/test/EthMetaVault.t.sol b/test/EthMetaVault.t.sol index d722d3cd..772e5279 100644 --- a/test/EthMetaVault.t.sol +++ b/test/EthMetaVault.t.sol @@ -1,29 +1,54 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.22; -import {Test, stdStorage, StdStorage, console} from "forge-std/Test.sol"; +import {Test, console, Vm} from "forge-std/Test.sol"; 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 {IVaultEnterExit} from "../contracts/interfaces/IVaultEnterExit.sol"; +import {IVaultOsToken} from "../contracts/interfaces/IVaultOsToken.sol"; +import {ISubVaultsCurator} from "../contracts/interfaces/ISubVaultsCurator.sol"; import {Errors} from "../contracts/libraries/Errors.sol"; -import {EthMetaVault} from "../contracts/vaults/ethereum/custom/EthMetaVault.sol"; -import {EthMetaVaultFactory} from "../contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol"; +import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; +import {EthMetaVaultFactory} from "../contracts/vaults/ethereum/EthMetaVaultFactory.sol"; import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; import {CuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; import {EthHelpers} from "./helpers/EthHelpers.sol"; import {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; +import {EthOsTokenRedeemer} from "../contracts/tokens/EthOsTokenRedeemer.sol"; contract EthMetaVaultTest is Test, EthHelpers { - using stdStorage for StdStorage; + bytes32 private constant exitQueueEnteredTopic = keccak256("ExitQueueEntered(address,address,uint256,uint256)"); + address private constant FORK_META_VAULT = 0x34284C27A2304132aF751b0dEc5bBa2CF98eD039; + + struct ExitRequest { + address vault; + uint256 positionTicket; + uint64 timestamp; + } + + struct PreUpgradeState { + address admin; + address feeRecipient; + uint16 feePercent; + uint256 totalShares; + uint256 totalAssets; + uint256 capacity; + address curator; + uint128 rewardsNonce; + uint128 queuedShares; + uint128 unclaimedAssets; + uint256 totalTickets; + } ForkContracts public contracts; EthMetaVault public metaVault; address public admin; - address public curator; address public sender; address public receiver; address public referrer; @@ -31,38 +56,47 @@ contract EthMetaVaultTest is Test, EthHelpers { // Sub vaults address[] public subVaults; + // Pre-upgrade state for fork vault upgrade test + PreUpgradeState public preUpgradeState; + address[] public preUpgradeSubVaults; + mapping(address => IVaultSubVaults.SubVaultState) public preUpgradeSubVaultStates; + 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"); + 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); - // Create a curator - curator = address(new BalancedCurator()); - - // Register the curator in the registry - vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); - CuratorsRegistry(_curatorsRegistry).addCurator(curator); + // Capture pre-upgrade state if using fork vaults + if (vm.envBool("TEST_USE_FORK_VAULTS")) { + _capturePreUpgradeState(); + } // Deploy meta vault bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ - subVaultsCurator: curator, - capacity: 1000 ether, - feePercent: 1000, // 10% + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" }) ); metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + // Get existing sub vaults (if any) + address[] memory currentSubVaults = metaVault.getSubVaults(); + for (uint256 i = 0; i < currentSubVaults.length; i++) { + subVaults.push(currentSubVaults[i]); + } + // Deploy and add sub vaults for (uint256 i = 0; i < 3; i++) { address subVault = _createSubVault(admin); @@ -74,6 +108,28 @@ contract EthMetaVaultTest is Test, EthHelpers { } } + function _capturePreUpgradeState() internal { + EthMetaVault vault = EthMetaVault(payable(FORK_META_VAULT)); + require(vault.version() == 5, "Fork vault is not version 5"); + + 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.queuedShares, preUpgradeState.unclaimedAssets,,, preUpgradeState.totalTickets) = + vault.getExitQueueData(); + + address[] memory vaultSubVaults = vault.getSubVaults(); + for (uint256 i = 0; i < vaultSubVaults.length; i++) { + preUpgradeSubVaults.push(vaultSubVaults[i]); + preUpgradeSubVaultStates[vaultSubVaults[i]] = vault.subVaultsStates(vaultSubVaults[i]); + } + } + function _createSubVault(address _admin) internal returns (address) { bytes memory initParams = abi.encode( IEthVault.EthVaultInitParams({ @@ -86,42 +142,38 @@ contract EthMetaVaultTest is Test, EthHelpers { return _createVault(VaultType.EthVault, _admin, initParams, false); } - function test_deployWithZeroAdmin() public { - // Attempt to deploy a meta vault with zero admin - bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ - subVaultsCurator: curator, - capacity: 1000 ether, - feePercent: 1000, - metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" - }) - ); + 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); + } - EthMetaVaultFactory factory = _getOrCreateMetaFactory(VaultType.EthMetaVault); - vm.deal(address(this), address(this).balance + _securityDeposit); - vm.expectRevert(Errors.ZeroAddress.selector); - factory.createVault{value: _securityDeposit}(address(0), initParams); + // Update meta vault state + metaVault.updateState(_getEmptyHarvestParams()); } function test_deployment() public view { // Verify the vault was deployed correctly assertEq(metaVault.vaultId(), keccak256("EthMetaVault"), "Incorrect vault ID"); - assertEq(metaVault.version(), 5, "Incorrect version"); + assertEq(metaVault.version(), 6, "Incorrect version"); assertEq(metaVault.admin(), admin, "Incorrect admin"); - assertEq(metaVault.subVaultsCurator(), curator, "Incorrect curator"); - assertEq(metaVault.capacity(), 1000 ether, "Incorrect capacity"); - assertEq(metaVault.feePercent(), 1000, "Incorrect fee percent"); + assertEq(metaVault.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(); - assertEq(storedSubVaults.length, 3, "Incorrect number of sub vaults"); - for (uint256 i = 0; i < 3; i++) { + for (uint256 i = 0; i < subVaults.length; i++) { assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); } } function test_deposit() public { + uint256 totalAssetsBefore = metaVault.totalAssets(); + uint256 totalSharesBefore = metaVault.totalShares(); uint256 depositAmount = 10 ether; uint256 expectedShares = metaVault.convertToShares(depositAmount); @@ -132,11 +184,11 @@ contract EthMetaVaultTest is Test, EthHelpers { // Verify shares were minted to the receiver assertApproxEqAbs(shares, expectedShares, 1, "Incorrect shares minted"); - assertEq(metaVault.getShares(receiver), expectedShares, "Receiver did not receive shares"); + assertApproxEqAbs(metaVault.getShares(receiver), expectedShares, 1, "Receiver did not receive shares"); // Verify total assets and shares - assertApproxEqAbs(metaVault.totalAssets(), depositAmount + _securityDeposit, 1, "Incorrect total assets"); - assertApproxEqAbs(metaVault.totalShares(), expectedShares + _securityDeposit, 1, "Incorrect total shares"); + assertApproxEqAbs(metaVault.totalAssets(), totalAssetsBefore + depositAmount, 1, "Incorrect total assets"); + assertApproxEqAbs(metaVault.totalShares(), totalSharesBefore + expectedShares, 1, "Incorrect total shares"); } function test_depositViaFallback() public { @@ -185,13 +237,14 @@ contract EthMetaVaultTest is Test, EthHelpers { // Verify state was updated uint256 expectedShares = metaVault.convertToShares(depositAmount); - assertEq(shares, expectedShares, "Incorrect number of shares returned"); + assertApproxEqAbs(shares, expectedShares, 1, "Incorrect number of shares returned"); // Verify deposit was processed uint256 receiverSharesAfter = metaVault.getShares(receiver); - assertEq( + assertApproxEqAbs( receiverSharesAfter, receiverSharesBefore + expectedShares, + 1, "Receiver did not receive correct number of shares" ); @@ -199,7 +252,7 @@ contract EthMetaVaultTest is Test, EthHelpers { uint256 totalAssetsAfter = metaVault.totalAssets(); uint256 totalSharesAfter = metaVault.totalShares(); assertEq(totalAssetsAfter, totalAssetsBefore + depositAmount, "Total assets not updated correctly"); - assertEq(totalSharesAfter, totalSharesBefore + expectedShares, "Total shares not updated correctly"); + assertApproxEqAbs(totalSharesAfter, totalSharesBefore + expectedShares, 1, "Total shares not updated correctly"); // Verify withdrawable assets assertEq(metaVault.withdrawableAssets(), depositAmount, "Withdrawable assets incorrect after deposit"); @@ -289,16 +342,16 @@ contract EthMetaVaultTest is Test, EthHelpers { // Test with empty sub vaults // Create a new meta vault without sub vaults - bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ - subVaultsCurator: curator, + bytes memory emptyInitParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, capacity: 1000 ether, feePercent: 1000, metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" }) ); EthMetaVault emptyMetaVault = - EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, emptyInitParams, false))); // Verify empty vault behavior assertFalse(emptyMetaVault.isStateUpdateRequired(), "Empty vault should not require state update"); @@ -340,13 +393,21 @@ contract EthMetaVaultTest is Test, EthHelpers { _setVaultRewardsNonce(subVaults[i], newNonce); } - // Update state to process the exit queue + // Record events to capture ExitQueueEntered from sub vaults + vm.recordLogs(); + + // Update state to process the exit queue (this creates exit entries in sub vaults) metaVault.updateState(_getEmptyHarvestParams()); + // Extract exit positions from recorded logs + ExitRequest[] memory extractedExits = + _extractExitPositions(subVaults, vm.getRecordedLogs(), uint64(vm.getBlockTimestamp())); + // Process exits in sub vaults for (uint256 i = 0; i < subVaults.length; i++) { // Ensure sub vaults have enough ETH to process exits - vm.deal(subVaults[i], 5 ether); + // Add to existing balance to avoid underflow with forked vault's unclaimedAssets + vm.deal(subVaults[i], address(subVaults[i]).balance + 5 ether); IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(subVaults[i], 0, 0); IVaultState(subVaults[i]).updateState(harvestParams); @@ -354,12 +415,14 @@ contract EthMetaVaultTest is Test, EthHelpers { // Prepare exit requests for claiming from sub vaults to meta vault IVaultSubVaults.SubVaultExitRequest[] memory exitRequests = - new IVaultSubVaults.SubVaultExitRequest[](subVaults.length); - for (uint256 i = 0; i < subVaults.length; i++) { + new IVaultSubVaults.SubVaultExitRequest[](extractedExits.length); + for (uint256 i = 0; i < extractedExits.length; i++) { exitRequests[i] = IVaultSubVaults.SubVaultExitRequest({ - vault: subVaults[i], - exitQueueIndex: uint256(IVaultEnterExit(subVaults[i]).getExitQueueIndex(0)), - timestamp: exitTimestamp + vault: extractedExits[i].vault, + exitQueueIndex: uint256( + IVaultEnterExit(extractedExits[i].vault).getExitQueueIndex(extractedExits[i].positionTicket) + ), + timestamp: extractedExits[i].timestamp }); } @@ -398,7 +461,7 @@ contract EthMetaVaultTest is Test, EthHelpers { // Verify sender received their ETH (approximately the original deposit minus fees) // The exact amount might be slightly less due to fees and rounding uint256 amountReceived = senderBalanceAfter - senderBalanceBefore; - assertEq(amountReceived, depositAmount, "User received significantly less than expected"); + assertApproxEqAbs(amountReceived, depositAmount, 3, "User received significantly less than expected"); // Verify exit queue data updated (, uint128 unclaimedAssets,,,) = metaVault.getExitQueueData(); @@ -417,13 +480,14 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.deposit{value: depositAmount}(sender, referrer); metaVault.depositToSubVaults(); + _updateMetaVaultState(); + uint256 donationAmount = 1 ether; // Get vault state before donation uint256 vaultBalanceBefore = address(metaVault).balance; uint256 totalAssetsBefore = metaVault.totalAssets(); - // Approve GNO token for donation vm.startPrank(sender); // Check event emission @@ -439,13 +503,7 @@ contract EthMetaVaultTest is Test, EthHelpers { assertEq(metaVault.totalAssets(), totalAssetsBefore, "Meta vault total assets didn't increase"); // Process donation by updating state - uint64 newNonce = contracts.keeper.rewardsNonce() + 1; - _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); - } - - metaVault.updateState(_getEmptyHarvestParams()); + _updateMetaVaultState(); assertEq(address(metaVault).balance, vaultBalanceBefore + donationAmount, "Meta vault ETH balance increased"); assertEq(metaVault.totalAssets(), totalAssetsBefore + donationAmount, "Meta vault total assets increased"); @@ -464,21 +522,1269 @@ contract EthMetaVaultTest is Test, EthHelpers { metaVault.donateAssets{value: 0}(); } - function _getEmptyHarvestParams() internal pure returns (IKeeperRewards.HarvestParams memory) { - bytes32[] memory emptyProof; - return - IKeeperRewards.HarvestParams({rewardsRoot: bytes32(0), proof: emptyProof, reward: 0, unlockedMevReward: 0}); + function test_forkMetaVaultUpgrade_preservesState() public view { + // Skip if not using fork vaults or no pre-upgrade state was captured + if (!vm.envBool("TEST_USE_FORK_VAULTS")) { + return; + } + + EthMetaVault vault = EthMetaVault(payable(FORK_META_VAULT)); + + // Verify version was upgraded + assertEq(vault.version(), 6, "Vault should be version 6 after upgrade"); + assertEq(vault.vaultId(), keccak256("EthMetaVault"), "Vault ID should be preserved"); + + // Note: admin and feeRecipient are intentionally changed by _getOrCreateVault for testing + // Verify fee percent is preserved (admin/feeRecipient changes are expected) + assertEq(vault.feePercent(), preUpgradeState.feePercent, "Fee percent should be preserved"); + + // Verify vault state preserved + 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"); + + // Verify sub vaults state preserved (original sub vaults should still be present) + address[] memory postSubVaults = vault.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]]; + assertEq(postState.stakedShares, preState.stakedShares, "Staked shares should be preserved"); + assertEq(postState.queuedShares, preState.queuedShares, "Queued shares should be preserved"); + } + + // Verify exit queue data preserved + (uint128 postQueuedShares, uint128 postUnclaimedAssets,,, uint256 postTotalTickets) = vault.getExitQueueData(); + assertEq(postQueuedShares, preUpgradeState.queuedShares, "Queued shares should be preserved"); + assertEq(postUnclaimedAssets, preUpgradeState.unclaimedAssets, "Unclaimed assets should be preserved"); + assertEq(postTotalTickets, preUpgradeState.totalTickets, "Total tickets should be preserved"); + } + + function test_calculateSubVaultsRedemptions_notHarvested() public { + // First deposit to meta vault and sub vaults to establish initial state + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.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"); + + // Try to call calculateSubVaultsRedemptions - should revert with NotHarvested + vm.expectRevert(Errors.NotHarvested.selector); + metaVault.calculateSubVaultsRedemptions(1 ether); + } + + function test_calculateSubVaultsRedemptions_withMetaSubVault() public { + // First, update main meta vault state to establish baseline nonce + _updateMetaVaultState(); + + // Get the current nonce that main meta vault is at + uint128 currentNonce = metaVault.subVaultsRewardsNonce(); + + // Create another meta vault to use as a sub vault + bytes memory metaInitParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault subMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, metaInitParams, false))); + + // Add a sub vault to the sub meta vault and collateralize it + address nestedSubVault = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), nestedSubVault); + + // Set the nested sub vault's nonce to match what the sub meta vault expects + _setVaultRewardsNonce(nestedSubVault, uint64(currentNonce)); + + vm.prank(admin); + subMetaVault.addSubVault(nestedSubVault); + + // Deposit to sub meta vault + vm.deal(admin, 50 ether); + vm.prank(admin); + subMetaVault.deposit{value: 20 ether}(admin, address(0)); + + // Update sub meta vault state to sync nonces - first increment the keeper + uint64 newNonce = uint64(currentNonce) + 1; + _setKeeperRewardsNonce(newNonce); + _setVaultRewardsNonce(nestedSubVault, newNonce); + + // Update all existing sub vaults of main meta vault too + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], newNonce); + } + + // Update both meta vaults + subMetaVault.updateState(_getEmptyHarvestParams()); + metaVault.updateState(_getEmptyHarvestParams()); + + // Deposit to sub vaults + subMetaVault.depositToSubVaults(); + + // Propose adding meta vault as sub vault (requires approval) + vm.prank(admin); + metaVault.addSubVault(address(subMetaVault)); + + // Accept the meta sub vault (requires VaultsRegistry owner) + address registryOwner = contracts.vaultsRegistry.owner(); + vm.prank(registryOwner); + metaVault.acceptMetaSubVault(address(subMetaVault)); + + // Deposit to main meta vault + vm.prank(sender); + metaVault.deposit{value: 10 ether}(sender, referrer); + + // Update all vaults for next nonce + newNonce = newNonce + 1; + _setKeeperRewardsNonce(newNonce); + _setVaultRewardsNonce(nestedSubVault, newNonce); + for (uint256 i = 0; i < subVaults.length; i++) { + _setVaultRewardsNonce(subVaults[i], newNonce); + } + subMetaVault.updateState(_getEmptyHarvestParams()); + metaVault.updateState(_getEmptyHarvestParams()); + + // Deposit main meta vault assets to sub vaults to make withdrawable assets 0 + metaVault.depositToSubVaults(); + + // Set meta vault balance to cover unclaimed assets only (withdrawable = 0) + (, uint256 unclaimedAssets,,,) = metaVault.getExitQueueData(); + vm.deal(address(metaVault), unclaimedAssets); + + // Set sub meta vault balance to cover its unclaimed assets only + (, uint256 subMetaUnclaimedAssets,,,) = subMetaVault.getExitQueueData(); + vm.deal(address(subMetaVault), subMetaUnclaimedAssets); + + // Set all regular sub vaults to have 0 withdrawable assets + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 subVaultUnclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], subVaultUnclaimed); + } + + // Set nested sub vault to have 0 withdrawable assets + (, uint256 nestedUnclaimed,,,) = IVaultState(nestedSubVault).getExitQueueData(); + vm.deal(nestedSubVault, nestedUnclaimed); + + // Verify withdrawable assets are 0 + assertEq(metaVault.withdrawableAssets(), 0, "Meta vault withdrawable should be 0"); + assertEq(subMetaVault.withdrawableAssets(), 0, "Sub meta vault withdrawable should be 0"); + + // Test calculateSubVaultsRedemptions with meta sub vault present + uint256 assetsToRedeem = 5 ether; + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + + // Should return exit requests since withdrawable assets are 0 + assertGt(requests.length, 0, "Should return exit requests when no withdrawable assets"); + + // Calculate total requested assets + uint256 totalRequestedAssets; + for (uint256 i = 0; i < requests.length; i++) { + totalRequestedAssets += requests[i].assets; + + // 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); + + // Sub meta vault should also return exit requests from its nested sub vault + assertGt(subMetaRequests.length, 0, "Sub meta vault should return exit requests"); + + // Verify the nested sub vault is included in sub meta vault's requests + bool hasNestedSubVault = false; + for (uint256 j = 0; j < subMetaRequests.length; j++) { + if (subMetaRequests[j].vault == nestedSubVault) { + hasNestedSubVault = true; + break; + } + } + assertTrue(hasNestedSubVault, "Sub meta vault should request from nested sub vault"); + } + } + + // Total requested should cover the assets to redeem + assertGe(totalRequestedAssets, assetsToRedeem, "Total requested should cover redemption amount"); + } + + function test_calculateSubVaultsRedemptions_withEjectingSubVault() public { + // First deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Start ejecting one of the sub vaults + address vaultToEject = subVaults[0]; + vm.prank(admin); + metaVault.ejectSubVault(vaultToEject); + + // Verify ejecting sub vault is set + assertEq(metaVault.ejectingSubVault(), vaultToEject, "Ejecting sub vault should be set"); + + // Update meta vault state after ejection + _updateMetaVaultState(); + + // Get withdrawable assets (should be 0 since all deposited to sub vaults) + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + + // Get ejecting sub vault assets - these are counted as available in calculateSubVaultsRedemptions + IVaultSubVaults.SubVaultState memory ejectingState = metaVault.subVaultsStates(vaultToEject); + uint256 ejectingAssets = 0; + if (ejectingState.queuedShares > 0) { + ejectingAssets = IVaultState(vaultToEject).convertToAssets(ejectingState.queuedShares); + } + + // 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); + + // Should return exit requests since we're requesting more than available + assertGt(requests.length, 0, "Should return exit requests"); + + // Calculate total assets from redemption requests + uint256 totalRequestedAssets; + for (uint256 i = 0; i < requests.length; i++) { + totalRequestedAssets += requests[i].assets; + } + + // Check that total requests + ejecting assets + withdrawable assets cover assets to redeem + uint256 totalAvailable = totalRequestedAssets + ejectingAssets + withdrawableAssets; + assertGe(totalAvailable, assetsToRedeem, "Total available should cover assets to redeem"); + + // Verify ejecting sub vault has 0 assets in redemption requests + // (ejecting sub vault is included by the curator but with 0 assets since its shares are in exit queue) + bool ejectingVaultFound = false; + for (uint256 i = 0; i < requests.length; i++) { + if (requests[i].vault == vaultToEject) { + ejectingVaultFound = true; + assertEq(requests[i].assets, 0, "Ejecting sub vault should have 0 assets in redemption requests"); + break; + } + } + assertTrue(ejectingVaultFound, "Ejecting sub vault should be in redemption requests"); + } + + function test_calculateSubVaultsRedemptions_insufficientWithdrawableAssets() public { + // First deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Get withdrawable assets (should be 0 since all deposited to sub vaults) + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + assertEq(withdrawableAssets, 0, "Withdrawable assets should be 0 after deposit to sub vaults"); + + // Request more assets than withdrawable + uint256 assetsToRedeem = 10 ether; + _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_insufficientWithdrawableAssets"); + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + _stopSnapshotGas(); + + // Should return exit requests from sub vaults + assertGt(requests.length, 0, "Should return exit requests when withdrawable assets insufficient"); + + // Calculate total requested assets + uint256 totalRequestedAssets; + for (uint256 i = 0; i < requests.length; i++) { + totalRequestedAssets += requests[i].assets; + } + + // Total requested should cover the assets to redeem + assertGe(totalRequestedAssets, assetsToRedeem, "Total requested should cover redemption"); + } + + function test_calculateSubVaultsRedemptions_exactWithdrawableAssets() public { + // Deposit to meta vault but don't deposit to sub vaults + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Update meta vault state (needed for harvested check) + _updateMetaVaultState(); + + // Get withdrawable assets (may include small amounts from fork state) + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + assertGe(withdrawableAssets, depositAmount, "Withdrawable assets should be at least the deposit amount"); + + // Request exactly the withdrawable assets + _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_exactWithdrawableAssets"); + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(withdrawableAssets); + _stopSnapshotGas(); + + // Should return empty array since withdrawable assets exactly match + assertEq(requests.length, 0, "Should return empty requests when withdrawable equals redemption amount"); + } + + function test_calculateSubVaultsRedemptions_success() public { + // Deposit to meta vault and sub vaults + uint256 depositAmount = 50 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Get withdrawable assets + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + + // Request redemption exceeding withdrawable assets + uint256 assetsToRedeem = withdrawableAssets + 20 ether; + + _startSnapshotGas("EthMetaVaultTest_test_calculateSubVaultsRedemptions_success"); + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + _stopSnapshotGas(); + + // Should return exit requests from sub vaults + assertGt(requests.length, 0, "Should return exit requests"); + + // Verify all requests are for valid sub vaults + for (uint256 i = 0; i < requests.length; i++) { + bool isValidSubVault = false; + for (uint256 j = 0; j < subVaults.length; j++) { + if (requests[i].vault == subVaults[j]) { + isValidSubVault = true; + break; + } + } + assertTrue(isValidSubVault, "Exit request should be for a valid sub vault"); + assertGt(requests[i].assets, 0, "Exit request assets should be greater than 0"); + } + + // Calculate total requested assets + uint256 totalRequestedAssets; + for (uint256 i = 0; i < requests.length; i++) { + totalRequestedAssets += requests[i].assets; + } + + // Total requested should cover the assets to redeem minus withdrawable + uint256 expectedMinAssets = assetsToRedeem - withdrawableAssets; + assertGe(totalRequestedAssets, expectedMinAssets, "Total requested should cover needed redemption"); + } + + function test_calculateSubVaultsRedemptions_zeroAssets() public { + // Deposit to meta vault + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Update meta vault state + _updateMetaVaultState(); + + // Request zero assets - should return empty array + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(0); + assertEq(requests.length, 0, "Should return empty requests for zero assets"); + } + + function test_calculateSubVaultsRedemptions_lessThanWithdrawable() public { + // Deposit to meta vault but don't deposit to sub vaults + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Update meta vault state + _updateMetaVaultState(); + + // Request less than withdrawable + uint256 assetsToRedeem = 5 ether; + ISubVaultsCurator.ExitRequest[] memory requests = metaVault.calculateSubVaultsRedemptions(assetsToRedeem); + + // Should return empty array since withdrawable covers it + assertEq(requests.length, 0, "Should return empty requests when less than withdrawable"); + } + + function test_redeemSubVaultsAssets_accessControl() public { + // Only the redeemer can call redeemSubVaultsAssets + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.redeemSubVaultsAssets(1 ether); + + vm.prank(admin); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.redeemSubVaultsAssets(1 ether); + } + + function test_redeemSubVaultsAssets_zeroAssets() public { + // Get the redeemer address + address redeemer = contracts.osTokenConfig.redeemer(); + + // Try to redeem zero assets + vm.prank(redeemer); + vm.expectRevert(Errors.InvalidAssets.selector); + metaVault.redeemSubVaultsAssets(0); + } + + function test_redeemSubVaultsAssets_noRedeemRequests() public { + // Deposit to meta vault but don't deposit to sub vaults (keep assets withdrawable) + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Update meta vault state + _updateMetaVaultState(); + + // Verify withdrawable assets cover the redemption amount + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + assertGe(withdrawableAssets, depositAmount, "Withdrawable should cover deposit"); + + // Get the redeemer address + address redeemer = contracts.osTokenConfig.redeemer(); + + // Record meta vault balance before + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + // Call redeemSubVaultsAssets with amount less than withdrawable + // Should return 0 since no redeem requests needed (withdrawable covers it) + vm.prank(redeemer); + _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_noRedeemRequests"); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(depositAmount); + _stopSnapshotGas(); + + // Verify meta vault balance unchanged (no redemptions occurred) + assertEq(address(metaVault).balance, metaVaultBalanceBefore, "Meta vault balance should not change"); + + // Should return 0 since withdrawable assets cover the redemption + assertEq(totalRedeemed, 0, "Should return 0 when no redeem requests needed"); + } + + function test_redeemSubVaultsAssets_redeemAssetsExceedSubVaultsWithdrawableAssets() public { + // Deploy and set EthOsTokenRedeemer as the redeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Verify withdrawable assets are 0 (all deposited to sub vaults) + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + assertEq(withdrawableAssets, 0, "Withdrawable should be 0 after deposit to sub vaults"); + + // Add extra ETH to sub vaults to ensure they have withdrawable assets + // (unclaimed assets + extra balance = withdrawable > 0) + uint256 totalSubVaultWithdrawable = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + (depositAmount / subVaults.length / 2)); + totalSubVaultWithdrawable += IVaultState(subVaults[i]).withdrawableAssets(); + } + assertGt(totalSubVaultWithdrawable, 0, "Sub vaults should have withdrawable assets"); + assertGt(depositAmount, totalSubVaultWithdrawable, "Deposit should exceed sub vaults' withdrawable"); + + // Request to redeem more than what's withdrawable from sub vaults + // Using 2x withdrawable to test the "exceed withdrawable" scenario + // without hitting rounding issues with totalAssets() + uint256 assetsToRedeem = 2 * totalSubVaultWithdrawable; + + // Record meta vault balance before + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsExceedSubVaultsWithdrawableAssets"); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + _stopSnapshotGas(); + + // Verify redemption occurred - total redeemed should equal total sub vault withdrawable + assertLe(totalRedeemed, totalSubVaultWithdrawable, "Total redeemed should not exceed sub vaults' withdrawable"); + + // Verify meta vault received the redeemed assets + assertEq( + address(metaVault).balance, + metaVaultBalanceBefore + totalRedeemed, + "Meta vault should receive redeemed assets" + ); + } + + function test_redeemSubVaultsAssets_redeemAssetsLessThanSubVaultsWithdrawableAssets() public { + // Deploy and set EthOsTokenRedeemer as the redeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Give sub vaults significantly more withdrawable assets than we will request + uint256 assetsToRedeem = 5 ether; + uint256 subVaultExtraBalance = 15 ether; // Each sub vault gets 15 ETH extra + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + subVaultExtraBalance); + } + + // Calculate total sub vault withdrawable assets + uint256 totalSubVaultWithdrawable = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalSubVaultWithdrawable += IVaultState(subVaults[i]).withdrawableAssets(); + } + + // Verify sub vaults have significantly more withdrawable than we're requesting + assertGt(totalSubVaultWithdrawable, assetsToRedeem, "Sub vaults should have more withdrawable than requested"); + assertGt( + totalSubVaultWithdrawable, assetsToRedeem * 2, "Sub vaults should have at least 2x the requested amount" + ); + + // Record meta vault balance before + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_redeemAssetsLessThanSubVaultsWithdrawableAssets"); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + _stopSnapshotGas(); + + // Verify redemption occurred and matches requested amount (not more) + assertGt(totalRedeemed, 0, "Should redeem assets from sub vaults"); + assertApproxEqAbs(totalRedeemed, assetsToRedeem, 10, "Redeemed should match requested amount"); + assertLt(totalRedeemed, totalSubVaultWithdrawable, "Redeemed should be less than total withdrawable"); + + // Verify meta vault received the redeemed assets + assertEq( + address(metaVault).balance, + metaVaultBalanceBefore + totalRedeemed, + "Meta vault should receive redeemed assets" + ); + + // Verify sub vaults still have remaining withdrawable assets + uint256 totalSubVaultWithdrawableAfter = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalSubVaultWithdrawableAfter += IVaultState(subVaults[i]).withdrawableAssets(); + } + assertApproxEqAbs( + totalSubVaultWithdrawableAfter, + totalSubVaultWithdrawable - totalRedeemed, + 10, + "Sub vaults should have reduced withdrawable by redeemed amount" + ); + } + + function test_redeemSubVaultsAssets_success() public { + // Deploy and set EthOsTokenRedeemer as the redeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Verify withdrawable assets are 0 (all deposited to sub vaults) + uint256 withdrawableAssets = metaVault.withdrawableAssets(); + assertEq(withdrawableAssets, 0, "Withdrawable should be 0 after deposit to sub vaults"); + + // Give sub vaults sufficient withdrawable assets to cover redemption + uint256 assetsToRedeem = 10 ether; + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + // Give each sub vault more than enough to cover its portion + vm.deal(subVaults[i], unclaimed + (assetsToRedeem / subVaults.length) + 1 ether); + } + + // Calculate total sub vault withdrawable assets + uint256 totalSubVaultWithdrawable = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalSubVaultWithdrawable += IVaultState(subVaults[i]).withdrawableAssets(); + } + assertGe(totalSubVaultWithdrawable, assetsToRedeem, "Sub vaults should have sufficient withdrawable assets"); + + // Record meta vault balance before + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_success"); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + _stopSnapshotGas(); + + // Verify redemption occurred + assertGt(totalRedeemed, 0, "Should redeem assets from sub vaults"); + assertApproxEqAbs(totalRedeemed, assetsToRedeem, 10, "Redeemed amount should match requested"); + + // Verify meta vault received the redeemed assets + assertEq( + address(metaVault).balance, + metaVaultBalanceBefore + totalRedeemed, + "Meta vault should receive redeemed assets" + ); } - function _setVaultRewardsNonce(address vault, uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewards(address)").with_key(vault).depth( - 1 - ).checked_write(rewardsNonce); + function test_redeemSubVaultsAssets_noRoundingErrors() public { + // Deploy and set EthOsTokenRedeemer as the redeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit to meta vault and sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Update meta vault state + _updateMetaVaultState(); + + // Give sub vaults sufficient withdrawable assets + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 15 ether); + } + + // Test with an odd amount that could cause rounding issues + uint256 assetsToRedeem = 7.123456789012345678 ether; + + // Record balances before + uint256 metaVaultBalanceBefore = address(metaVault).balance; + uint256 metaVaultTotalAssetsBefore = metaVault.totalAssets(); + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + _startSnapshotGas("EthMetaVaultTest_test_redeemSubVaultsAssets_noRoundingErrors"); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + _stopSnapshotGas(); + + // Verify no rounding errors - redeemed amount should match requested exactly or be very close + // Allow up to 10 wei difference for rounding (share-to-asset conversions can cause small differences) + assertApproxEqAbs(totalRedeemed, assetsToRedeem, 10, "Redeemed should match requested with minimal rounding"); + + // Verify meta vault balance increased by exactly the redeemed amount + uint256 metaVaultBalanceAfter = address(metaVault).balance; + assertEq( + metaVaultBalanceAfter - metaVaultBalanceBefore, + totalRedeemed, + "Balance increase should exactly match redeemed amount" + ); + + // Verify total assets remain consistent (redeemed assets moved from sub vaults to meta vault) + uint256 metaVaultTotalAssetsAfter = metaVault.totalAssets(); + assertApproxEqAbs( + metaVaultTotalAssetsAfter, + metaVaultTotalAssetsBefore, + 10, + "Total assets should remain the same after redemption" + ); + + // 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]); + if (state.stakedShares > 0) { + totalSubVaultAssets += IVaultState(subVaults[i]).convertToAssets(state.stakedShares); + } + } + + // Total assets should equal sub vault assets plus withdrawable assets + uint256 expectedTotalAssets = totalSubVaultAssets + metaVault.withdrawableAssets(); + assertApproxEqAbs( + metaVaultTotalAssetsAfter, + expectedTotalAssets, + 10, + "Total assets should match sub vault assets plus withdrawable" + ); + } + + function test_redeemSubVaultsAssets_notHarvested() public { + // Setup: deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Increase keeper nonce by 2 to trigger NotHarvested + uint64 initialNonce = contracts.keeper.rewardsNonce(); + _setKeeperRewardsNonce(initialNonce + 2); + + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Verify state update is required + assertTrue(metaVault.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); + } + + function test_redeemSubVaultsAssets_emitsEvent() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Give sub vaults withdrawable assets + uint256 assetsToRedeem = 10 ether; + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + (assetsToRedeem / subVaults.length) + 1 ether); + } + + // Perform redemption and check event is emitted + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + vm.recordLogs(); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + + // Find and verify the SubVaultsAssetsRedeemed event + Vm.Log[] memory logs = vm.getRecordedLogs(); + bool eventFound = false; + bytes32 eventSig = keccak256("SubVaultsAssetsRedeemed(uint256)"); + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSig) { + uint256 emittedAmount = abi.decode(logs[i].data, (uint256)); + assertEq(emittedAmount, totalRedeemed, "Event amount should match redeemed amount"); + eventFound = true; + break; + } + } + assertTrue(eventFound, "SubVaultsAssetsRedeemed event should be emitted"); + } + + function test_redeemSubVaultsAssets_updatesSubVaultStates() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.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]); + stakedSharesBefore[i] = state.stakedShares; + } + + // Give sub vaults withdrawable assets + uint256 assetsToRedeem = 10 ether; + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + (assetsToRedeem / subVaults.length) + 1 ether); + } + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + metaVault.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]); + if (stateAfter.stakedShares < stakedSharesBefore[i]) { + anySharesReduced = true; + break; + } + } + assertTrue(anySharesReduced, "At least one sub vault should have reduced staked shares"); } - function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewardsNonce()").checked_write( - rewardsNonce + function test_redeemSubVaultsAssets_withEjectingSubVault() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.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); + + _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); + uint256 ejectingVaultAssets = IVaultState(vaultToEject).convertToAssets(ejectingState.queuedShares); + + // Give remaining sub vaults (all except the ejecting one) withdrawable assets + for (uint256 i = 0; i < subVaults.length - 1; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 10 ether); + } + + // Request more than what the ejecting vault has in queued shares + // This forces redemption from other sub vaults + uint256 assetsToRedeem = ejectingVaultAssets + 5 ether; + + // Record ejecting vault's state before + IVaultSubVaults.SubVaultState memory ejectingStateBefore = metaVault.subVaultsStates(vaultToEject); + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(assetsToRedeem); + + // Verify ejecting vault's state unchanged (ejecting shares are not modified by redemption) + IVaultSubVaults.SubVaultState memory ejectingStateAfter = metaVault.subVaultsStates(vaultToEject); + assertEq( + ejectingStateAfter.stakedShares, + ejectingStateBefore.stakedShares, + "Ejecting vault staked shares should not change" + ); + assertEq( + ejectingStateAfter.queuedShares, + ejectingStateBefore.queuedShares, + "Ejecting vault queued shares should not change" + ); + + // Verify assets were redeemed from other sub vaults (ejecting vault is skipped) + assertGt(totalRedeemed, 0, "Should redeem some assets from other sub vaults"); + assertApproxEqAbs( + totalRedeemed, assetsToRedeem, 100, "Should redeem the requested amount from other sub vaults" + ); + } + + function test_redeemSubVaultsAssets_allSubVaultsZeroWithdrawable() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Set all sub vaults to have exactly their unclaimed assets (0 withdrawable) + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed); + } + + // Verify all sub vaults have 0 withdrawable + for (uint256 i = 0; i < subVaults.length; i++) { + assertEq(IVaultState(subVaults[i]).withdrawableAssets(), 0, "Sub vault should have 0 withdrawable"); + } + + // 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); + + assertEq(totalRedeemed, 0, "Should redeem 0 when all sub vaults have 0 withdrawable"); + } + + function test_redeemSubVaultsAssets_multipleRedemptions() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Give sub vaults plenty of withdrawable assets + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 20 ether); + } + + address redeemer = contracts.osTokenConfig.redeemer(); + + // Track total redeemed across all calls + uint256 totalRedeemed = 0; + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + // Perform first redemption + vm.prank(redeemer); + uint256 redeemed1 = metaVault.redeemSubVaultsAssets(3 ether); + totalRedeemed += redeemed1; + + // Note: After first redemption, meta vault now has withdrawable assets (redeemed1) + // Subsequent redemptions will first use meta vault's withdrawable before going to sub vaults + // So we need to request more than what's now withdrawable + + // Second redemption - request more than current withdrawable + uint256 metaVaultWithdrawable = metaVault.withdrawableAssets(); + vm.prank(redeemer); + uint256 redeemed2 = metaVault.redeemSubVaultsAssets(metaVaultWithdrawable + 4 ether); + totalRedeemed += redeemed2; + + // Third redemption + metaVaultWithdrawable = metaVault.withdrawableAssets(); + vm.prank(redeemer); + uint256 redeemed3 = metaVault.redeemSubVaultsAssets(metaVaultWithdrawable + 2 ether); + totalRedeemed += redeemed3; + + // Verify total redeemed is sum of all redemptions + assertApproxEqAbs(totalRedeemed, redeemed1 + redeemed2 + redeemed3, 1, "Total should equal sum of redemptions"); + + // Verify meta vault balance increased by total redeemed + assertEq( + address(metaVault).balance, + metaVaultBalanceBefore + totalRedeemed, + "Meta vault balance should increase by total redeemed" + ); + + // Verify we actually redeemed significant assets + assertGt(totalRedeemed, 5 ether, "Should have redeemed significant assets across all calls"); + } + + function test_redeemSubVaultsAssets_verySmallAmount() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Give sub vaults withdrawable assets + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 1 ether); + } + + // Try to redeem a small amount + address redeemer = contracts.osTokenConfig.redeemer(); + uint256 smallAmount = 10; + vm.prank(redeemer); + uint256 totalRedeemed = metaVault.redeemSubVaultsAssets(smallAmount); + + // Verify redemption succeeded with minimal rounding + assertApproxEqAbs(totalRedeemed, smallAmount, 5, "Small redemption should work with minimal rounding"); + } + + function test_redeemSubVaultsAssets_partialWithdrawable() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit but don't push all to sub vaults - keep some withdrawable in meta vault + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Deposit only part to sub vaults by manipulating the balance + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Add some ETH back to meta vault to simulate partial withdrawable + vm.deal(address(metaVault), 5 ether); + + // Verify meta vault has some withdrawable (allow small rounding in fork mode) + uint256 metaVaultWithdrawable = metaVault.withdrawableAssets(); + assertApproxEqAbs(metaVaultWithdrawable, 5 ether, 100, "Meta vault should have ~5 ether withdrawable"); + + // Give sub vaults additional withdrawable + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 5 ether); + } + + // 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); + + // Should only redeem from sub vaults the amount exceeding meta vault's withdrawable (3 ether) + // Allow slightly larger delta for fork state rounding differences + assertApproxEqAbs(totalRedeemed, 3 ether, 100, "Should redeem ~3 ether from sub vaults"); + } + + function test_redeemSubVaultsAssets_singleSubVaultHasWithdrawable() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Record staked shares before for first sub vault + IVaultSubVaults.SubVaultState memory stateBefore = metaVault.subVaultsStates(subVaults[0]); + uint256 stakedAssetsBefore = IVaultState(subVaults[0]).convertToAssets(stateBefore.stakedShares); + + // Set all sub vaults to have 0 withdrawable except the first one + uint256 firstSubVaultWithdrawable = 10 ether; + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + if (i == 0) { + // First sub vault gets extra balance + vm.deal(subVaults[i], unclaimed + firstSubVaultWithdrawable); + } else { + // Others have exactly unclaimed (0 withdrawable) + vm.deal(subVaults[i], unclaimed); + } + } + + // Verify only first sub vault has withdrawable + uint256 actualFirstWithdrawable = IVaultState(subVaults[0]).withdrawableAssets(); + assertGt(actualFirstWithdrawable, 0, "First sub vault should have withdrawable"); + for (uint256 i = 1; i < subVaults.length; i++) { + assertEq(IVaultState(subVaults[i]).withdrawableAssets(), 0, "Other sub vaults should have 0 withdrawable"); + } + + // Perform redemption - the curator will distribute requests across vaults, + // but only the first vault can actually provide assets + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + uint256 totalRedeemed = metaVault.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 + // than the total, and only the first vault can provide assets + 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]); + uint256 stakedAssetsAfter = IVaultState(subVaults[0]).convertToAssets(stateAfter.stakedShares); + assertLt(stakedAssetsAfter, stakedAssetsBefore, "First sub vault staked assets should decrease"); + + // Verify the redeemed amount matches the decrease in staked assets + assertApproxEqAbs( + stakedAssetsBefore - stakedAssetsAfter, + totalRedeemed, + 10, + "Staked assets decrease should match redeemed amount" + ); + } + + function test_redeemSubVaultsAssets_osTokenPositionsClosed() public { + // Deploy and set EthOsTokenRedeemer + address redeemerOwner = makeAddr("RedeemerOwner"); + EthOsTokenRedeemer osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + redeemerOwner, + 12 hours + ); + vm.prank(Ownable(address(contracts.osTokenConfig)).owner()); + contracts.osTokenConfig.setRedeemer(address(osTokenRedeemer)); + + // Deposit and push to sub vaults + uint256 depositAmount = 30 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + _updateMetaVaultState(); + + // Give sub vaults withdrawable assets + for (uint256 i = 0; i < subVaults.length; i++) { + (, uint256 unclaimed,,,) = IVaultState(subVaults[i]).getExitQueueData(); + vm.deal(subVaults[i], unclaimed + 10 ether); + } + + // Verify meta vault has no osToken positions in sub vaults before + for (uint256 i = 0; i < subVaults.length; i++) { + assertEq( + IVaultOsToken(subVaults[i]).osTokenPositions(address(metaVault)), + 0, + "Meta vault should have no osToken position before" + ); + } + + // Perform redemption + address redeemer = contracts.osTokenConfig.redeemer(); + vm.prank(redeemer); + metaVault.redeemSubVaultsAssets(10 ether); + + // Verify meta vault still has no osToken positions in sub vaults after + for (uint256 i = 0; i < subVaults.length; i++) { + assertEq( + IVaultOsToken(subVaults[i]).osTokenPositions(address(metaVault)), + 0, + "Meta vault should have no osToken position after" + ); + } + } + + function _extractExitPositions(address[] memory _subVaults, Vm.Log[] memory logs, uint64 timestamp) + internal + view + returns (ExitRequest[] memory exitRequests) + { + uint256 subVaultsCount = _subVaults.length; + uint256 exitSubVaultsCount = metaVault.ejectingSubVault() != address(0) ? subVaultsCount - 1 : subVaultsCount; + exitRequests = new ExitRequest[](exitSubVaultsCount); + uint256 subVaultIndex = 0; + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].topics[0] != exitQueueEnteredTopic) { + continue; + } + (uint256 positionTicket,) = abi.decode(logs[i].data, (uint256, uint256)); + exitRequests[subVaultIndex] = + ExitRequest({vault: logs[i].emitter, positionTicket: positionTicket, timestamp: timestamp}); + subVaultIndex++; + } } } diff --git a/test/EthOsTokenRedeemer.t.sol b/test/EthOsTokenRedeemer.t.sol index dea49dcc..2922641a 100644 --- a/test/EthOsTokenRedeemer.t.sol +++ b/test/EthOsTokenRedeemer.t.sol @@ -1,22 +1,30 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.22; -import {Test} from "forge-std/Test.sol"; +import {Test, stdStorage, StdStorage} from "forge-std/Test.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 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 {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"; contract EthOsTokenRedeemerTest is Test, EthHelpers { + using stdStorage for StdStorage; + // Test contracts ForkContracts public contracts; EthOsTokenRedeemer public osTokenRedeemer; EthVault public vault; EthVault public vault2; + EthMetaVault public metaVault; + address[] public subVaults; // Test accounts address public owner; @@ -44,19 +52,24 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Setup test accounts - owner = makeAddr("owner"); - positionsManager = makeAddr("positionsManager"); - user1 = makeAddr("user1"); - user2 = makeAddr("user2"); - user3 = makeAddr("user3"); - admin = makeAddr("admin"); + owner = makeAddr("Owner"); + positionsManager = makeAddr("PositionsManager"); + user1 = makeAddr("User1"); + user2 = makeAddr("User2"); + user3 = makeAddr("User3"); + admin = makeAddr("Admin"); // Fund accounts _fundAccounts(); // Deploy OsTokenRedeemer - osTokenRedeemer = - new EthOsTokenRedeemer(_osToken, address(contracts.osTokenVaultController), owner, EXIT_QUEUE_UPDATE_DELAY); + osTokenRedeemer = new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + owner, + EXIT_QUEUE_UPDATE_DELAY + ); // Set up manager vm.prank(owner); @@ -116,12 +129,69 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { _depositToVault(address(vault2), DEPOSIT_AMOUNT, user3, user3); } - function _proposeAndAcceptPositions(IOsTokenRedeemer.RedeemablePositions memory positions) internal { - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(positions); + function _setupMetaVault() internal { + // Deploy meta vault + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + + // Get existing sub vaults (if any from fork) + address[] memory currentSubVaults = metaVault.getSubVaults(); + for (uint256 i = 0; i < currentSubVaults.length; i++) { + subVaults.push(currentSubVaults[i]); + } + + // Deploy and add sub vaults + for (uint256 i = 0; i < 2; i++) { + address subVault = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + subVaults.push(subVault); + + vm.prank(admin); + metaVault.addSubVault(subVault); + } + + // Deposit to meta vault to make it have assets + vm.deal(user1, 100 ether); + vm.prank(user1); + metaVault.deposit{value: 50 ether}(user1, address(0)); + + // Update meta vault state to distribute to sub vaults + _updateMetaVaultState(); + } + + function _createSubVault(address _admin) internal returns (address) { + bytes memory initParams = abi.encode( + IEthVault.EthVaultInitParams({ + capacity: 1000 ether, + feePercent: 5, // 5% + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + return _createVault(VaultType.EthVault, _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()); + } + + function _setRedeemablePositions(IOsTokenRedeemer.RedeemablePositions memory positions) internal { vm.prank(owner); - osTokenRedeemer.acceptRedeemablePositions(); + osTokenRedeemer.setRedeemablePositions(positions); } function _hashPair(bytes32 leaf1, bytes32 leaf2) internal pure returns (bytes32) { @@ -152,10 +222,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Create merkle tree for positions IOsTokenRedeemer.OsTokenPosition[] memory positions = new IOsTokenRedeemer.OsTokenPosition[](1); positions[0] = IOsTokenRedeemer.OsTokenPosition({ - vault: address(vault), - owner: user1, - leafShares: shares, - sharesToRedeem: shares + vault: address(vault), owner: user1, leafShares: shares, sharesToRedeem: shares }); // Create merkle root @@ -164,13 +231,14 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: merkleRoot, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); uint256 redeemedSharesBefore = osTokenRedeemer.redeemedShares(); uint256 redeemedAssetsBefore = osTokenRedeemer.redeemedAssets(); bytes32[] memory proof = new bytes32[](0); bool[] memory proofFlags = new bool[](0); + vm.prank(positionsManager); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); uint256 assets = contracts.osTokenVaultController.convertToAssets(shares); @@ -197,11 +265,17 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { uint256 invalidExitQueueDelay = uint256(type(uint64).max) + 1; vm.expectRevert(Errors.InvalidDelay.selector); - new EthOsTokenRedeemer(_osToken, address(contracts.osTokenVaultController), owner, invalidExitQueueDelay); + new EthOsTokenRedeemer( + address(contracts.vaultsRegistry), + _osToken, + address(contracts.osTokenVaultController), + owner, + invalidExitQueueDelay + ); } function test_setPositionsManager_notOwner() public { - address newManager = makeAddr("newManager"); + address newManager = makeAddr("NewManager"); vm.prank(user1); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1)); @@ -221,7 +295,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { } function test_setPositionsManager_success() public { - address newManager = makeAddr("newManager"); + address newManager = makeAddr("NewManager"); vm.expectEmit(true, false, false, true); emit IOsTokenRedeemer.PositionsManagerUpdated(newManager); @@ -234,181 +308,66 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { assertEq(osTokenRedeemer.positionsManager(), newManager); } - function test_proposeRedeemablePositions_notPositionsManager() public { + function test_setRedeemablePositions_notOwner() public { IOsTokenRedeemer.RedeemablePositions memory positions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - vm.prank(user1); - vm.expectRevert(Errors.AccessDenied.selector); - osTokenRedeemer.proposeRedeemablePositions(positions); - } - - function test_proposeRedeemablePositions_invalidPositions() public { - // Test with empty merkle root - IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: EMPTY_ROOT, ipfsHash: TEST_IPFS_HASH}); - vm.prank(positionsManager); - vm.expectRevert(Errors.InvalidRedeemablePositions.selector); - osTokenRedeemer.proposeRedeemablePositions(positions); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, positionsManager)); + osTokenRedeemer.setRedeemablePositions(positions); - // Test with empty IPFS hash - positions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: ""}); - - vm.prank(positionsManager); - vm.expectRevert(Errors.InvalidRedeemablePositions.selector); - osTokenRedeemer.proposeRedeemablePositions(positions); - } - - function test_proposeRedeemablePositions_hasPendingProposal() public { - IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(positions); - - // Try to propose again - vm.prank(positionsManager); - vm.expectRevert(Errors.RedeemablePositionsProposed.selector); - osTokenRedeemer.proposeRedeemablePositions(positions); + vm.prank(user1); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1)); + osTokenRedeemer.setRedeemablePositions(positions); } - function test_proposeRedeemablePositions_sameValue() public { + function test_setRedeemablePositions_sameValue() public { IOsTokenRedeemer.RedeemablePositions memory positions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - // First set a root - _proposeAndAcceptPositions(positions); + // First set + vm.prank(owner); + osTokenRedeemer.setRedeemablePositions(positions); - // Try to propose the same root again - vm.prank(positionsManager); + // Try setting same value again + vm.prank(owner); vm.expectRevert(Errors.ValueNotChanged.selector); - osTokenRedeemer.proposeRedeemablePositions(positions); + osTokenRedeemer.setRedeemablePositions(positions); } - function test_proposeRedeemablePositions_success() public { + function test_setRedeemablePositions_zeroRoot() public { + // Try to set redeemable positions with zero merkle root IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); + IOsTokenRedeemer.RedeemablePositions({merkleRoot: bytes32(0), ipfsHash: TEST_IPFS_HASH}); - vm.expectEmit(true, true, false, true); - emit IOsTokenRedeemer.RedeemablePositionsProposed(positions.merkleRoot, positions.ipfsHash); - - vm.prank(positionsManager); - _startSnapshotGas("EthOsTokenRedeemerTest_test_proposeRedeemablePositions_success"); - osTokenRedeemer.proposeRedeemablePositions(positions); - _stopSnapshotGas(); - - (bytes32 pendingRoot, string memory pendingIpfs) = osTokenRedeemer.pendingRedeemablePositions(); - assertEq(pendingRoot, positions.merkleRoot); - assertEq(pendingIpfs, positions.ipfsHash); - } - - function test_acceptRedeemablePositions_notOwner() public { - IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(positions); + vm.prank(owner); + vm.expectRevert(Errors.InvalidRedeemablePositions.selector); + osTokenRedeemer.setRedeemablePositions(positions); - vm.prank(user1); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1)); - osTokenRedeemer.acceptRedeemablePositions(); - } + // Try to set redeemable positions with empty ipfsHash + positions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: ""}); - function test_acceptRedeemablePositions_noPendingProposal() public { vm.prank(owner); vm.expectRevert(Errors.InvalidRedeemablePositions.selector); - osTokenRedeemer.acceptRedeemablePositions(); + osTokenRedeemer.setRedeemablePositions(positions); } - function test_acceptRedeemablePositions_success() public { + function test_setRedeemablePositions_success() public { IOsTokenRedeemer.RedeemablePositions memory positions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(positions); - vm.expectEmit(true, true, false, true); - emit IOsTokenRedeemer.RedeemablePositionsAccepted(positions.merkleRoot, positions.ipfsHash); + emit IOsTokenRedeemer.RedeemablePositionsUpdated(positions.merkleRoot, positions.ipfsHash); vm.prank(owner); - _startSnapshotGas("EthOsTokenRedeemerTest_test_acceptRedeemablePositions_success"); - osTokenRedeemer.acceptRedeemablePositions(); + _startSnapshotGas("EthOsTokenRedeemerTest_test_setRedeemablePositions_success"); + osTokenRedeemer.setRedeemablePositions(positions); _stopSnapshotGas(); // Verify positions were accepted (bytes32 currentRoot, string memory currentIpfs) = osTokenRedeemer.redeemablePositions(); assertEq(currentRoot, positions.merkleRoot); assertEq(currentIpfs, positions.ipfsHash); - - // Verify pending positions were cleared - (bytes32 pendingRoot, string memory pendingIpfs) = osTokenRedeemer.pendingRedeemablePositions(); - assertEq(pendingRoot, EMPTY_ROOT); - assertEq(pendingIpfs, ""); - } - - function test_denyRedeemablePositions_notOwner() public { - vm.prank(user1); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1)); - osTokenRedeemer.denyRedeemablePositions(); - } - - function test_denyRedeemablePositions_noPendingProposal() public { - // Should not revert even if no pending positions - vm.prank(owner); - osTokenRedeemer.denyRedeemablePositions(); - } - - function test_denyRedeemablePositions_success() public { - IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(positions); - - vm.expectEmit(true, true, false, true); - emit IOsTokenRedeemer.RedeemablePositionsDenied(positions.merkleRoot, positions.ipfsHash); - - vm.prank(owner); - _startSnapshotGas("EthOsTokenRedeemerTest_test_denyRedeemablePositions_success"); - osTokenRedeemer.denyRedeemablePositions(); - _stopSnapshotGas(); - - // Verify pending positions were cleared - (bytes32 pendingRoot, string memory pendingIpfs) = osTokenRedeemer.pendingRedeemablePositions(); - assertEq(pendingRoot, EMPTY_ROOT); - assertEq(pendingIpfs, ""); - } - - function test_removeRedeemablePositions_notOwner() public { - vm.prank(user1); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, user1)); - osTokenRedeemer.removeRedeemablePositions(); - } - - function test_removeRedeemablePositions_noPendingProposal() public { - vm.prank(owner); - osTokenRedeemer.removeRedeemablePositions(); - } - - function test_removeRedeemablePositions_success() public { - IOsTokenRedeemer.RedeemablePositions memory positions = - IOsTokenRedeemer.RedeemablePositions({merkleRoot: keccak256("test"), ipfsHash: TEST_IPFS_HASH}); - - _proposeAndAcceptPositions(positions); - - vm.expectEmit(true, true, false, true); - emit IOsTokenRedeemer.RedeemablePositionsRemoved(positions.merkleRoot, positions.ipfsHash); - - vm.prank(owner); - _startSnapshotGas("EthOsTokenRedeemerTest_test_removeRedeemablePositions_success"); - osTokenRedeemer.removeRedeemablePositions(); - _stopSnapshotGas(); - - // Verify positions were removed - (bytes32 currentRoot, string memory currentIpfsHash) = osTokenRedeemer.redeemablePositions(); - assertEq(currentRoot, EMPTY_ROOT); - assertEq(currentIpfsHash, ""); } function test_permitOsToken_success() public { @@ -649,6 +608,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { uint256 initialQueuedShares = osTokenRedeemer.queuedShares(); uint256 initialOsTokenBalance = IERC20(_osToken).balanceOf(user1); uint256 initialRedeemerBalance = IERC20(_osToken).balanceOf(address(osTokenRedeemer)); + uint256 initialCumulativeTickets = osTokenRedeemer.getExitQueueCumulativeTickets(); // Calculate expected position ticket (,, uint256 totalTickets) = osTokenRedeemer.getExitQueueData(); @@ -672,6 +632,13 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { osTokenRedeemer.queuedShares(), initialQueuedShares + osTokenShares, "Queued shares not updated correctly" ); + // Verify cumulative tickets increased correctly + assertEq( + osTokenRedeemer.getExitQueueCumulativeTickets(), + initialCumulativeTickets + osTokenShares, + "Cumulative tickets not updated correctly" + ); + // Verify osToken was transferred from user to redeemer assertEq( IERC20(_osToken).balanceOf(user1), @@ -690,7 +657,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { assertEq(exitRequestShares, osTokenShares, "Exit request not recorded correctly"); // Test entering queue with different receiver - address receiver = makeAddr("receiver"); + address receiver = makeAddr("Receiver"); // Mint more osTokens vm.prank(user1); @@ -709,14 +676,155 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { assertEq(exitRequestShares2, osTokenShares, "Exit request with different receiver not recorded correctly"); } + function test_redeemSubVaultsAssets_notPositionsManager() public { + _setupMetaVault(); + + // Try to call redeemSubVaultsAssets as a non-positionsManager + vm.prank(user1); + vm.expectRevert(Errors.AccessDenied.selector); + osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), 1 ether); + + // Try with owner (still not positionsManager) + vm.prank(owner); + vm.expectRevert(Errors.AccessDenied.selector); + osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), 1 ether); + } + + function test_redeemSubVaultsAssets_invalidMetaVault() public { + // Try with a regular vault (not a meta vault) + vm.prank(positionsManager); + vm.expectRevert(Errors.InvalidVault.selector); + osTokenRedeemer.redeemSubVaultsAssets(address(vault), 1 ether); + + // Try with a random address (not registered in vaults registry) + address randomAddress = makeAddr("RandomAddress"); + vm.prank(positionsManager); + vm.expectRevert(Errors.InvalidVault.selector); + osTokenRedeemer.redeemSubVaultsAssets(randomAddress, 1 ether); + } + + function test_redeemSubVaultsAssets_zeroAssetsToRedeem() public { + _setupMetaVault(); + + // Try to redeem zero assets + vm.prank(positionsManager); + vm.expectRevert(Errors.InvalidAssets.selector); + osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), 0); + } + + function test_redeemSubVaultOsToken_invalidMetaVault() public { + _setupMetaVault(); + + // Try calling from a non-meta vault (user) + vm.prank(user1); + vm.expectRevert(Errors.AccessDenied.selector); + osTokenRedeemer.redeemSubVaultOsToken(subVaults[0], 1 ether); + + // Try calling from a regular vault (not a meta vault) + vm.prank(address(vault)); + vm.expectRevert(Errors.AccessDenied.selector); + osTokenRedeemer.redeemSubVaultOsToken(subVaults[0], 1 ether); + + // Try with invalid sub vault (not registered) + address randomSubVault = makeAddr("RandomSubVault"); + vm.prank(address(metaVault)); + vm.expectRevert(Errors.AccessDenied.selector); + osTokenRedeemer.redeemSubVaultOsToken(randomSubVault, 1 ether); + } + + function test_redeemSubVaultOsToken_zeroSharesToRedeem() public { + _setupMetaVault(); + + // Try to redeem zero shares + vm.prank(address(metaVault)); + vm.expectRevert(Errors.InvalidShares.selector); + osTokenRedeemer.redeemSubVaultOsToken(subVaults[0], 0); + } + + function test_getExitQueueMissingAssets_noMissingAssets() public { + // Setup: Enter exit queue and process to create some totalTickets + uint256 sharesToQueue = 10 ether; + address user = makeAddr("User"); + _enterExitQueue(user, sharesToQueue); + + // Swap all queued shares to process them + _swapQueuedShares(sharesToQueue); + + // Process the exit queue + vm.warp(block.timestamp + EXIT_QUEUE_UPDATE_DELAY + 1); + osTokenRedeemer.processExitQueue(); + + // Get the totalTickets after processing + (,, uint256 totalTickets) = osTokenRedeemer.getExitQueueData(); + + // Query with targetCumulativeTickets <= totalTickets should return 0 + uint256 missingAssets = osTokenRedeemer.getExitQueueMissingAssets(totalTickets); + assertEq(missingAssets, 0, "Missing assets should be 0 when target <= totalTickets"); + + // Query with targetCumulativeTickets < totalTickets should also return 0 + missingAssets = osTokenRedeemer.getExitQueueMissingAssets(totalTickets / 2); + assertEq(missingAssets, 0, "Missing assets should be 0 when target < totalTickets"); + } + + function test_getExitQueueMissingAssets_availableAssetsExceedMissing() public { + // Setup: Enter exit queue with some shares + uint256 sharesToQueue = 10 ether; + address user = makeAddr("User"); + _enterExitQueue(user, sharesToQueue); + + // Send ETH directly to the redeemer contract (not via swap) to create available assets + // This way availableAssets > unclaimedAssets + uint256 directDeposit = 20 ether; + vm.deal(address(this), directDeposit); + (bool success,) = address(osTokenRedeemer).call{value: directDeposit}(""); + require(success, "Transfer failed"); + + // Get exit queue data + (,, uint256 totalTickets) = osTokenRedeemer.getExitQueueData(); + + // Target slightly above totalTickets - the direct deposit should cover this + uint256 ticketsToCover = 1 ether; + uint256 targetCumulativeTickets = totalTickets + ticketsToCover; + + // Query missing assets - should be 0 because available assets exceed the missing amount + uint256 missingAssets = osTokenRedeemer.getExitQueueMissingAssets(targetCumulativeTickets); + + // The contract has enough ETH to cover the missing assets + assertEq(missingAssets, 0, "Missing assets should be 0 when available assets exceed missing"); + } + + function test_getExitQueueMissingAssets_missingAssetsExceedAvailable() public { + // Setup: Enter exit queue with shares + uint256 sharesToQueue = 100 ether; + address user = makeAddr("User"); + _enterExitQueue(user, sharesToQueue); + + // Don't add any available assets - the redeemer contract has no ETH + // Get exit queue data + (,, uint256 totalTickets) = osTokenRedeemer.getExitQueueData(); + uint256 cumulativeTickets = osTokenRedeemer.getExitQueueCumulativeTickets(); + + // Target the full cumulativeTickets (all queued shares) + uint256 targetCumulativeTickets = cumulativeTickets; + + // Calculate expected missing assets + uint256 ticketsToCover = targetCumulativeTickets - totalTickets; + uint256 expectedMissingAssets = contracts.osTokenVaultController.convertToAssets(ticketsToCover); + + // Query missing assets + uint256 missingAssets = osTokenRedeemer.getExitQueueMissingAssets(targetCumulativeTickets); + + // Since redeemer has no available assets, missing assets should equal the full amount needed + assertEq( + missingAssets, expectedMissingAssets, "Missing assets should equal full amount when no available assets" + ); + } + function test_redeemOsTokenPositions_noQueuedShares() public { // Prepare merkle tree data IOsTokenRedeemer.OsTokenPosition[] memory positions = new IOsTokenRedeemer.OsTokenPosition[](1); positions[0] = IOsTokenRedeemer.OsTokenPosition({ - vault: address(vault), - owner: user1, - leafShares: 1 ether, - sharesToRedeem: 1 ether + vault: address(vault), owner: user1, leafShares: 1 ether, sharesToRedeem: 1 ether }); bytes32[] memory proof = new bytes32[](0); @@ -727,10 +835,10 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { keccak256(bytes.concat(keccak256(abi.encode(osTokenRedeemer.nonce(), address(vault), 1 ether, user1)))); IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: leaf, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); // Call redeemOsTokenPositions with no queued shares - should return early - vm.prank(user1); + vm.prank(positionsManager); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); // Verify no shares were redeemed @@ -759,7 +867,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { uint256 queuedSharesBefore = osTokenRedeemer.queuedShares(); - vm.prank(user1); + vm.prank(positionsManager); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); // Verify nothing was redeemed @@ -796,14 +904,14 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { keccak256(bytes.concat(keccak256(abi.encode(osTokenRedeemer.nonce(), address(vault), 1 ether, user1)))); IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: leaf, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); bytes32[] memory proof = new bytes32[](0); bool[] memory proofFlags = new bool[](0); uint256 queuedSharesBefore = osTokenRedeemer.queuedShares(); - vm.prank(user1); + vm.prank(positionsManager); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); // Verify nothing was redeemed @@ -829,10 +937,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Create position IOsTokenRedeemer.OsTokenPosition[] memory positions = new IOsTokenRedeemer.OsTokenPosition[](1); positions[0] = IOsTokenRedeemer.OsTokenPosition({ - vault: address(vault), - owner: user1, - leafShares: 1 ether, - sharesToRedeem: 1 ether + vault: address(vault), owner: user1, leafShares: 1 ether, sharesToRedeem: 1 ether }); // Set up merkle root with different values (invalid proof) @@ -841,7 +946,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { ); IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: leaf, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); // Provide wrong proof bytes32[] memory proof = new bytes32[](1); @@ -855,6 +960,38 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); } + function test_redeemOsTokenPositions_zeroRoot() public { + // Setup: Create queued shares + _collateralizeEthVault(address(vault)); + _depositToVault(address(vault), DEPOSIT_AMOUNT, user1, user1); + + uint256 osTokenShares = contracts.osTokenVaultController.convertToShares(1 ether); + vm.prank(user1); + vault.mintOsToken(user1, osTokenShares, address(0)); + + vm.prank(user1); + IERC20(_osToken).approve(address(osTokenRedeemer), osTokenShares); + + vm.prank(user1); + osTokenRedeemer.enterExitQueue(osTokenShares, user1); + + // Create position (redeemable positions not set - merkle root is zero) + IOsTokenRedeemer.OsTokenPosition[] memory positions = new IOsTokenRedeemer.OsTokenPosition[](1); + positions[0] = IOsTokenRedeemer.OsTokenPosition({ + vault: address(vault), owner: user1, leafShares: osTokenShares, sharesToRedeem: osTokenShares + }); + + bytes32[] memory proof = new bytes32[](0); + bool[] memory proofFlags = new bool[](0); + + // When redeemable positions were never set, nonce is 0 + // The contract uses nonce - 1 for leaf calculation, which causes an underflow + // This effectively protects against calling redeemOsTokenPositions without setting positions first + vm.prank(positionsManager); + vm.expectRevert(); // Arithmetic underflow because nonce = 0 and code does nonce - 1 + osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); + } + function test_redeemOsTokenPositions_success_singlePosition() public { // Setup: Create vault position and mint osTokens _collateralizeEthVault(address(vault)); @@ -886,7 +1023,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { ); IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: leaf, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); bytes32[] memory proof = new bytes32[](0); bool[] memory proofFlags = new bool[](0); @@ -900,7 +1037,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { emit IOsTokenRedeemer.OsTokenPositionsRedeemed(expectedRedeemedShares, expectedRedeemedAssets); // Redeem position - vm.prank(user1); + vm.prank(positionsManager); _startSnapshotGas("EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition"); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); _stopSnapshotGas(); @@ -948,16 +1085,10 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Create multiple positions IOsTokenRedeemer.OsTokenPosition[] memory positions = new IOsTokenRedeemer.OsTokenPosition[](2); positions[0] = IOsTokenRedeemer.OsTokenPosition({ - vault: address(vault), - owner: user1, - leafShares: user1Shares, - sharesToRedeem: user1Shares / 2 + vault: address(vault), owner: user1, leafShares: user1Shares, sharesToRedeem: user1Shares / 2 }); positions[1] = IOsTokenRedeemer.OsTokenPosition({ - vault: address(vault2), - owner: user2, - leafShares: user2Shares, - sharesToRedeem: user2Shares + vault: address(vault2), owner: user2, leafShares: user2Shares, sharesToRedeem: user2Shares }); // Create merkle tree @@ -970,7 +1101,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: merkleRoot, ipfsHash: TEST_IPFS_HASH}); - _proposeAndAcceptPositions(redeemablePositions); + _setRedeemablePositions(redeemablePositions); // Create merkle proof bytes32[] memory proof = new bytes32[](0); @@ -986,7 +1117,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { uint256 redeemedSharesBefore = osTokenRedeemer.redeemedShares(); // Redeem positions - vm.prank(user1); + vm.prank(positionsManager); _startSnapshotGas("EthOsTokenRedeemerTest_test_redeemOsTokenPositions_success_multiplePositions"); osTokenRedeemer.redeemOsTokenPositions(_positions, proof, proofFlags); _stopSnapshotGas(); @@ -1127,7 +1258,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { function test_processExitQueue_success() public { uint256 queuedShares = 100 ether; - address user = makeAddr("user"); + address user = makeAddr("User"); _enterExitQueue(user, queuedShares); _swapQueuedShares(queuedShares / 2); @@ -1196,7 +1327,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { // Setup: Create a non-existent position ticket uint256 nonExistentTicket = 99999; uint256 nonExistentExitQueueIndex = 0; - address nonExistentUser = makeAddr("nonExistentUser"); + address nonExistentUser = makeAddr("NonExistentUser"); // Act & Assert: Try to claim from a non-existent position vm.prank(nonExistentUser); @@ -1220,7 +1351,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { function test_claimExitedAssets_partialWithdrawal() public { // Setup: Create a user with a large position uint256 sharesToQueue = 100 ether; - address user = makeAddr("user"); + address user = makeAddr("User"); uint256 positionTicket = _enterExitQueue(user, sharesToQueue); // Simulate partial processing by having limited assets available @@ -1290,7 +1421,7 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { function test_claimExitedAssets_fullWithdrawal() public { // Setup: Create a user with a large position uint256 sharesToQueue = 100 ether; - address user = makeAddr("user"); + address user = makeAddr("User"); uint256 positionTicket = _enterExitQueue(user, sharesToQueue); // Simulate partial processing by having limited assets available @@ -1331,4 +1462,236 @@ contract EthOsTokenRedeemerTest is Test, EthHelpers { userBalanceAfter - userBalanceBefore, exitedAssets, "User should receive the partially processed assets" ); } + + // ========== Concurrent Redemption Tests ========== + + function test_redeemSubVaultsAssets_concurrentMultipleSubVaults() public { + // Setup meta vault with 4 sub vaults to test concurrent redemptions + _setupMetaVaultWithSubVaults(4); + + // Deposit significant amount and distribute to sub vaults + uint256 depositAmount = 100 ether; + vm.deal(user1, depositAmount); + vm.prank(user1); + metaVault.deposit{value: depositAmount}(user1, address(0)); + + // Distribute assets to sub-vaults + metaVault.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; + } + + // Request redemption that will require multiple sub-vaults + uint256 assetsToRedeem = 30 ether; + + vm.prank(positionsManager); + _startSnapshotGas("test_redeemSubVaultsAssets_concurrentMultipleSubVaults"); + uint256 totalRedeemed = osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), assetsToRedeem); + _stopSnapshotGas(); + + // 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; + if (stakedSharesAfter < stakedSharesBefore[i]) { + subVaultsWithRedemptions++; + } + } + + assertGt(subVaultsWithRedemptions, 1, "Should redeem from multiple sub-vaults concurrently"); + assertApproxEqRel(totalRedeemed, assetsToRedeem, 1, "Total redeemed should match requested"); + } + + function test_redeemSubVaultsAssets_multipleSequentialRedemptions() public { + // Setup meta vault + _setupMetaVaultWithSubVaults(3); + + // User deposits + vm.deal(user1, 100 ether); + vm.prank(user1); + metaVault.deposit{value: 100 ether}(user1, address(0)); + + // Distribute assets to sub-vaults + metaVault.depositToSubVaults(); + _updateMetaVaultState(); + + // Use fixed amounts for redemption + uint256 firstRedeemAmount = 10 ether; + + vm.prank(positionsManager); + uint256 redeemed1 = osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), firstRedeemAmount); + + // Verify first redemption succeeded (may be less than requested if limited by withdrawable) + assertGt(redeemed1, 0, "First redemption should succeed"); + + // Second redemption + vm.prank(positionsManager); + uint256 redeemed2 = osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), firstRedeemAmount); + + // Verify second redemption succeeded + assertGt(redeemed2, 0, "Second redemption should succeed"); + + // Total should be greater than zero and both redemptions should work + assertGt(redeemed1 + redeemed2, redeemed1, "Second redemption should add to total"); + } + + function test_redeemSubVaultsAssets_stateUpdatesAcrossSubVaults() public { + // Setup with known configuration + _setupMetaVaultWithSubVaults(3); + + uint256 depositAmount = 60 ether; + vm.deal(user1, depositAmount); + vm.prank(user1); + metaVault.deposit{value: depositAmount}(user1, address(0)); + + // Distribute assets to sub-vaults + metaVault.depositToSubVaults(); + _updateMetaVaultState(); + + // Record initial states + uint256 totalStakedBefore = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalStakedBefore += metaVault.subVaultsStates(subVaults[i]).stakedShares; + } + + // Redeem significant amount + uint256 assetsToRedeem = 40 ether; + vm.prank(positionsManager); + osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), assetsToRedeem); + + // Verify state updates were properly applied + uint256 totalStakedAfter = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalStakedAfter += metaVault.subVaultsStates(subVaults[i]).stakedShares; + } + + // Total staked shares should decrease + assertLt(totalStakedAfter, totalStakedBefore, "Total staked shares should decrease after redemption"); + + // Verify no negative shares or overflow + for (uint256 i = 0; i < subVaults.length; i++) { + uint128 stakedShares = metaVault.subVaultsStates(subVaults[i]).stakedShares; + assertGe(stakedShares, 0, "Staked shares should never be negative"); + } + } + + function test_redeemSubVaultsAssets_largeAmountAcrossAllSubVaults() public { + // Setup with many sub vaults + _setupMetaVaultWithSubVaults(5); + + // Large deposit + uint256 depositAmount = 200 ether; + vm.deal(user1, depositAmount); + vm.prank(user1); + metaVault.deposit{value: depositAmount}(user1, address(0)); + + // Distribute assets to sub-vaults + metaVault.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; + } + + // Request a large redemption amount (uses fixed amount like other working tests) + uint256 assetsToRedeem = 100 ether; + + vm.prank(positionsManager); + _startSnapshotGas("test_redeemSubVaultsAssets_largeAmountAcrossAllSubVaults"); + uint256 totalRedeemed = osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), assetsToRedeem); + _stopSnapshotGas(); + + // Verify we redeemed something + assertGt(totalRedeemed, 0, "Should redeem some assets"); + + // 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; + if (currentShares < initialShares[i]) { + vaultsWithReductions++; + } + } + + // Multiple vaults should have contributed to the redemption + assertGt(vaultsWithReductions, 0, "Some sub-vaults should have been redeemed from"); + } + + function test_redeemSubVaultsAssets_accountingConsistencyAfterRedemption() public { + // Test that sub vault accounting remains consistent after redemption + _setupMetaVaultWithSubVaults(4); + + uint256 depositAmount = 100 ether; + vm.deal(user1, depositAmount); + vm.prank(user1); + metaVault.deposit{value: depositAmount}(user1, address(0)); + + // Distribute assets to sub-vaults + metaVault.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; + } + + // Perform a redemption + uint256 redeemAmount = 20 ether; + vm.prank(positionsManager); + uint256 redeemed = osTokenRedeemer.redeemSubVaultsAssets(address(metaVault), redeemAmount); + + // Verify redemption succeeded + assertGt(redeemed, 0, "Should have redeemed some assets"); + + // Verify sub vault shares decreased + uint256 totalSubVaultSharesAfter = 0; + for (uint256 i = 0; i < subVaults.length; i++) { + totalSubVaultSharesAfter += metaVault.subVaultsStates(subVaults[i]).stakedShares; + } + assertLt(totalSubVaultSharesAfter, totalSubVaultSharesBefore, "Sub vault shares should decrease"); + + // Meta vault's balance should have increased (it received the redeemed assets) + uint256 metaVaultBalanceAfter = address(metaVault).balance; + assertGt(metaVaultBalanceAfter, metaVaultBalanceBefore, "Meta vault balance should increase"); + + // The balance increase should approximately match the redeemed amount + assertApproxEqRel( + metaVaultBalanceAfter - metaVaultBalanceBefore, redeemed, 5e16, "Balance increase should match redeemed" + ); + } + + // Helper to setup meta vault with specific number of sub vaults + function _setupMetaVaultWithSubVaults(uint256 numSubVaults) internal { + // Clear existing subVaults array + delete subVaults; + + // Deploy meta vault + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + metaVault = EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // Deploy and add sub vaults + for (uint256 i = 0; i < numSubVaults; i++) { + address subVault = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + subVaults.push(subVault); + + vm.prank(admin); + metaVault.addSubVault(subVault); + } + } } diff --git a/test/EthOsTokenVaultEscrow.t.sol b/test/EthOsTokenVaultEscrow.t.sol index 2470c7f0..52318775 100644 --- a/test/EthOsTokenVaultEscrow.t.sol +++ b/test/EthOsTokenVaultEscrow.t.sol @@ -34,9 +34,9 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Setup addresses - user = makeAddr("user"); - admin = makeAddr("admin"); - liquidator = makeAddr("liquidator"); + user = makeAddr("User"); + admin = makeAddr("Admin"); + liquidator = makeAddr("Liquidator"); // Fund accounts vm.deal(user, 100 ether); @@ -497,7 +497,7 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { contracts.osTokenVaultEscrow.processExitedAssets(address(vault), exitPositionTicket, timestamp, exitQueueIndex); // Try to claim as a different user - address otherUser = makeAddr("otherUser"); + address otherUser = makeAddr("OtherUser"); _mintOsToken(otherUser, osTokenShares); // Give them the required osToken shares vm.startPrank(otherUser); @@ -540,11 +540,12 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { vm.prank(user); _startSnapshotGas("EthOsTokenVaultEscrowTest_test_claimExitedAssets_insufficientShares"); vm.expectRevert(Errors.InvalidShares.selector); - contracts.osTokenVaultEscrow.claimExitedAssets( - address(vault), - exitPositionTicket, - osTokenShares * 2 // More than available - ); + contracts.osTokenVaultEscrow + .claimExitedAssets( + address(vault), + exitPositionTicket, + osTokenShares * 2 // More than available + ); _stopSnapshotGas(); } @@ -794,9 +795,8 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { // Liquidate the position vm.prank(liquidator); _startSnapshotGas("OsTokenLiquidationTest_test_liquidateOsToken_success"); - contracts.osTokenVaultEscrow.liquidateOsToken( - address(vault), exitPositionTicket_, liquidationShares, liquidator - ); + contracts.osTokenVaultEscrow + .liquidateOsToken(address(vault), exitPositionTicket_, liquidationShares, liquidator); _stopSnapshotGas(); // Get position details after liquidation @@ -808,13 +808,13 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { assertApproxEqAbs( liquidatorBalanceAfter - liquidatorBalanceBefore, exitedAssetsBefore, - 2, + 5, "Liquidator did not receive correct amount of assets" ); // Verify position was updated assertEq(ownerAfter, ownerBefore, "Owner should not change"); - assertApproxEqAbs(exitedAssetsAfter, 0, 2, "Exited assets not correctly reduced"); + assertApproxEqAbs(exitedAssetsAfter, 0, 5, "Exited assets not correctly reduced"); assertLt(sharesAfter, sharesBefore, "Shares not correctly reduced"); } @@ -942,9 +942,8 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { // Liquidate half the position vm.prank(liquidator); _startSnapshotGas("OsTokenLiquidationTest_test_liquidateOsToken_partialLiquidation"); - contracts.osTokenVaultEscrow.liquidateOsToken( - address(vault), exitPositionTicket, halfLiquidationShares, liquidator - ); + contracts.osTokenVaultEscrow + .liquidateOsToken(address(vault), exitPositionTicket, halfLiquidationShares, liquidator); _stopSnapshotGas(); // push down the stack @@ -996,7 +995,7 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { uint256 expectedAssets = contracts.osTokenVaultController.convertToAssets(osTokenShares); // Mint osToken shares to the redeemer - address redeemer = makeAddr("redeemer"); + address redeemer = makeAddr("Redeemer"); _mintOsToken(redeemer, osTokenShares); // set redeemer @@ -1004,7 +1003,7 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { contracts.osTokenConfig.setRedeemer(redeemer); // Record redeemer balance before - address receiver = makeAddr("receiver"); + address receiver = makeAddr("Receiver"); uint256 receiverBalanceBefore = receiver.balance; // Expect OsTokenRedeemed event @@ -1052,7 +1051,7 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { contracts.osTokenVaultEscrow.processExitedAssets(address(vault), exitPositionTicket, timestamp, exitQueueIndex); // Mock the osTokenConfig.redeemer call to return an official redeemer address - address officialRedeemer = makeAddr("officialRedeemer"); + address officialRedeemer = makeAddr("OfficialRedeemer"); vm.mockCall( address(contracts.osTokenConfig), abi.encodeWithSelector(bytes4(keccak256("redeemer()"))), @@ -1060,15 +1059,14 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { ); // Try to redeem from an unauthorized address - address unauthorizedCaller = makeAddr("unauthorizedCaller"); + address unauthorizedCaller = makeAddr("UnauthorizedCaller"); _mintOsToken(unauthorizedCaller, osTokenShares); vm.prank(unauthorizedCaller); _startSnapshotGas("OsTokenLiquidationTest_test_redeemOsToken_notRedeemer"); vm.expectRevert(Errors.AccessDenied.selector); - contracts.osTokenVaultEscrow.redeemOsToken( - address(vault), exitPositionTicket, osTokenShares, unauthorizedCaller - ); + contracts.osTokenVaultEscrow + .redeemOsToken(address(vault), exitPositionTicket, osTokenShares, unauthorizedCaller); _stopSnapshotGas(); // Clear the mock @@ -1110,7 +1108,7 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { uint256 newLiqBonusPercent = 1.1e18; // 110% // Get a non-owner address - address nonOwner = makeAddr("nonOwner"); + address nonOwner = makeAddr("NonOwner"); // Call updateLiqConfig as non-owner, should revert vm.prank(nonOwner); @@ -1168,11 +1166,11 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { address currentAuthenticator = contracts.osTokenVaultEscrow.authenticator(); // Create a new authenticator address - address newAuthenticator = makeAddr("newAuthenticator"); + address newAuthenticator = makeAddr("NewAuthenticator"); // Ensure it's different from the current one if (newAuthenticator == currentAuthenticator) { - newAuthenticator = makeAddr("newAuthenticator2"); + newAuthenticator = makeAddr("NewAuthenticator2"); } // Expect AuthenticatorUpdated event @@ -1191,10 +1189,10 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { function test_setAuthenticator_onlyOwner() public { // Create a new authenticator address - address newAuthenticator = makeAddr("newAuthenticator"); + address newAuthenticator = makeAddr("NewAuthenticator"); // Get a non-owner address - address nonOwner = makeAddr("nonOwner"); + address nonOwner = makeAddr("NonOwner"); // Call setAuthenticator as non-owner, should revert vm.prank(nonOwner); diff --git a/test/EthPrivErc20Vault.t.sol b/test/EthPrivErc20Vault.t.sol index a8ea6033..1f9b2fb6 100644 --- a/test/EthPrivErc20Vault.t.sol +++ b/test/EthPrivErc20Vault.t.sol @@ -31,11 +31,11 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - whitelister = makeAddr("whitelister"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + whitelister = makeAddr("Whitelister"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); diff --git a/test/EthPrivMetaVault.t.sol b/test/EthPrivMetaVault.t.sol new file mode 100644 index 00000000..5b46a784 --- /dev/null +++ b/test/EthPrivMetaVault.t.sol @@ -0,0 +1,582 @@ +// 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 {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +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 {IKeeperRewards} from "../contracts/interfaces/IKeeperRewards.sol"; +import {Errors} from "../contracts/libraries/Errors.sol"; +import {EthPrivMetaVault} from "../contracts/vaults/ethereum/EthPrivMetaVault.sol"; +import {EthHelpers} from "./helpers/EthHelpers.sol"; + +contract EthPrivMetaVaultTest is Test, EthHelpers { + ForkContracts public contracts; + EthPrivMetaVault public metaVault; + + address public admin; + address public sender; + address public receiver; + address public referrer; + address public whitelister; + address public other; + + // 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"); + other = makeAddr("Other"); + + // Deal ETH to accounts + vm.deal(admin, 100 ether); + vm.deal(sender, 100 ether); + vm.deal(other, 100 ether); + + // Deploy private meta vault + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: _balancedCurator, + capacity: type(uint256).max, + feePercent: 0, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + metaVault = EthPrivMetaVault(payable(_getOrCreateVault(VaultType.EthPrivMetaVault, admin, initParams, false))); + + // Get existing sub vaults (if any) + address[] memory currentSubVaults = metaVault.getSubVaults(); + for (uint256 i = 0; i < currentSubVaults.length; i++) { + subVaults.push(currentSubVaults[i]); + } + + // Deploy and add sub vaults + for (uint256 i = 0; i < 3; i++) { + address subVault = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault); + subVaults.push(subVault); + + vm.prank(admin); + metaVault.addSubVault(subVault); + } + } + + function _createSubVault(address _admin) internal returns (address) { + bytes memory initParams = abi.encode( + IEthVault.EthVaultInitParams({ + capacity: 1000 ether, + feePercent: 5, // 5% + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + + return _createVault(VaultType.EthVault, _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("EthPrivMetaVault"), "Incorrect vault ID"); + 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(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(); + for (uint256 i = 0; i < subVaults.length; i++) { + assertEq(storedSubVaults[i], subVaults[i], "Incorrect sub vault address"); + } + } + + function test_cannotInitializeTwice() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + metaVault.initialize{value: 0}("0x"); + } + + // ============ Whitelist Deposit Tests ============ + + function test_cannotDepositFromNotWhitelistedSender() public { + uint256 amount = 1 ether; + + // Set whitelister and whitelist receiver but not sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(receiver, true); + + // Try to deposit from non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.deposit{value: amount}(receiver, referrer); + } + + function test_cannotDepositToNotWhitelistedReceiver() public { + uint256 amount = 1 ether; + + // Set whitelister and whitelist sender but not receiver + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Try to deposit to non-whitelisted receiver + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.deposit{value: amount}(receiver, referrer); + } + + function test_canDepositAsWhitelistedUser() public { + uint256 amount = 1 ether; + 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(); + + // Deposit as whitelisted user + vm.prank(sender); + _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositAsWhitelistedUser"); + uint256 shares = metaVault.deposit{value: amount}(receiver, referrer); + _stopSnapshotGas(); + + // Check balances + assertApproxEqAbs(shares, expectedShares, 1, "Incorrect shares minted"); + assertApproxEqAbs(metaVault.getShares(receiver), expectedShares, 1, "Receiver did not receive shares"); + assertApproxEqAbs(metaVault.totalShares(), totalSharesBefore + expectedShares, 1, "Incorrect total shares"); + } + + // ============ Whitelist Receive Tests ============ + + function test_cannotDepositUsingReceiveAsNotWhitelistedUser() public { + uint256 amount = 1 ether; + + // Set whitelister and don't whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + // Try to deposit using receive function as non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + Address.sendValue(payable(metaVault), amount); + } + + function test_canDepositUsingReceiveAsWhitelistedUser() public { + uint256 amount = 1 ether; + uint256 expectedShares = metaVault.convertToShares(amount); + + // Set whitelister and whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit using receive function as whitelisted user + vm.prank(sender); + _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositUsingReceiveAsWhitelistedUser"); + Address.sendValue(payable(metaVault), amount); + _stopSnapshotGas(); + + // Check balances + assertApproxEqAbs(metaVault.getShares(sender), expectedShares, 1, "Sender did not receive shares"); + } + + function test_subVaultCanSendEthWithoutWhitelist() public { + // Set whitelister + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + // Whitelist sender for initial deposit + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit and push to sub vaults + uint256 depositAmount = 10 ether; + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Sub vault is NOT whitelisted + assertFalse(metaVault.whitelistedAccounts(subVaults[0]), "Sub vault should not be whitelisted"); + + // Fund sub vault with ETH to simulate claiming + vm.deal(subVaults[0], 1 ether); + + // Sub vault should be able to send ETH (simulating claim) without being whitelisted + uint256 metaVaultBalanceBefore = address(metaVault).balance; + + vm.prank(subVaults[0]); + Address.sendValue(payable(metaVault), 0.5 ether); + + uint256 metaVaultBalanceAfter = address(metaVault).balance; + assertEq( + metaVaultBalanceAfter, metaVaultBalanceBefore + 0.5 ether, "Meta vault should receive ETH from sub vault" + ); + } + + // ============ Whitelist MintOsToken Tests ============ + + function test_cannotMintOsTokenFromNotWhitelistedUser() public { + uint256 depositAmount = 10 ether; + + // Set whitelister and whitelist user for initial deposit + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.startPrank(whitelister); + metaVault.updateWhitelist(sender, true); + vm.stopPrank(); + + // Deposit ETH to get vault shares + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // 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 = 10 ether; + + // Set whitelister and whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit ETH to get vault shares + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // Deposit to sub vaults to collateralize + metaVault.depositToSubVaults(); + + // Mint osToken as whitelisted user + uint256 osTokenShares = depositAmount / 2; + vm.prank(sender); + _startSnapshotGas("EthPrivMetaVaultTest_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"); + } + + // ============ Whitelist DepositAndMintOsToken Tests ============ + + function test_cannotDepositAndMintOsTokenAsNotWhitelistedUser() public { + uint256 depositAmount = 10 ether; + + // First collateralize the meta vault + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(admin, true); + + vm.prank(admin); + metaVault.deposit{value: depositAmount}(admin, referrer); + metaVault.depositToSubVaults(); + + // Whitelist receiver but not sender + vm.prank(whitelister); + metaVault.updateWhitelist(receiver, true); + + // Try to deposit and mint osToken as non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.depositAndMintOsToken{value: depositAmount}(receiver, depositAmount / 2, referrer); + } + + function test_canDepositAndMintOsTokenAsWhitelistedUser() public { + uint256 depositAmount = 10 ether; + + // First collateralize the meta vault + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.startPrank(whitelister); + metaVault.updateWhitelist(sender, true); + vm.stopPrank(); + + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + metaVault.depositToSubVaults(); + + // Deposit and mint osToken as whitelisted user + uint256 osTokenShares = depositAmount / 2; + vm.prank(sender); + _startSnapshotGas("EthPrivMetaVaultTest_test_canDepositAndMintOsTokenAsWhitelistedUser"); + uint256 assets = metaVault.depositAndMintOsToken{value: depositAmount}(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"); + } + + // ============ Whitelist UpdateStateAndDeposit Tests ============ + + function test_cannotUpdateStateAndDepositFromNotWhitelistedUser() public { + // Set whitelister and don't whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Try to update state and deposit from non-whitelisted user + vm.prank(sender); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.updateStateAndDeposit{value: 1 ether}(receiver, referrer, harvestParams); + } + + function test_canUpdateStateAndDepositAsWhitelistedUser() public { + // 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(); + + IKeeperRewards.HarvestParams memory harvestParams = _getEmptyHarvestParams(); + + // Update state and deposit as whitelisted user + uint256 amount = 1 ether; + uint256 expectedShares = metaVault.convertToShares(amount); + vm.prank(sender); + _startSnapshotGas("EthPrivMetaVaultTest_test_canUpdateStateAndDepositAsWhitelistedUser"); + uint256 shares = metaVault.updateStateAndDeposit{value: amount}(receiver, referrer, harvestParams); + _stopSnapshotGas(); + + // Check balances + assertApproxEqAbs(shares, expectedShares, 1, "Incorrect shares minted"); + assertApproxEqAbs(metaVault.getShares(receiver), expectedShares, 1, "Receiver did not receive shares"); + } + + // ============ 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("EthPrivMetaVaultTest_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("EthPrivMetaVaultTest_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 = 1 ether; + uint256 expectedShares = metaVault.convertToShares(amount); + + // Set whitelister and whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit ETH to get vault shares + vm.prank(sender); + metaVault.deposit{value: amount}(sender, referrer); + + uint256 initialBalance = metaVault.getShares(sender); + assertApproxEqAbs(initialBalance, expectedShares, 1, "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{value: 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 = 10 ether; + + // Set whitelister and whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + // 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("EthPrivMetaVaultTest_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 = 10 ether; + + // Set whitelister and whitelist sender + vm.prank(admin); + metaVault.setWhitelister(whitelister); + + vm.prank(whitelister); + metaVault.updateWhitelist(sender, true); + + // Deposit + vm.prank(sender); + metaVault.deposit{value: depositAmount}(sender, referrer); + + 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("EthPrivMetaVaultTest_test_enterExitQueueWorksForWhitelistedUserAfterRemoval"); + metaVault.enterExitQueue(senderShares, sender); + _stopSnapshotGas(); + + // Verify shares were reduced + assertEq(metaVault.getShares(sender), 0, "Shares should be 0 after entering exit queue"); + } +} diff --git a/test/EthPrivVault.t.sol b/test/EthPrivVault.t.sol index a72ea830..e767fa1c 100644 --- a/test/EthPrivVault.t.sol +++ b/test/EthPrivVault.t.sol @@ -31,11 +31,11 @@ contract EthPrivVaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - whitelister = makeAddr("whitelister"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + whitelister = makeAddr("Whitelister"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); @@ -391,7 +391,7 @@ contract EthPrivVaultTest is Test, EthHelpers { } function test_setWhitelister() public { - address newWhitelister = makeAddr("newWhitelister"); + address newWhitelister = makeAddr("NewWhitelister"); // Non-admin cannot set whitelister vm.prank(other); diff --git a/test/EthRewardSplitter.t.sol b/test/EthRewardSplitter.t.sol index d4076f6c..02aba7ef 100644 --- a/test/EthRewardSplitter.t.sol +++ b/test/EthRewardSplitter.t.sol @@ -34,11 +34,11 @@ contract EthRewardSplitterTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - shareholder1 = makeAddr("shareholder1"); - shareholder2 = makeAddr("shareholder2"); - depositor = makeAddr("depositor"); - claimer = makeAddr("claimer"); + admin = makeAddr("Admin"); + shareholder1 = makeAddr("Shareholder1"); + shareholder2 = makeAddr("Shareholder2"); + depositor = makeAddr("Depositor"); + claimer = makeAddr("Claimer"); // Fund accounts vm.deal(admin, 100 ether); @@ -298,7 +298,7 @@ contract EthRewardSplitterTest is Test, EthHelpers { // Someone else enters exit queue on behalf of shareholder1 vm.prank(claimer); uint256 timestamp = vm.getBlockTimestamp(); - vm.expectEmit(true, false, false, true); + vm.expectEmit(true, false, false, false); emit IRewardSplitter.ExitQueueEnteredOnBehalf(shareholder1, 0, rewards); // Position ticket is unknown at this point _startSnapshotGas("EthRewardSplitter_enterExitQueueOnBehalf"); uint256 positionTicket = rewardSplitter.enterExitQueueOnBehalf(rewards, shareholder1); @@ -461,7 +461,7 @@ contract EthRewardSplitterTest is Test, EthHelpers { rewardSplitter.decreaseShares(address(0), 1000); // Also test non-zero but invalid account (one that has no shares) - address randomAccount = makeAddr("randomAccount"); + address randomAccount = makeAddr("RandomAccount"); vm.prank(admin); vm.expectRevert(); // This will revert when trying to decrease below zero, but the error type may vary rewardSplitter.decreaseShares(randomAccount, 1000); diff --git a/test/EthValidatorsChecker.t.sol b/test/EthValidatorsChecker.t.sol index 1ed7f7af..12834085 100644 --- a/test/EthValidatorsChecker.t.sol +++ b/test/EthValidatorsChecker.t.sol @@ -43,8 +43,8 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { ); // Setup accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); vm.deal(user, 100 ether); // Create and prepare a vault with sufficient funds @@ -89,7 +89,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { function testValidatorsManagerSignature_InvalidVault() public { // Use non-existent vault - address invalidVault = makeAddr("nonVault"); + address invalidVault = makeAddr("NonVault"); // Test with invalid vault (uint256 blockNumber, IValidatorsChecker.Status status) = @@ -155,7 +155,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { function testCheckDepositDataRoot_InvalidVault() public { // Use non-existent vault - address invalidVault = makeAddr("nonVault"); + address invalidVault = makeAddr("NonVault"); // Create params with invalid vault IValidatorsChecker.DepositDataRootCheckParams memory params = IValidatorsChecker.DepositDataRootCheckParams({ @@ -249,7 +249,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { _collateralizeEthVault(address(customVault)); // Set a custom validators manager - address customManager = makeAddr("customManager"); + address customManager = makeAddr("CustomManager"); vm.prank(admin); IVaultValidators(customVault).setValidatorsManager(customManager); @@ -360,7 +360,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { function test_checkValidatorsManagerSignature_Success() public { // 1. Get the validators manager - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); uint256 validatorsManagerPrivKey = uint256(keccak256(abi.encodePacked("validatorsManager_key"))); validatorsManager = vm.addr(validatorsManagerPrivKey); @@ -482,6 +482,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { uint256 missingAssets = validatorsChecker.getExitQueueMissingAssets( emptyVault, 0, // No pending assets + 0, // No redemption assets targetCumulativeTickets ); @@ -507,13 +508,14 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { uint256 missingAssetsNoPending = validatorsChecker.getExitQueueMissingAssets( vault, 0, // No pending assets + 0, // No redemption assets cumulativeTickets ); // Test with some pending assets uint256 pendingAssets = 0.5 ether; uint256 missingAssetsWithPending = - validatorsChecker.getExitQueueMissingAssets(vault, pendingAssets, cumulativeTickets); + validatorsChecker.getExitQueueMissingAssets(vault, pendingAssets, 0, cumulativeTickets); // Pending assets should reduce missing assets assertGt(missingAssetsNoPending, missingAssetsWithPending, "Pending assets should reduce missing assets"); @@ -542,13 +544,14 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { uint256 initialMissingAssets = validatorsChecker.getExitQueueMissingAssets( vault, 0, // No pending assets + 0, cumulativeTickets ); // Add pending assets and check changes uint256 pendingAssets = 4 ether; uint256 updatedMissingAssets = - validatorsChecker.getExitQueueMissingAssets(vault, pendingAssets, cumulativeTickets); + validatorsChecker.getExitQueueMissingAssets(vault, pendingAssets, 0, cumulativeTickets); // Verify missing assets decrease with pending assets assertLt(updatedMissingAssets, initialMissingAssets, "Missing assets should decrease with pending assets"); @@ -566,12 +569,12 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { uint256 cumulativeTickets = validatorsChecker.getExitQueueCumulativeTickets(vault); // Get missing assets with no pending assets - uint256 missingAssetsBefore = validatorsChecker.getExitQueueMissingAssets(vault, 0, cumulativeTickets); + uint256 missingAssetsBefore = validatorsChecker.getExitQueueMissingAssets(vault, 0, 0, cumulativeTickets); // Test with large amount of pending assets (more than missing) uint256 excessPendingAssets = missingAssetsBefore * 2; uint256 missingAssetsAfter = - validatorsChecker.getExitQueueMissingAssets(vault, excessPendingAssets, cumulativeTickets); + validatorsChecker.getExitQueueMissingAssets(vault, excessPendingAssets, 0, cumulativeTickets); // No missing assets with excess pending assets assertEq(missingAssetsAfter, 0, "No missing assets with excess pending assets"); @@ -595,6 +598,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { uint256 missingAssets = validatorsChecker.getExitQueueMissingAssets( vault, 0, // No pending assets + 0, cumulativeTickets ); diff --git a/test/EthVault.t.sol b/test/EthVault.t.sol index 5693697c..374ce49f 100644 --- a/test/EthVault.t.sol +++ b/test/EthVault.t.sol @@ -32,11 +32,11 @@ contract EthVaultTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - referrer = makeAddr("referrer"); - validatorsManager = makeAddr("validatorsManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + referrer = makeAddr("Referrer"); + validatorsManager = makeAddr("ValidatorsManager"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); @@ -178,8 +178,7 @@ contract EthVaultTest is Test, EthHelpers { uint256 senderSharesBefore = vault.getShares(sender); ( uint128 queuedSharesBefore, - uint128 unclaimedAssetsBefore, - , + uint128 unclaimedAssetsBefore,, uint128 totalExitingAssetsBefore, uint256 totalTicketsBefore ) = vault.getExitQueueData(); @@ -196,8 +195,7 @@ contract EthVaultTest is Test, EthHelpers { ( uint128 queuedSharesAfter, - uint128 unclaimedAssetsAfter, - , + uint128 unclaimedAssetsAfter,, uint128 totalExitingAssetsAfter, uint256 totalTicketsAfter ) = vault.getExitQueueData(); @@ -289,7 +287,7 @@ contract EthVaultTest is Test, EthHelpers { function test_withdrawValidator_unknown() public { // Create an unknown address - address unknown = makeAddr("unknown"); + address unknown = makeAddr("Unknown"); // Fund the unknown account uint256 withdrawFee = 0.1 ether; diff --git a/test/KeeperOracles.t.sol b/test/KeeperOracles.t.sol index b4796970..e0cf560c 100644 --- a/test/KeeperOracles.t.sol +++ b/test/KeeperOracles.t.sol @@ -23,8 +23,8 @@ contract KeeperOraclesTest is Test, EthHelpers { // Set up test accounts owner = keeper.owner(); - newOracle = makeAddr("newOracle"); - nonOwner = makeAddr("nonOwner"); + newOracle = makeAddr("NewOracle"); + nonOwner = makeAddr("NonOwner"); } // Test cases for addOracle @@ -97,7 +97,7 @@ contract KeeperOraclesTest is Test, EthHelpers { vm.prank(owner); _startSnapshotGas("KeeperOraclesTest_test_addOracle_maxOraclesExceeded"); vm.expectRevert(Errors.MaxOraclesExceeded.selector); - keeper.addOracle(makeAddr("oneMoreOracle")); + keeper.addOracle(makeAddr("OneMoreOracle")); _stopSnapshotGas(); } diff --git a/test/KeeperRewards.t.sol b/test/KeeperRewards.t.sol index 67b967d9..281368b6 100644 --- a/test/KeeperRewards.t.sol +++ b/test/KeeperRewards.t.sol @@ -30,8 +30,8 @@ contract KeeperRewardsTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); // Fund accounts vm.deal(admin, 100 ether); @@ -180,7 +180,7 @@ contract KeeperRewardsTest is Test, EthHelpers { // Make sure we add enough oracles while (contracts.keeper.totalOracles() < newMinOracles) { - address newOracle = makeAddr("newOracle"); + address newOracle = makeAddr("NewOracle"); vm.prank(keeperOwner); contracts.keeper.addOracle(newOracle); } diff --git a/test/KeeperValidators.t.sol b/test/KeeperValidators.t.sol index 2ccc9a46..665e9326 100644 --- a/test/KeeperValidators.t.sol +++ b/test/KeeperValidators.t.sol @@ -29,8 +29,8 @@ contract KeeperValidatorsTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); owner = contracts.keeper.owner(); // Fund accounts @@ -242,12 +242,13 @@ contract KeeperValidatorsTest is Test, EthHelpers { // Act & Assert: Call should fail due to invalid vault _startSnapshotGas("KeeperValidatorsTest_test_updateExitSignatures_invalidVault"); vm.expectRevert(Errors.InvalidVault.selector); - contracts.keeper.updateExitSignatures( - address(contracts.keeper), // Using Keeper as vault (invalid) - deadline, - exitSignaturesIpfsHash, - signatures - ); + contracts.keeper + .updateExitSignatures( + address(contracts.keeper), // Using Keeper as vault (invalid) + deadline, + exitSignaturesIpfsHash, + signatures + ); _stopSnapshotGas(); // Clean up @@ -407,7 +408,7 @@ contract KeeperValidatorsTest is Test, EthHelpers { uint256 currentMinOracles = contracts.keeper.validatorsMinOracles(); // Act & Assert: Call from non-owner should fail - address nonOwner = makeAddr("nonOwner"); + address nonOwner = makeAddr("NonOwner"); vm.prank(nonOwner); _startSnapshotGas("KeeperValidatorsTest_test_setValidatorsMinOracles_unauthorized"); vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", nonOwner)); diff --git a/test/OsToken.t.sol b/test/OsToken.t.sol index 3747def2..fb1e1184 100644 --- a/test/OsToken.t.sol +++ b/test/OsToken.t.sol @@ -28,9 +28,9 @@ contract OsTokenTest is Test, EthHelpers { osToken = OsToken(_osToken); // Setup test addresses - owner = makeAddr("owner"); - user = makeAddr("user"); - controller = makeAddr("controller"); + owner = makeAddr("Owner"); + user = makeAddr("User"); + controller = makeAddr("Controller"); // Fund user account for transactions vm.deal(user, 100 ether); @@ -287,7 +287,7 @@ contract OsTokenTest is Test, EthHelpers { uint256 amount = 50 ether; _mintOsToken(user, amount); - address recipient = makeAddr("recipient"); + address recipient = makeAddr("Recipient"); uint256 transferAmount = 10 ether; // Test transfer function @@ -301,7 +301,7 @@ contract OsTokenTest is Test, EthHelpers { assertEq(osToken.balanceOf(user), amount - transferAmount, "User's balance should be reduced"); // Test transferFrom function - address spender = makeAddr("spender"); + address spender = makeAddr("Spender"); uint256 approvalAmount = 20 ether; // Approve spender @@ -354,12 +354,12 @@ contract OsTokenTest is Test, EthHelpers { assertGt(osToken.balanceOf(user), initialOsTokenBalance, "osToken balance should increase"); // Due to conversion rates and fees, the exact amounts may not match perfectly - // We verify the amounts are close enough (within 5%) + // We verify the amounts are close enough (within 6%) uint256 actualIncrease = osToken.balanceOf(user) - initialOsTokenBalance; assertApproxEqRel( actualIncrease, mintedOsTokenShares, - 0.05e18, // 5% tolerance + 0.06e18, // 6% tolerance "Minted osToken amount too far from expected" ); } diff --git a/test/OsTokenConfig.t.sol b/test/OsTokenConfig.t.sol index a480f93f..91c6ccb6 100644 --- a/test/OsTokenConfig.t.sol +++ b/test/OsTokenConfig.t.sol @@ -32,10 +32,10 @@ contract OsTokenConfigTest is Test, EthHelpers { // Set up test accounts owner = Ownable(address(osTokenConfig)).owner(); - nonOwner = makeAddr("nonOwner"); - newRedeemer = makeAddr("newRedeemer"); - vault = makeAddr("vault"); - anotherVault = makeAddr("anotherVault"); + nonOwner = makeAddr("NonOwner"); + newRedeemer = makeAddr("NewRedeemer"); + vault = makeAddr("Vault"); + anotherVault = makeAddr("AnotherVault"); } // Test for initial contract state @@ -78,7 +78,7 @@ contract OsTokenConfigTest is Test, EthHelpers { // Make sure our new redeemer is different if (newRedeemer == currentRedeemer) { - newRedeemer = makeAddr("anotherNewRedeemer"); + newRedeemer = makeAddr("AnotherNewRedeemer"); } // Set up expected event @@ -360,13 +360,11 @@ contract OsTokenConfigTest is Test, EthHelpers { uint128 newLiqBonusPercent = 1.1e18; // 110% IOsTokenConfig.Config memory newDefaultConfig = IOsTokenConfig.Config({ - ltvPercent: newLtvPercent, - liqThresholdPercent: newLiqThresholdPercent, - liqBonusPercent: newLiqBonusPercent + ltvPercent: newLtvPercent, liqThresholdPercent: newLiqThresholdPercent, liqBonusPercent: newLiqBonusPercent }); // Test address that doesn't have a specific config - address randomVault = makeAddr("randomVault"); + address randomVault = makeAddr("RandomVault"); // Expect OsTokenConfigUpdated event vm.expectEmit(true, false, true, true); diff --git a/test/OsTokenFlashLoans.t.sol b/test/OsTokenFlashLoans.t.sol index f0c8f903..8432e76d 100644 --- a/test/OsTokenFlashLoans.t.sol +++ b/test/OsTokenFlashLoans.t.sol @@ -34,8 +34,8 @@ contract OsTokenFlashLoansTest is Test, EthHelpers { osToken = OsToken(_osToken); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); vm.deal(user, 100 ether); // Deploy mock recipient diff --git a/test/OwnMevEscrow.t.sol b/test/OwnMevEscrow.t.sol index 21739435..949bab50 100644 --- a/test/OwnMevEscrow.t.sol +++ b/test/OwnMevEscrow.t.sol @@ -22,8 +22,8 @@ contract OwnMevEscrowTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Setup test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); vm.deal(admin, 100 ether); vm.deal(user, 100 ether); @@ -161,7 +161,7 @@ contract OwnMevEscrowTest is Test, EthHelpers { // Test 7: Multiple senders function test_multipleSenders() public { // Create another user - address anotherUser = makeAddr("anotherUser"); + address anotherUser = makeAddr("AnotherUser"); vm.deal(anotherUser, 5 ether); // Send ETH from multiple accounts @@ -190,7 +190,7 @@ contract OwnMevEscrowTest is Test, EthHelpers { // Test 8: Create escrow directly function test_createEscrowDirectly() public { - address newVault = makeAddr("newVault"); + address newVault = makeAddr("NewVault"); // Create a new escrow with newVault as the vault address OwnMevEscrow newEscrow = new OwnMevEscrow(newVault); diff --git a/test/PriceFeed.t.sol b/test/PriceFeed.t.sol index 27ba27b0..be387022 100644 --- a/test/PriceFeed.t.sol +++ b/test/PriceFeed.t.sol @@ -26,7 +26,7 @@ contract PriceFeedTest is Test, EthHelpers { osTokenVaultController = contracts.osTokenVaultController; // Set up test accounts - user = makeAddr("user"); + user = makeAddr("User"); // Deploy the PriceFeed contract priceFeed = new PriceFeed(address(osTokenVaultController), "StakeWise osETH/ETH Price Feed"); diff --git a/test/SharedMevEscrow.t.sol b/test/SharedMevEscrow.t.sol index 2600f06a..e70667ff 100644 --- a/test/SharedMevEscrow.t.sol +++ b/test/SharedMevEscrow.t.sol @@ -19,8 +19,9 @@ contract MockVaultEthStaking { // Mock non-compliant vault that doesn't implement receiveFromMevEscrow contract MockNonCompliantVault { -// No receiveFromMevEscrow function -} + // No receiveFromMevEscrow function + + } contract SharedMevEscrowTest is Test { VaultsRegistry public vaultsRegistry; @@ -36,9 +37,9 @@ contract SharedMevEscrowTest is Test { function setUp() public { // Setup test accounts - owner = makeAddr("owner"); - nonRegisteredAccount = makeAddr("nonRegisteredAccount"); - mevSender = makeAddr("mevSender"); + owner = makeAddr("Owner"); + nonRegisteredAccount = makeAddr("NonRegisteredAccount"); + mevSender = makeAddr("MevSender"); // Fund accounts vm.deal(mevSender, 10 ether); diff --git a/test/VaultAdmin.t.sol b/test/VaultAdmin.t.sol index 749a035e..d79edea2 100644 --- a/test/VaultAdmin.t.sol +++ b/test/VaultAdmin.t.sol @@ -25,9 +25,9 @@ contract VaultAdminTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - nonAdmin = makeAddr("nonAdmin"); - newAdmin = makeAddr("newAdmin"); + admin = makeAddr("Admin"); + nonAdmin = makeAddr("NonAdmin"); + newAdmin = makeAddr("NewAdmin"); // Create a vault with admin as the admin bytes memory initParams = abi.encode( diff --git a/test/VaultEnterExit.t.sol b/test/VaultEnterExit.t.sol index 426258ea..ce54e585 100644 --- a/test/VaultEnterExit.t.sol +++ b/test/VaultEnterExit.t.sol @@ -26,10 +26,10 @@ contract VaultEnterExitTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - sender2 = makeAddr("sender2"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); + sender = makeAddr("Sender"); + sender2 = makeAddr("Sender2"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); // Fund accounts with ETH for testing vm.deal(sender, 100 ether); @@ -166,7 +166,7 @@ contract VaultEnterExitTest is Test, EthHelpers { function test_deposit_success_withReferrer() public { // Set up a referrer - address validReferrer = makeAddr("referrer"); + address validReferrer = makeAddr("Referrer"); // Record initial balances and state uint256 senderBalanceBefore = sender.balance; @@ -448,12 +448,13 @@ contract VaultEnterExitTest is Test, EthHelpers { function test_calculateExitedAssets_invalidPosition() public { // Try to calculate exited assets for a non-existent position _startSnapshotGas("VaultEnterExitTest_test_calculateExitedAssets_invalidPosition"); - (uint256 leftTickets, uint256 exitedTickets, uint256 exitedAssets) = vault.calculateExitedAssets( - receiver, - 999, // Non-existent position ticket - vm.getBlockTimestamp(), - 0 - ); + (uint256 leftTickets, uint256 exitedTickets, uint256 exitedAssets) = + vault.calculateExitedAssets( + receiver, + 999, // Non-existent position ticket + vm.getBlockTimestamp(), + 0 + ); _stopSnapshotGas(); assertEq(leftTickets, 0, "Left tickets should be 0 for non-existent position"); @@ -641,7 +642,7 @@ contract VaultEnterExitTest is Test, EthHelpers { } function test_enterExitQueue_afterValidatorExit() public { - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); diff --git a/test/VaultEthStaking.t.sol b/test/VaultEthStaking.t.sol index 1b6f0306..da8d8468 100644 --- a/test/VaultEthStaking.t.sol +++ b/test/VaultEthStaking.t.sol @@ -29,11 +29,11 @@ contract VaultEthStakingTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - referrer = makeAddr("referrer"); - validatorsManager = makeAddr("validatorsManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + referrer = makeAddr("Referrer"); + validatorsManager = makeAddr("ValidatorsManager"); // Fund accounts for testing vm.deal(sender, 100 ether); @@ -61,7 +61,7 @@ contract VaultEthStakingTest is Test, EthHelpers { function test_invalidSecurityDeposit() public { // Security deposit amount is defined as 1e9 (1 Gwei) in the EthHelpers contract // Create a new admin address for this test - address newAdmin = makeAddr("newAdmin"); + address newAdmin = makeAddr("NewAdmin"); vm.deal(newAdmin, 1 ether); // Get the factory for creating vaults diff --git a/test/VaultFee.t.sol b/test/VaultFee.t.sol index 656a5fe7..5540e737 100644 --- a/test/VaultFee.t.sol +++ b/test/VaultFee.t.sol @@ -28,10 +28,10 @@ contract VaultFeeTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); - feeRecipient = makeAddr("feeRecipient"); - newFeeRecipient = makeAddr("newFeeRecipient"); + admin = makeAddr("Admin"); + user = makeAddr("User"); + feeRecipient = makeAddr("FeeRecipient"); + newFeeRecipient = makeAddr("NewFeeRecipient"); // Fund accounts with ETH for testing vm.deal(admin, 100 ether); @@ -174,13 +174,13 @@ contract VaultFeeTest is Test, EthHelpers { } function test_setFeePercent_tooSoon() public { - // First set fee percentage - uint16 firstFeePercent = 500; // 5% + // First set fee percentage (use a valid increase from initial) + uint16 firstFeePercent = initialFeePercent + 1; vm.prank(admin); vault.setFeePercent(firstFeePercent); // Then try to set again too soon (before feeChangeDelay have passed) - uint16 secondFeePercent = 600; // 6% + uint16 secondFeePercent = firstFeePercent + 1; vm.prank(admin); _startSnapshotGas("VaultFeeTest_test_setFeePercent_tooSoon"); vm.expectRevert(Errors.TooEarlyUpdate.selector); @@ -198,16 +198,16 @@ contract VaultFeeTest is Test, EthHelpers { } function test_setFeePercent_maxIncrease() public { - // First set fee percentage - uint16 firstFeePercent = 500; // 5% + // First set fee percentage to a known value (use a valid increase from initial) + uint16 firstFeePercent = initialFeePercent + 1; vm.prank(admin); vault.setFeePercent(firstFeePercent); // Wait for delay period vm.warp(vm.getBlockTimestamp() + feeChangeDelay + 1); - // Try to increase fee by more than 20% - uint16 invalidIncrease = 700; // 7% (more than 20% increase from 5%) + // Try to increase fee by more than 20% (more than 120% of current) + uint16 invalidIncrease = uint16((uint256(firstFeePercent) * 121) / 100); // ~21% increase vm.prank(admin); _startSnapshotGas("VaultFeeTest_test_setFeePercent_maxIncrease"); vm.expectRevert(Errors.InvalidFeePercent.selector); @@ -217,8 +217,8 @@ contract VaultFeeTest is Test, EthHelpers { // Fee percentage should remain at the first update assertEq(vault.feePercent(), firstFeePercent, "Fee percent should not change"); - // Try a valid increase (below 20%) - uint16 validIncrease = 600; // 6% (20% increase from 5%) + // Try a valid increase (at most 20%) + uint16 validIncrease = uint16((uint256(firstFeePercent) * 120) / 100); // exactly 20% increase vm.prank(admin); vault.setFeePercent(validIncrease); assertEq(vault.feePercent(), validIncrease, "Fee percent should update with valid increase"); @@ -233,7 +233,7 @@ contract VaultFeeTest is Test, EthHelpers { // Test setting fee percentage without harvesting vm.warp(vm.getBlockTimestamp() + feeChangeDelay + 1); - uint16 newFeePercent = 500; // 5% + uint16 newFeePercent = initialFeePercent + 1; // valid increase vm.prank(admin); _startSnapshotGas("VaultFeeTest_test_setFeePercent_requiresHarvest"); vm.expectRevert(Errors.NotHarvested.selector); diff --git a/test/VaultOsToken.t.sol b/test/VaultOsToken.t.sol index 4eda742e..424d2ff7 100644 --- a/test/VaultOsToken.t.sol +++ b/test/VaultOsToken.t.sol @@ -43,10 +43,10 @@ contract VaultOsTokenTest is Test, EthHelpers { osTokenConfig = contracts.osTokenConfig; // Set up test accounts - owner = makeAddr("owner"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - referrer = makeAddr("referrer"); + owner = makeAddr("Owner"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + referrer = makeAddr("Referrer"); // Fund accounts for testing vm.deal(owner, 100 ether); @@ -349,8 +349,8 @@ contract VaultOsTokenTest is Test, EthHelpers { // Test minting to different receivers function test_mintOsToken_multipleReceivers() public { - address receiver1 = makeAddr("receiver1"); - address receiver2 = makeAddr("receiver2"); + address receiver1 = makeAddr("Receiver1"); + address receiver2 = makeAddr("Receiver2"); uint256 mintAmount = contracts.osTokenVaultController.convertToShares(1 ether); @@ -469,7 +469,7 @@ contract VaultOsTokenTest is Test, EthHelpers { // Test burning with non-existent position function test_burnOsToken_invalidPosition() public { // Use a different address that has no position - address nonPositionHolder = makeAddr("nonPositionHolder"); + address nonPositionHolder = makeAddr("NonPositionHolder"); uint256 mintAmount = contracts.osTokenVaultController.convertToShares(1 ether); vm.prank(owner); @@ -620,7 +620,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vault.mintOsToken(owner, mintAmount, referrer); // Try to redeem as non-redeemer - address nonRedeemer = makeAddr("nonRedeemer"); + address nonRedeemer = makeAddr("NonRedeemer"); vm.prank(nonRedeemer); _startSnapshotGas("VaultOsTokenTest_test_redeemOsToken_onlyRedeemer"); vm.expectRevert(Errors.AccessDenied.selector); @@ -671,7 +671,7 @@ contract VaultOsTokenTest is Test, EthHelpers { // Test redemption with non-existent position function test_redeemOsToken_nonExistentPosition() public { // Try to redeem from an address with no position - address noPositionAddr = makeAddr("noPosition"); + address noPositionAddr = makeAddr("NoPosition"); address redeemer = osTokenConfig.redeemer(); vm.prank(redeemer); @@ -890,7 +890,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vault.enterExitQueue(exitAmount, owner); // Try to liquidate - will fail because health factor not below threshold yet - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); vm.prank(liquidator); vm.expectRevert(Errors.InvalidHealthFactor.selector); vault.liquidateOsToken(maxOsTokenShares / 2, owner, liquidator); @@ -931,7 +931,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vault.updateState(harvestParams); // Verify the position is now liquidatable - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); uint256 liquidatorInitialBalance = liquidator.balance; _mintOsToken(liquidator, osTokenShares); @@ -985,7 +985,7 @@ contract VaultOsTokenTest is Test, EthHelpers { uint256 expectedAssets = (normalAssets * config.liqBonusPercent) / 1e18; // Prepare liquidator - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); uint256 liquidatorInitialBalance = liquidator.balance; _mintOsToken(liquidator, liquidationAmount); @@ -1020,7 +1020,7 @@ contract VaultOsTokenTest is Test, EthHelpers { // Test that liquidation is disabled when configured function test_liquidateOsToken_liquidationDisabled() public { // Create a vault with disabled liquidations - address adminWithDisabledLiq = makeAddr("adminWithDisabledLiq"); + address adminWithDisabledLiq = makeAddr("AdminWithDisabledLiq"); vm.deal(adminWithDisabledLiq, 100 ether); bytes memory initParams = abi.encode( IEthVault.EthVaultInitParams({ @@ -1044,7 +1044,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vm.stopPrank(); // Deposit to vault and collateralize - address vaultOwner = makeAddr("vaultOwner"); + address vaultOwner = makeAddr("VaultOwner"); vm.deal(vaultOwner, 100 ether); _depositToVault(address(vaultWithDisabledLiq), 50 ether, vaultOwner, vaultOwner); _collateralizeEthVault(address(vaultWithDisabledLiq)); @@ -1060,7 +1060,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vaultWithDisabledLiq.updateState(harvestParams); // Prepare liquidator - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); uint256 liquidationAmount = osTokenShares / 4; _mintOsToken(liquidator, liquidationAmount); @@ -1107,7 +1107,7 @@ contract VaultOsTokenTest is Test, EthHelpers { uint256 initialPosition = vault.osTokenPositions(owner); // Prepare for partial liquidation - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); uint256 liquidationAmount = osTokenShares / 3; _mintOsToken(liquidator, liquidationAmount); @@ -1179,7 +1179,7 @@ contract VaultOsTokenTest is Test, EthHelpers { vault.updateState(harvestParams); // Verify the position is now liquidatable - address liquidator = makeAddr("liquidator"); + address liquidator = makeAddr("Liquidator"); _mintOsToken(liquidator, osTokenShares); // remove withdrawable assets @@ -1235,9 +1235,10 @@ contract VaultOsTokenTest is Test, EthHelpers { // Process the exited assets _startSnapshotGas("VaultOsTokenTest_test_transferOsTokenPositionToEscrow_process"); - contracts.osTokenVaultEscrow.processExitedAssets( - address(vault), exitPositionTicket, timestamp, uint256(vault.getExitQueueIndex(exitPositionTicket)) - ); + contracts.osTokenVaultEscrow + .processExitedAssets( + address(vault), exitPositionTicket, timestamp, uint256(vault.getExitQueueIndex(exitPositionTicket)) + ); _stopSnapshotGas(); // Record user's ETH balance before claiming diff --git a/test/VaultState.t.sol b/test/VaultState.t.sol index 568bc13a..53fa69a3 100644 --- a/test/VaultState.t.sol +++ b/test/VaultState.t.sol @@ -25,10 +25,10 @@ contract VaultStateTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Setup test accounts - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - user2 = makeAddr("user2"); - admin = makeAddr("admin"); + owner = makeAddr("Owner"); + user1 = makeAddr("User1"); + user2 = makeAddr("User2"); + admin = makeAddr("Admin"); // Fund accounts vm.deal(owner, 100 ether); @@ -74,7 +74,7 @@ contract VaultStateTest is Test, EthHelpers { uint256 convertedShares = vault.convertToShares(originalAssets); uint256 convertedBackAssets = vault.convertToAssets(convertedShares); assertApproxEqAbs( - convertedBackAssets, originalAssets, 1, "Round-trip conversion should approximately preserve value" + convertedBackAssets, originalAssets, 2, "Round-trip conversion should approximately preserve value" ); } @@ -246,8 +246,8 @@ contract VaultStateTest is Test, EthHelpers { // Enter exit queue vm.prank(owner); - uint256 timestamp = vm.getBlockTimestamp(); uint256 positionTicket = vault.enterExitQueue(exitShares, owner); + uint256 timestamp = vm.getBlockTimestamp(); // Verify share reduction uint256 ownerSharesAfter = vault.getShares(owner); diff --git a/test/VaultSubVaults.t.sol b/test/VaultSubVaults.t.sol index a02da081..088a81fb 100644 --- a/test/VaultSubVaults.t.sol +++ b/test/VaultSubVaults.t.sol @@ -10,8 +10,9 @@ 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/custom/EthMetaVault.sol"; +import {EthMetaVault} from "../contracts/vaults/ethereum/EthMetaVault.sol"; import {BalancedCurator} from "../contracts/curators/BalancedCurator.sol"; import {CuratorsRegistry} from "../contracts/curators/CuratorsRegistry.sol"; import {EthHelpers} from "./helpers/EthHelpers.sol"; @@ -40,7 +41,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up accounts - admin = makeAddr("admin"); + admin = makeAddr("Admin"); vm.deal(admin, 100 ether); // Create a curator @@ -51,7 +52,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -60,6 +61,12 @@ contract VaultSubVaultsTest is Test, EthHelpers { ); metaVault = EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + // Get existing sub vaults (if any) from fork vault + address[] memory currentSubVaults = metaVault.getSubVaults(); + for (uint256 i = 0; i < currentSubVaults.length; i++) { + subVaults.push(currentSubVaults[i]); + } + // Deploy and add sub vaults for (uint256 i = 0; i < 3; i++) { address subVault = _createSubVault(admin); @@ -77,8 +84,8 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_setSubVaultsCurator_notAdmin() public { // Setup - address nonAdmin = makeAddr("nonAdmin"); - address newCurator = makeAddr("newCurator"); + address nonAdmin = makeAddr("NonAdmin"); + address newCurator = makeAddr("NewCurator"); // Register the new curator in the curators registry vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); @@ -109,7 +116,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_setSubVaultsCurator_notRegisteredCurator() public { // Setup: Create a new curator address that is not registered - address unregisteredCurator = makeAddr("unregisteredCurator"); + address unregisteredCurator = makeAddr("UnregisteredCurator"); // Action & Assert: Expect revert when trying to set an unregistered curator vm.prank(admin); @@ -119,7 +126,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_setSubVaultsCurator_success() public { // Setup: Create and register a new curator - address newCurator = makeAddr("newCurator"); + address newCurator = makeAddr("NewCurator"); vm.prank(CuratorsRegistry(_curatorsRegistry).owner()); CuratorsRegistry(_curatorsRegistry).addCurator(newCurator); @@ -147,7 +154,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); // Setup: Create a non-admin user - address nonAdmin = makeAddr("nonAdmin"); + address nonAdmin = makeAddr("NonAdmin"); // Action & Assert: Non-admin cannot add a sub vault vm.prank(nonAdmin); @@ -171,7 +178,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_addSubVault_notRegisteredVault() public { // Setup: Create an address that's not registered as a vault - address fakeVault = makeAddr("fakeVault"); + address fakeVault = makeAddr("FakeVault"); // Action & Assert: Cannot add non-registered vault vm.prank(admin); @@ -190,8 +197,13 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_addSubVault_moreThanMaxSubVaults() public { - // We already have 3 sub vaults from setUp, so we need to add 47 more to reach the max of 50 - for (uint256 i = 0; i < 47; i++) { + // Get current sub vault count + uint256 currentCount = metaVault.getSubVaults().length; + uint256 maxSubVaults = 50; + + // Add sub vaults until we reach the max + uint256 toAdd = maxSubVaults - currentCount; + for (uint256 i = 0; i < toAdd; i++) { // Create and collateralize a new sub vault address newSubVault = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); @@ -284,10 +296,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { metaVault.addSubVault(newSubVault); } - function test_addSubVault_firstSubVault() internal { + function test_addSubVault_firstSubVault() public { // create new meta vault bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -295,7 +307,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { }) ); EthMetaVault newMetaVault = - EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); // create new sub vault address subVault = _createSubVault(admin); @@ -361,7 +373,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { address subVaultToEject = subVaults[0]; // Setup: Create a non-admin user - address nonAdmin = makeAddr("nonAdmin"); + address nonAdmin = makeAddr("NonAdmin"); // Action & Assert: Non-admin cannot eject a sub vault vm.prank(nonAdmin); @@ -389,18 +401,40 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_ejectSubVault_singleSubVaultLeft() public { - // eject all the vaults until the last one - for (uint256 i = 0; i < 2; i++) { - vm.prank(admin); - metaVault.ejectSubVault(subVaults[i]); - } - subVaults = metaVault.getSubVaults(); - assertEq(subVaults.length, 1, "Should have 1 sub vault left"); + // Create a new meta vault with empty sub vaults to allow ejecting all but one + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault newMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // 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); + + address subVault2 = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault2); + vm.prank(admin); + newMetaVault.addSubVault(subVault2); + + // Eject the first sub vault (empty, so it's immediately removed) + vm.prank(admin); + newMetaVault.ejectSubVault(subVault1); + + address[] memory remainingSubVaults = newMetaVault.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); - metaVault.ejectSubVault(subVaults[0]); + newMetaVault.ejectSubVault(subVault2); } function test_ejectSubVault_notInSubVaults() public { @@ -415,42 +449,67 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_ejectSubVault_emptySubVault() public { - // Setup: Get a sub vaults to eject - address subVault1ToEject = subVaults[0]; - address subVault2ToEject = subVaults[1]; + // Create a new meta vault with empty sub vaults (no staked shares) + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault newMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); - // Get sub vault count before ejection - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + // 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); + + address subVault2 = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault2); + vm.prank(admin); + newMetaVault.addSubVault(subVault2); + + address subVault3 = _createSubVault(admin); + _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), subVault3); + vm.prank(admin); + newMetaVault.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"); // Start gas measurement _startSnapshotGas("test_ejectSubVault_emptySubVault"); // Expect SubVaultEjected event vm.expectEmit(true, true, false, false); - emit IVaultSubVaults.SubVaultEjected(admin, subVault1ToEject); + emit IVaultSubVaults.SubVaultEjected(admin, subVault1); // Action: Eject the sub vault vm.prank(admin); - metaVault.ejectSubVault(subVault1ToEject); + newMetaVault.ejectSubVault(subVault1); // Stop gas measurement _stopSnapshotGas(); // Assert: Verify the sub vault was removed from the list - address[] memory subVaultsAfter = metaVault.getSubVaults(); - assertEq(subVaultsAfter.length, subVaultsCountBefore - 1, "Sub vault should be removed"); + address[] memory subVaultsAfter = newMetaVault.getSubVaults(); + assertEq(subVaultsAfter.length, 2, "Should have 2 sub vaults left"); // Expect SubVaultEjected event vm.expectEmit(true, true, false, false); - emit IVaultSubVaults.SubVaultEjected(admin, subVault2ToEject); + emit IVaultSubVaults.SubVaultEjected(admin, subVault2); // Can remove another sub vault vm.prank(admin); - metaVault.ejectSubVault(subVault2ToEject); + newMetaVault.ejectSubVault(subVault2); // Assert: Verify the sub vault was removed from the list - subVaultsAfter = metaVault.getSubVaults(); - assertEq(subVaultsAfter.length, subVaultsCountBefore - 2, "Sub vault should be removed"); + subVaultsAfter = newMetaVault.getSubVaults(); + assertEq(subVaultsAfter.length, 1, "Should have 1 sub vault left"); } function test_ejectSubVault_subVaultWithShares() public { @@ -541,7 +600,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_depositToSubVaults_emptySubVaults() public { // Setup: Create a new meta vault bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -549,7 +608,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { }) ); EthMetaVault newMetaVault = - EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); // Action & Assert: Expect revert when trying to deposit to empty sub vaults vm.prank(admin); @@ -584,24 +643,21 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_singleSubVault"); - // Expect the Deposited event - vm.expectEmit(true, true, true, true, depositSubVault); - emit IVaultEnterExit.Deposited(address(metaVault), address(metaVault), availableAssets, newShares, address(0)); - // Action: Deposit to sub vaults metaVault.depositToSubVaults(); - // check withdrawable assets empty - assertApproxEqAbs(metaVault.withdrawableAssets(), 0, 2, "Withdrawable assets should be 0"); - // Stop gas measurement _stopSnapshotGas(); - // Assert: Verify the sub vault received staked shares + // check withdrawable assets empty + 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]); - assertEq( + assertApproxEqAbs( finalState.stakedShares, initialState.stakedShares + newShares, + 1, "Sub vault should have received staked shares" ); } @@ -610,46 +666,15 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Setup: Get initial state of all sub vaults uint256 subVaultCount = subVaults.length; IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaultCount); + uint256 initialTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { initialStates[i] = metaVault.subVaultsStates(subVaults[i]); - } - - // Calculate available assets and expected distribution - uint256 availableAssets = metaVault.withdrawableAssets(); - uint256 assetsPerVault = availableAssets / subVaultCount; - uint256 dust = availableAssets % subVaultCount; - - // Calculate expected new shares for each vault - uint256[] memory expectedNewShares = new uint256[](subVaultCount); - for (uint256 i = 0; i < subVaultCount; i++) { - if (i == 0) { - // The first vault gets the dust if available - expectedNewShares[i] = IEthVault(subVaults[i]).convertToShares(assetsPerVault + dust); - } else { - // Other vaults get equal shares - expectedNewShares[i] = IEthVault(subVaults[i]).convertToShares(assetsPerVault); - } + initialTotalStaked += initialStates[i].stakedShares; } // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_depositToSubVaults_multipleSubVaults"); - // Expect Deposited events for each sub vault - for (uint256 i = 0; i < subVaultCount; i++) { - vm.expectEmit(true, true, true, true, subVaults[i]); - if (i == 0) { - // The first vault gets the dust if available - emit IVaultEnterExit.Deposited( - address(metaVault), address(metaVault), assetsPerVault + dust, expectedNewShares[i], address(0) - ); - } else { - // Other vaults get equal shares - emit IVaultEnterExit.Deposited( - address(metaVault), address(metaVault), assetsPerVault, expectedNewShares[i], address(0) - ); - } - } - // Action: Deposit to sub vaults metaVault.depositToSubVaults(); @@ -659,15 +684,18 @@ contract VaultSubVaultsTest is Test, EthHelpers { // check withdrawable assets empty assertApproxEqAbs(metaVault.withdrawableAssets(), 0, 2, "Withdrawable assets should be 0"); - // Assert: Verify all sub vaults received the expected staked shares + // 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]); - assertEq( - finalState.stakedShares, - initialStates[i].stakedShares + expectedNewShares[i], - "Sub vault should have received expected staked shares" + assertGe( + finalState.stakedShares, initialStates[i].stakedShares, "Sub vault staked shares should not decrease" ); + finalTotalStaked += finalState.stakedShares; } + + // Total staked should have increased by approximately the available assets worth of shares + assertGt(finalTotalStaked, initialTotalStaked, "Total staked shares should have increased"); } function test_depositToSubVaults_ejectingSubVault() public { @@ -677,40 +705,18 @@ contract VaultSubVaultsTest is Test, EthHelpers { vm.deal(address(this), 10 ether); metaVault.deposit{value: 10 ether}(address(this), address(0)); - address ejectingSubVault = subVaults[2]; + // Use the last sub vault to eject (guaranteed to exist) + address ejectingSubVault = subVaults[subVaults.length - 1]; vm.prank(metaVault.admin()); metaVault.ejectSubVault(ejectingSubVault); // Setup: Get initial state of all sub vaults uint256 subVaultCount = subVaults.length; IVaultSubVaults.SubVaultState[] memory initialStates = new IVaultSubVaults.SubVaultState[](subVaultCount); + uint256 initialTotalStaked = 0; for (uint256 i = 0; i < subVaultCount; i++) { initialStates[i] = metaVault.subVaultsStates(subVaults[i]); - } - - // Calculate available assets and expected distribution - uint256 availableAssets = metaVault.withdrawableAssets(); - uint256 assetsPerVault = availableAssets / (subVaultCount - 1); - - // Calculate expected new shares for each vault - uint256[] memory expectedNewShares = new uint256[](subVaultCount); - for (uint256 i = 0; i < subVaultCount; i++) { - if (ejectingSubVault == subVaults[i]) { - expectedNewShares[i] = 0; - } else { - expectedNewShares[i] = IEthVault(subVaults[i]).convertToShares(assetsPerVault); - } - } - - // Expect Deposited events for each sub vault - for (uint256 i = 0; i < subVaultCount; i++) { - if (ejectingSubVault == subVaults[i]) { - continue; - } - vm.expectEmit(true, true, true, true, subVaults[i]); - emit IVaultEnterExit.Deposited( - address(metaVault), address(metaVault), assetsPerVault, expectedNewShares[i], address(0) - ); + initialTotalStaked += initialStates[i].stakedShares; } _startSnapshotGas("test_depositToSubVaults_ejectingSubVault"); @@ -723,24 +729,33 @@ contract VaultSubVaultsTest is Test, EthHelpers { // check withdrawable assets empty assertApproxEqAbs(metaVault.withdrawableAssets(), 0, 2, "Withdrawable assets should be 0"); - // Assert: Verify all sub vaults received the expected staked shares + // Assert: Verify ejecting sub vault received no new staked shares + IVaultSubVaults.SubVaultState memory ejectingFinalState = metaVault.subVaultsStates(ejectingSubVault); + assertEq( + ejectingFinalState.stakedShares, + initialStates[subVaults.length - 1].stakedShares, + "Ejecting sub vault should not have received new staked shares" + ); + + // 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]); - assertEq( - finalState.stakedShares, - initialStates[i].stakedShares + expectedNewShares[i], - "Sub vault should have received expected staked shares" - ); + finalTotalStaked += finalState.stakedShares; } + assertGt(finalTotalStaked, initialTotalStaked, "Total staked shares should have increased"); } function test_depositToSubVaults_maxVaults() public { - // Create and add the maximum number of sub vaults (50) + // Get current sub vaults and add more to reach maximum (50) + address[] memory currentSubVaults = metaVault.getSubVaults(); + uint256 currentCount = currentSubVaults.length; + address[] memory maxSubVaults = new address[](50); - maxSubVaults[0] = subVaults[0]; - maxSubVaults[1] = subVaults[1]; - maxSubVaults[2] = subVaults[2]; - for (uint256 i = 3; i < 50; i++) { + for (uint256 i = 0; i < currentCount; i++) { + maxSubVaults[i] = currentSubVaults[i]; + } + for (uint256 i = currentCount; i < 50; i++) { address newSubVault = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); @@ -750,7 +765,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { } // Verify we have exactly 50 sub vaults - address[] memory currentSubVaults = metaVault.getSubVaults(); + currentSubVaults = metaVault.getSubVaults(); assertEq(currentSubVaults.length, 50, "Should have exactly 50 sub vaults"); // Get initial state of all sub vaults @@ -812,7 +827,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( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -820,7 +835,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { }) ); EthMetaVault newMetaVault = - EthMetaVault(payable(_getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false))); + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); // Expect revert when trying to update state with no sub vaults vm.expectRevert(Errors.EmptySubVaults.selector); @@ -894,194 +909,216 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_updateState_unprocessedSubVaultExit() public { - metaVault.depositToSubVaults(); + // Create a new meta vault to have precise control over state + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault newMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // 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]); + } + + // 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(); // user enters exit queue - metaVault.enterExitQueue(metaVault.getShares(address(this)), address(this)); + newMetaVault.enterExitQueue(newMetaVault.getShares(address(this)), address(this)); // update nonce for sub vaults to trigger enter exit queue uint64 newNonce = contracts.keeper.rewardsNonce() + 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } - // update nonce for meta vault and trigger enter exit queue + // update nonce for meta vault and trigger enter exit queue - record logs to capture position tickets uint64 timestamp = uint64(vm.getBlockTimestamp()); - metaVault.updateState(_getEmptyHarvestParams()); + vm.recordLogs(); + newMetaVault.updateState(_getEmptyHarvestParams()); + ExitRequest[] memory exitPositions = _extractExitPositions(newSubVaults, vm.getRecordedLogs(), timestamp); // process exits for sub vaults IKeeperRewards.HarvestParams memory harvestParams; - for (uint256 i = 0; i < subVaults.length; i++) { - harvestParams = _setEthVaultReward(subVaults[i], 0, 0); - IVaultState(subVaults[i]).updateState(harvestParams); + for (uint256 i = 0; i < newSubVaults.length; i++) { + harvestParams = _setEthVaultReward(newSubVaults[i], 0, 0); + IVaultState(newSubVaults[i]).updateState(harvestParams); } - // set up exit requests for sub vaults + // set up exit requests for sub vaults using captured position tickets IVaultSubVaults.SubVaultExitRequest[] memory exitRequests = - new IVaultSubVaults.SubVaultExitRequest[](subVaults.length); - for (uint256 i = 0; i < subVaults.length; i++) { + new IVaultSubVaults.SubVaultExitRequest[](exitPositions.length); + for (uint256 i = 0; i < exitPositions.length; i++) { exitRequests[i] = IVaultSubVaults.SubVaultExitRequest({ - vault: subVaults[i], - exitQueueIndex: uint256(IVaultEnterExit(subVaults[i]).getExitQueueIndex(0)), - timestamp: timestamp + vault: exitPositions[i].vault, + exitQueueIndex: uint256( + IVaultEnterExit(exitPositions[i].vault).getExitQueueIndex(exitPositions[i].positionTicket) + ), + timestamp: exitPositions[i].timestamp }); } // update nonce for sub vaults newNonce += 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } // try to update meta vault state with unclaimed exit positions vm.expectRevert(Errors.UnclaimedAssets.selector); - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); // claim exited assets vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - metaVault.claimSubVaultsExitedAssets(exitRequests); + newMetaVault.claimSubVaultsExitedAssets(exitRequests); // succeeds - uint256 feeRecipientShares = metaVault.getShares(metaVault.feeRecipient()); - uint256 securityDepositShares = metaVault.getShares(address(metaVault)); + uint256 feeRecipientShares = newMetaVault.getShares(newMetaVault.feeRecipient()); + uint256 securityDepositShares = newMetaVault.getShares(address(newMetaVault)); uint256 reservedShares = feeRecipientShares + securityDepositShares; - uint256 reservedAssets = metaVault.convertToAssets(reservedShares); + uint256 reservedAssets = newMetaVault.convertToAssets(reservedShares); _startSnapshotGas("test_updateState_unprocessedSubVaultExit"); - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); _stopSnapshotGas(); - assertApproxEqAbs(metaVault.totalAssets(), reservedAssets, 2, "Total assets should be equal"); - assertApproxEqAbs(metaVault.totalShares(), reservedShares, 2, "Total shares should be equal"); + assertApproxEqAbs(newMetaVault.totalAssets(), reservedAssets, 2, "Total assets should be equal"); + assertApproxEqAbs(newMetaVault.totalShares(), reservedShares, 2, "Total shares should be equal"); } function test_updateState_newTotalAssets() public { - metaVault.depositToSubVaults(); + // Create new meta vault with new sub vaults for precise state control + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + IEthMetaVault newMetaVault = IEthMetaVault(_createVault(VaultType.EthMetaVault, admin, initParams, false)); - // enter exit queue for 1/3 of all user's shares - uint256 sharesToExit = metaVault.getShares(address(this)) / 3; - metaVault.enterExitQueue(sharesToExit, address(this)); + // 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]); + } - uint256 totalAssetsBefore = metaVault.totalAssets(); + // 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(); + + uint256 totalAssetsBefore = newMetaVault.totalAssets(); // set equal nonce for all the sub vaults and keeper uint64 newNonce = contracts.keeper.rewardsNonce() + 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } - assertTrue(metaVault.canUpdateState(), "Meta vault should be able to update state"); + assertTrue(newMetaVault.canUpdateState(), "Meta vault should be able to update state"); - // update nonce for meta vault and trigger enter exit queue - vm.recordLogs(); - metaVault.updateState(_getEmptyHarvestParams()); - ExitRequest[] memory exitRequests1 = - _extractExitPositions(subVaults, vm.getRecordedLogs(), uint64(vm.getBlockTimestamp())); - assertApproxEqAbs(metaVault.totalAssets(), totalAssetsBefore, 2, "Total assets should be equal before rewards"); + // update nonce for meta vault + newMetaVault.updateState(_getEmptyHarvestParams()); + assertApproxEqAbs( + newMetaVault.totalAssets(), totalAssetsBefore, 2, "Total assets should be equal before rewards" + ); // all vaults earn 1 eth rewards uint256 vaultReward = 1 ether; IKeeperRewards.HarvestParams memory harvestParams; - for (uint256 i = 0; i < subVaults.length; i++) { - vm.deal(subVaults[i], 0); - harvestParams = _setEthVaultReward(subVaults[i], int160(int256(vaultReward)), 0); - IVaultState(subVaults[i]).updateState(harvestParams); + for (uint256 i = 0; i < newSubVaults.length; i++) { + harvestParams = _setEthVaultReward(newSubVaults[i], int160(int256(vaultReward)), 0); + IVaultState(newSubVaults[i]).updateState(harvestParams); } // set equal nonce for all the sub vaults newNonce += 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } // check total assets updated - uint256 expectedTotalAssets = totalAssetsBefore + (vaultReward * subVaults.length); - expectedTotalAssets -= - (vaultReward * subVaults.length) * (_securityDeposit * subVaults.length) / totalAssetsBefore; - - // Expect the SubVaultsHarvested event - vm.expectEmit(false, false, false, false); - emit IVaultSubVaults.SubVaultsHarvested(int256(expectedTotalAssets - totalAssetsBefore)); + uint256 expectedTotalAssets = totalAssetsBefore + (vaultReward * newSubVaults.length); + expectedTotalAssets -= (vaultReward * newSubVaults.length) * (_securityDeposit * newSubVaults.length) + / totalAssetsBefore; _startSnapshotGas("test_updateState_newTotalAssets"); - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); _stopSnapshotGas(); - assertApproxEqAbs(metaVault.totalAssets(), expectedTotalAssets, 3, "Total assets should have been changed"); - expectedTotalAssets = metaVault.totalAssets(); + assertApproxEqAbs(newMetaVault.totalAssets(), expectedTotalAssets, 2, "Total assets should have been changed"); + expectedTotalAssets = newMetaVault.totalAssets(); // enter exit queue for all user's shares - metaVault.enterExitQueue(metaVault.getShares(address(this)), address(this)); + newMetaVault.enterExitQueue(newMetaVault.getShares(address(this)), address(this)); // set equal nonce for all the sub vaults newNonce += 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } // update nonce for meta vault and trigger enter exit queue vm.recordLogs(); - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); ExitRequest[] memory exitRequests2 = - _extractExitPositions(subVaults, vm.getRecordedLogs(), uint64(vm.getBlockTimestamp())); + _extractExitPositions(newSubVaults, vm.getRecordedLogs(), uint64(vm.getBlockTimestamp())); assertApproxEqAbs( - metaVault.totalAssets(), expectedTotalAssets, 2, "Total assets should be equal before rewards" + newMetaVault.totalAssets(), expectedTotalAssets, 2, "Total assets should be equal before rewards" ); // all queued assets are processed for the vaults - for (uint256 i = 0; i < subVaults.length; i++) { - vm.deal(subVaults[i], 12 ether); - harvestParams = _setEthVaultReward(subVaults[i], int160(int256(vaultReward)), 0); - IVaultState(subVaults[i]).updateState(harvestParams); + for (uint256 i = 0; i < newSubVaults.length; i++) { + vm.deal(newSubVaults[i], address(newSubVaults[i]).balance + 12 ether); + harvestParams = _setEthVaultReward(newSubVaults[i], int160(int256(vaultReward)), 0); + IVaultState(newSubVaults[i]).updateState(harvestParams); } assertEq( - metaVault.totalAssets(), expectedTotalAssets, "Total assets should not change after queued assets processed" + newMetaVault.totalAssets(), + expectedTotalAssets, + "Total assets should not change after queued assets processed" ); // set equal nonce for all the sub vaults newNonce += 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } - // must fail + // must fail due to unclaimed exit requests vm.expectRevert(Errors.UnclaimedAssets.selector); - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); // claim exited assets vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - IVaultSubVaults.SubVaultExitRequest[] memory claims1 = - new IVaultSubVaults.SubVaultExitRequest[](exitRequests1.length); - for (uint256 i = 0; i < exitRequests1.length; i++) { - claims1[i] = IVaultSubVaults.SubVaultExitRequest({ - vault: exitRequests1[i].vault, - exitQueueIndex: uint256( - IVaultEnterExit(exitRequests1[i].vault).getExitQueueIndex(exitRequests1[i].positionTicket) - ), - timestamp: exitRequests1[i].timestamp - }); - } - metaVault.claimSubVaultsExitedAssets(claims1); - assertEq( - metaVault.totalAssets(), - expectedTotalAssets, - "Total assets should not change after first batch exited assets claimed" - ); - - // update state fails as half of exit requests are still not processed - vm.expectRevert(Errors.UnclaimedAssets.selector); - metaVault.updateState(_getEmptyHarvestParams()); - - IVaultSubVaults.SubVaultExitRequest[] memory claims2 = + IVaultSubVaults.SubVaultExitRequest[] memory claims = new IVaultSubVaults.SubVaultExitRequest[](exitRequests2.length); for (uint256 i = 0; i < exitRequests2.length; i++) { - claims2[i] = IVaultSubVaults.SubVaultExitRequest({ + claims[i] = IVaultSubVaults.SubVaultExitRequest({ vault: exitRequests2[i].vault, exitQueueIndex: uint256( IVaultEnterExit(exitRequests2[i].vault).getExitQueueIndex(exitRequests2[i].positionTicket) @@ -1090,34 +1127,39 @@ contract VaultSubVaultsTest is Test, EthHelpers { }); } - metaVault.claimSubVaultsExitedAssets(claims2); + newMetaVault.claimSubVaultsExitedAssets(claims); assertEq( - metaVault.totalAssets(), + newMetaVault.totalAssets(), expectedTotalAssets, - "Total assets should not change after second batch exited assets claimed" + "Total assets should not change after exited assets claimed" ); // update state goes through - metaVault.updateState(_getEmptyHarvestParams()); + newMetaVault.updateState(_getEmptyHarvestParams()); // check all the assets processed - uint256 feeRecipientShares = metaVault.getShares(metaVault.feeRecipient()); - uint256 securityDepositShares = metaVault.getShares(address(metaVault)); + uint256 feeRecipientShares = newMetaVault.getShares(newMetaVault.feeRecipient()); + uint256 securityDepositShares = newMetaVault.getShares(address(newMetaVault)); uint256 reservedShares = feeRecipientShares + securityDepositShares; - uint256 reservedAssets = metaVault.convertToAssets(reservedShares); + uint256 reservedAssets = newMetaVault.convertToAssets(reservedShares); assertApproxEqAbs( - metaVault.totalAssets(), reservedAssets, 2, "Total assets should not change after all assets processed" + newMetaVault.totalAssets(), reservedAssets, 2, "Total assets should not change after all assets processed" + ); + assertApproxEqAbs( + newMetaVault.totalShares(), reservedShares, 1, "Total shares should be equal to reserved shares" ); - assertApproxEqAbs(metaVault.totalShares(), reservedShares, 1, "Total shares should be equal to reserved shares"); } function test_updateState_enterExitQueueMaxVaults() public { - // Create and add the maximum number of sub vaults (50) + // Get current sub vaults and add more to reach maximum (50) + address[] memory currentSubVaults = metaVault.getSubVaults(); + uint256 currentCount = currentSubVaults.length; + address[] memory maxSubVaults = new address[](50); - maxSubVaults[0] = subVaults[0]; - maxSubVaults[1] = subVaults[1]; - maxSubVaults[2] = subVaults[2]; - for (uint256 i = 3; i < 50; i++) { + for (uint256 i = 0; i < currentCount; i++) { + maxSubVaults[i] = currentSubVaults[i]; + } + for (uint256 i = currentCount; i < 50; i++) { address newSubVault = _createSubVault(admin); _collateralizeVault(address(contracts.keeper), address(contracts.validatorsRegistry), newSubVault); @@ -1127,7 +1169,7 @@ contract VaultSubVaultsTest is Test, EthHelpers { } // Verify we have exactly 50 sub vaults - address[] memory currentSubVaults = metaVault.getSubVaults(); + currentSubVaults = metaVault.getSubVaults(); assertEq(currentSubVaults.length, 50, "Should have exactly 50 sub vaults"); // Deposit assets to all sub vaults @@ -1218,27 +1260,52 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets() public { - // Deposit to sub vaults first - metaVault.depositToSubVaults(); + // Create a new meta vault to have precise control over state + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault newMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // 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]); + } + + // 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(); // Get a reference to a single sub vault we'll use for the test - address testSubVault = subVaults[0]; + address testSubVault = newSubVaults[0]; // User enters exit queue with all their shares - metaVault.enterExitQueue(metaVault.getShares(address(this)), address(this)); + newMetaVault.enterExitQueue(newMetaVault.getShares(address(this)), address(this)); // Update nonces for sub vaults to trigger exit queue processing uint64 newNonce = contracts.keeper.rewardsNonce() + 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } - // Update state to process the exit queue - metaVault.updateState(_getEmptyHarvestParams()); + // Update state to process the exit queue - record logs to capture position tickets uint64 timestamp = uint64(vm.getBlockTimestamp()); + vm.recordLogs(); + newMetaVault.updateState(_getEmptyHarvestParams()); + ExitRequest[] memory exitPositions = _extractExitPositions(newSubVaults, vm.getRecordedLogs(), timestamp); - IVaultSubVaults.SubVaultState memory stateBefore = metaVault.subVaultsStates(testSubVault); + IVaultSubVaults.SubVaultState memory stateBefore = newMetaVault.subVaultsStates(testSubVault); // Process the exit request but only provide small amount of funds uint256 processedAssets = 0.1 ether; @@ -1251,29 +1318,36 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Fast-forward time to allow claiming vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - // Create a single exit request to claim + // Find the exit position for testSubVault + uint256 testSubVaultPositionTicket; + for (uint256 i = 0; i < exitPositions.length; i++) { + if (exitPositions[i].vault == testSubVault) { + testSubVaultPositionTicket = exitPositions[i].positionTicket; + break; + } + } + + // Create a single exit request to claim using captured position ticket IVaultSubVaults.SubVaultExitRequest[] memory singleExitRequest = new IVaultSubVaults.SubVaultExitRequest[](1); - int256 exitQueueIndex = IVaultEnterExit(testSubVault).getExitQueueIndex(0); + int256 exitQueueIndex = IVaultEnterExit(testSubVault).getExitQueueIndex(testSubVaultPositionTicket); singleExitRequest[0] = IVaultSubVaults.SubVaultExitRequest({ - vault: testSubVault, - exitQueueIndex: uint256(exitQueueIndex), - timestamp: timestamp + vault: testSubVault, exitQueueIndex: uint256(exitQueueIndex), timestamp: timestamp }); - uint256 metaVaultBalanceBefore = address(metaVault).balance; + uint256 metaVaultBalanceBefore = address(newMetaVault).balance; // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_claimSubVaultsExitedAssets_partiallyClaimsExitedAssets"); // Claim the exited assets - metaVault.claimSubVaultsExitedAssets(singleExitRequest); + newMetaVault.claimSubVaultsExitedAssets(singleExitRequest); // Stop gas measurement _stopSnapshotGas(); // Verify balance after claiming - uint256 metaVaultBalanceAfter = address(metaVault).balance; + uint256 metaVaultBalanceAfter = address(newMetaVault).balance; // The meta vault should have received the assets assertGt(metaVaultBalanceAfter, metaVaultBalanceBefore, "Meta vault balance should increase"); @@ -1281,110 +1355,245 @@ contract VaultSubVaultsTest is Test, EthHelpers { // Check how much we actually received uint256 claimedAssets = metaVaultBalanceAfter - metaVaultBalanceBefore; - // Should be approximately half of the total needed assets + // Should be approximately equal to processed assets 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 = metaVault.subVaultsStates(testSubVault); + IVaultSubVaults.SubVaultState memory stateAfter = newMetaVault.subVaultsStates(testSubVault); assertLt(stateAfter.queuedShares, stateBefore.queuedShares, "Queued shares should decrease"); - assertGt(stateAfter.queuedShares, 0, "Queued shares should not be zero - only half was processed"); + assertGt(stateAfter.queuedShares, 0, "Queued shares should not be zero - only partial was processed"); } function test_claimSubVaultsExitedAssets_ejectingSubVault() public { - // Deposit to sub vaults first - metaVault.depositToSubVaults(); + // Create a new meta vault to have precise control over state + bytes memory initParams = abi.encode( + IMetaVault.MetaVaultInitParams({ + subVaultsCurator: curator, + capacity: 1000 ether, + feePercent: 1000, + metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" + }) + ); + EthMetaVault newMetaVault = + EthMetaVault(payable(_createVault(VaultType.EthMetaVault, admin, initParams, false))); + + // 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]); + } + + // 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(); // Choose a sub vault to eject - address ejectingSubVault = subVaults[0]; + address ejectingSubVault = newSubVaults[0]; - // Eject the sub vault + // Eject the sub vault - record logs to capture position ticket + vm.recordLogs(); vm.prank(admin); - metaVault.ejectSubVault(ejectingSubVault); + newMetaVault.ejectSubVault(ejectingSubVault); + uint64 ejectTimestamp = uint64(vm.getBlockTimestamp()); + address[] memory ejectingVaults = new address[](1); + ejectingVaults[0] = ejectingSubVault; + ExitRequest[] memory ejectPositions = + _extractExitPositions(ejectingVaults, vm.getRecordedLogs(), ejectTimestamp); // Verify the ejecting sub vault is set correctly - assertEq(metaVault.ejectingSubVault(), ejectingSubVault, "Ejecting sub vault should be set"); + assertEq(newMetaVault.ejectingSubVault(), ejectingSubVault, "Ejecting sub vault should be set"); // Verify the vault has moved from staked to queued shares - IVaultSubVaults.SubVaultState memory state = metaVault.subVaultsStates(ejectingSubVault); + IVaultSubVaults.SubVaultState memory state = newMetaVault.subVaultsStates(ejectingSubVault); assertEq(state.stakedShares, 0, "Staked shares should be zero after ejection"); assertGt(state.queuedShares, 0, "Queued shares should be positive after ejection"); // Now have a user enter the exit queue with all of their shares - metaVault.enterExitQueue(metaVault.getShares(address(this)), address(this)); + newMetaVault.enterExitQueue(newMetaVault.getShares(address(this)), address(this)); // Update nonces to process exit queue uint64 newNonce = contracts.keeper.rewardsNonce() + 1; _setKeeperRewardsNonce(newNonce); - for (uint256 i = 0; i < subVaults.length; i++) { - _setVaultRewardsNonce(subVaults[i], newNonce); + for (uint256 i = 0; i < newSubVaults.length; i++) { + _setVaultRewardsNonce(newSubVaults[i], newNonce); } // Start gas measurement _startSnapshotGas("VaultSubVaultsTest_test_claimSubVaultsExitedAssets_ejectionConsumesShares"); // Update state which should process the user's exit and consume ejecting vault's shares - metaVault.updateState(_getEmptyHarvestParams()); - uint64 timestamp = uint64(vm.getBlockTimestamp()); + newMetaVault.updateState(_getEmptyHarvestParams()); // Stop gas measurement _stopSnapshotGas(); // Process the exit for sub vaults - for (uint256 i = 0; i < subVaults.length; i++) { - IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(subVaults[i], 0, 0); - IVaultState(subVaults[i]).updateState(harvestParams); + for (uint256 i = 0; i < newSubVaults.length; i++) { + IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(newSubVaults[i], 0, 0); + IVaultState(newSubVaults[i]).updateState(harvestParams); } // Fast-forward time to allow claiming vm.warp(vm.getBlockTimestamp() + _exitingAssetsClaimDelay + 1); - // Create exit requests for claiming + // 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({ vault: ejectingSubVault, - exitQueueIndex: uint256(IVaultEnterExit(ejectingSubVault).getExitQueueIndex(0)), - timestamp: timestamp + exitQueueIndex: uint256( + IVaultEnterExit(ejectingSubVault).getExitQueueIndex(ejectPositions[0].positionTicket) + ), + timestamp: ejectTimestamp }); // Claim exited assets - metaVault.claimSubVaultsExitedAssets(claimRequests); + newMetaVault.claimSubVaultsExitedAssets(claimRequests); // Verify the ejecting sub vault is removed from the state - assertEq(metaVault.ejectingSubVault(), address(0), "Ejecting sub vault should be removed"); + assertEq(newMetaVault.ejectingSubVault(), address(0), "Ejecting sub vault should be removed"); assertEq( - metaVault.subVaultsStates(ejectingSubVault).stakedShares, + newMetaVault.subVaultsStates(ejectingSubVault).stakedShares, 0, "Ejecting sub vault should have zero staked shares after claim" ); assertEq( - metaVault.subVaultsStates(ejectingSubVault).queuedShares, + newMetaVault.subVaultsStates(ejectingSubVault).queuedShares, 0, "Ejecting sub vault should have zero queued shares after claim" ); } - function test_addSubVault_metaVaultAsSubVault_success() public { + function test_addSubVault_metaVaultAsSubVault_proposesMetaVault() public { // Setup: Create meta vault as sub vault - address metaSubVault = _setupMetaSubVault(admin); + address metaSubVault = _createMetaSubVault(admin); - // Get sub vault count before adding - uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + // Verify pendingMetaSubVault is empty + assertEq(metaVault.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); // Start gas measurement - _startSnapshotGas("VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_success"); + _startSnapshotGas("VaultSubVaultsTest_test_addSubVault_metaVaultAsSubVault_proposesMetaVault"); - // Expect the SubVaultAdded event - vm.expectEmit(true, true, false, true); - emit IVaultSubVaults.SubVaultAdded(admin, metaSubVault); + // Action: Add the meta vault as sub vault (should only propose, not add) + vm.prank(admin); + metaVault.addSubVault(metaSubVault); - // Action: Add the meta vault as sub vault + // Stop gas measurement + _stopSnapshotGas(); + + // Assert: Verify the meta vault is pending, not added + assertEq(metaVault.pendingMetaSubVault(), metaSubVault, "Meta vault should be pending"); + + // Verify the vault was NOT added to the sub vaults list yet + address[] memory subVaultsAfter = metaVault.getSubVaults(); + bool found = false; + for (uint256 i = 0; i < subVaultsAfter.length; i++) { + if (subVaultsAfter[i] == metaSubVault) { + found = true; + break; + } + } + assertFalse(found, "Meta vault should NOT be in sub vaults list yet"); + } + + function test_addSubVault_metaVaultAsSubVault_pendingAlreadyExists() public { + // Setup: Create two meta vaults as sub vaults + address metaSubVault1 = _createMetaSubVault(admin); + address metaSubVault2 = _createMetaSubVault(admin); + + // Propose the first meta sub vault + vm.prank(admin); + metaVault.addSubVault(metaSubVault1); + + // Verify pendingMetaSubVault is set + assertEq(metaVault.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); + } + + function test_acceptMetaSubVault_notVaultsRegistryOwner() public { + // Setup: Create and propose meta sub vault + address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); + metaVault.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); + + // Also test that admin cannot accept + vm.prank(admin); + vm.expectRevert(Errors.AccessDenied.selector); + metaVault.acceptMetaSubVault(metaSubVault); + } + + function test_acceptMetaSubVault_zeroAddress() public { + // Setup: Create and propose meta sub vault + address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); + metaVault.addSubVault(metaSubVault); + + // Action & Assert: Cannot accept zero address + vm.prank(contracts.vaultsRegistry.owner()); + vm.expectRevert(Errors.InvalidVault.selector); + metaVault.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); + + // Setup: Create another meta sub vault (not pending) + address otherMetaSubVault = _createMetaSubVault(admin); + + // Action & Assert: Cannot accept a vault that is not pending + vm.prank(contracts.vaultsRegistry.owner()); + vm.expectRevert(Errors.InvalidVault.selector); + metaVault.acceptMetaSubVault(otherMetaSubVault); + } + + function test_acceptMetaSubVault_success() public { + // Setup: Create meta vault as sub vault + address metaSubVault = _createMetaSubVault(admin); + + // Propose meta sub vault vm.prank(admin); metaVault.addSubVault(metaSubVault); + // Get sub vault count before accepting + uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + + // Expect the SubVaultAdded event + vm.expectEmit(true, true, false, true); + emit IVaultSubVaults.SubVaultAdded(contracts.vaultsRegistry.owner(), 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); + // 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(subVaultsAfter.length, subVaultsCountBefore + 1, "Sub vault count should increase by 1"); @@ -1398,17 +1607,133 @@ contract VaultSubVaultsTest is Test, EthHelpers { assertTrue(found, "Meta vault should be found in sub vaults list"); } + function test_rejectMetaSubVault_notAdminOrOwner() public { + // Setup: Create and propose meta sub vault + address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); + metaVault.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); + } + + function test_rejectMetaSubVault_zeroAddress() public { + // Setup: Create and propose meta sub vault + address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); + metaVault.addSubVault(metaSubVault); + + // Action & Assert: Cannot reject zero address + vm.prank(admin); + vm.expectRevert(Errors.InvalidVault.selector); + metaVault.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); + + // Setup: Create another meta sub vault (not pending) + address otherMetaSubVault = _createMetaSubVault(admin); + + // Action & Assert: Cannot reject a vault that is not pending + vm.prank(admin); + vm.expectRevert(Errors.InvalidVault.selector); + metaVault.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); + + // Get sub vault count before rejection + uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + + // Expect the MetaSubVaultRejected event + vm.expectEmit(true, true, false, true); + emit IVaultSubVaults.MetaSubVaultRejected(contracts.vaultsRegistry.owner(), 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); + + // 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(subVaultsAfter.length, subVaultsCountBefore, "Sub vault count should not change"); + + bool found = false; + for (uint256 i = 0; i < subVaultsAfter.length; i++) { + if (subVaultsAfter[i] == metaSubVault) { + found = true; + break; + } + } + assertFalse(found, "Meta vault should NOT be in sub vaults list"); + } + + function test_rejectMetaSubVault_byAdmin_success() public { + // Setup: Create and propose meta sub vault + address metaSubVault = _createMetaSubVault(admin); + vm.prank(admin); + metaVault.addSubVault(metaSubVault); + + // Get sub vault count before rejection + uint256 subVaultsCountBefore = metaVault.getSubVaults().length; + + // Expect the MetaSubVaultRejected event + vm.expectEmit(true, true, false, true); + emit IVaultSubVaults.MetaSubVaultRejected(admin, metaSubVault); + + // Start gas measurement + _startSnapshotGas("VaultSubVaultsTest_test_rejectMetaSubVault_byAdmin_success"); + + // Action: Reject meta sub vault by admin + vm.prank(admin); + metaVault.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(subVaultsAfter.length, subVaultsCountBefore, "Sub vault count should not change"); + + bool found = false; + for (uint256 i = 0; i < subVaultsAfter.length; i++) { + if (subVaultsAfter[i] == metaSubVault) { + found = true; + break; + } + } + assertFalse(found, "Meta vault should NOT be in sub vaults list"); + } + function test_addSubVault_metaVaultAsSubVault_notCollateralized() public { // Setup: Create meta vault but don't collateralize it bytes memory initParams = abi.encode( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: type(uint256).max, feePercent: 0, metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" }) ); - address unCollateralizedMetaVault = _getOrCreateVault(VaultType.EthMetaVault, admin, initParams, false); + address unCollateralizedMetaVault = _createVault(VaultType.EthMetaVault, admin, initParams, false); // Action & Assert: Cannot add non-collateralized meta vault vm.prank(admin); @@ -1417,25 +1742,27 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function test_addSubVault_metaVaultAsSubVault_notHarvested() public { - // Setup: Create and collateralize meta vault as sub vault - address metaSubVault = _setupMetaSubVault(admin); + // Setup: Create meta vault as sub vault (but don't add it yet) + address metaSubVault = _createMetaSubVault(admin); // Setup: Set different rewards nonce for the meta sub vault uint64 currentNonce = contracts.keeper.rewardsNonce(); uint64 differentNonce = currentNonce + 1; _setMetaVaultRewardsNonce(metaSubVault, uint128(differentNonce)); - // Action & Assert: Cannot add meta vault with different rewards nonce + // Propose meta sub vault (this should succeed) vm.prank(admin); - vm.expectRevert(Errors.NotHarvested.selector); metaVault.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); } function test_ejectSubVault_metaVaultAsSubVault_emptySubVault() public { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); // Get sub vault count before ejection uint256 subVaultsCountBefore = metaVault.getSubVaults().length; @@ -1471,8 +1798,6 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_ejectSubVault_metaVaultAsSubVault_withShares() public { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); // Deposit to main meta vault vm.deal(address(this), 5 ether); @@ -1522,8 +1847,6 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_depositToSubVaults_withMetaVaultSubVault() public { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); // Deposit to main meta vault vm.deal(address(this), 10 ether); @@ -1562,8 +1885,6 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_depositToSubVaults_nestedMetaVaultDepositsToItsSubVaults() public { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); // Deposit to main meta vault vm.deal(address(this), 20 ether); @@ -1591,8 +1912,6 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_withMetaVaultSubVault_success() public { // Setup: Add meta vault as sub vault and deposit address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); vm.deal(address(this), 5 ether); metaVault.deposit{value: 5 ether}(address(this), address(0)); @@ -1633,8 +1952,6 @@ contract VaultSubVaultsTest is Test, EthHelpers { function test_updateState_withMetaVaultSubVault_notHarvested() public { // Setup: Add meta vault as sub vault address metaSubVault = _setupMetaSubVault(admin); - vm.prank(admin); - metaVault.addSubVault(metaSubVault); // Setup: Make the meta sub vault appear not harvested by setting outdated nonce uint64 currentNonce = contracts.keeper.rewardsNonce(); @@ -1669,16 +1986,29 @@ contract VaultSubVaultsTest is Test, EthHelpers { } function _setupMetaSubVault(address _admin) internal returns (address metaSubVault) { + // Deploy meta vault that will be used as sub vault + metaSubVault = _createMetaSubVault(_admin); + + // Propose and accept the meta sub vault on the main metaVault + vm.prank(_admin); + metaVault.addSubVault(metaSubVault); + + // Accept the meta sub vault by VaultsRegistry owner + vm.prank(contracts.vaultsRegistry.owner()); + metaVault.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( - IEthMetaVault.EthMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: type(uint256).max, feePercent: 0, // 0% metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" }) ); - metaSubVault = _getOrCreateVault(VaultType.EthMetaVault, _admin, initParams, false); + metaSubVault = _createVault(VaultType.EthMetaVault, _admin, initParams, false); // Create regular vault as sub vaults of the meta sub vault address subVault = _createSubVault(_admin); @@ -1703,28 +2033,10 @@ contract VaultSubVaultsTest is Test, EthHelpers { EthMetaVault(payable(metaSubVault)).updateState(_getEmptyHarvestParams()); } - function _setVaultRewardsNonce(address vault, uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewards(address)").with_key(vault).depth( - 1 - ).checked_write(rewardsNonce); - } - function _setMetaVaultRewardsNonce(address vault, uint128 rewardsNonce) internal { stdstore.target(vault).sig("subVaultsRewardsNonce()").checked_write(rewardsNonce); } - function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewardsNonce()").checked_write( - rewardsNonce - ); - } - - function _getEmptyHarvestParams() internal pure returns (IKeeperRewards.HarvestParams memory) { - bytes32[] memory emptyProof; - return - IKeeperRewards.HarvestParams({rewardsRoot: bytes32(0), proof: emptyProof, reward: 0, unlockedMevReward: 0}); - } - function _extractExitPositions(address[] memory _subVaults, Vm.Log[] memory logs, uint64 timestamp) internal view diff --git a/test/VaultToken.t.sol b/test/VaultToken.t.sol index 29b5794e..ce9294c2 100644 --- a/test/VaultToken.t.sol +++ b/test/VaultToken.t.sol @@ -26,10 +26,10 @@ contract VaultTokenTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Setup test accounts - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - user2 = makeAddr("user2"); - admin = makeAddr("admin"); + owner = makeAddr("Owner"); + user1 = makeAddr("User1"); + user2 = makeAddr("User2"); + admin = makeAddr("Admin"); // Fund accounts vm.deal(owner, 100 ether); @@ -323,7 +323,7 @@ contract VaultTokenTest is Test, EthHelpers { // Verify queued shares (uint128 queuedShares,,,,) = vault.getExitQueueData(); - assertEq(queuedShares, exitShares, "Queued shares should match exit amount"); + assertApproxEqAbs(queuedShares, exitShares, 1, "Queued shares should match exit amount"); } // Test _updateExitQueue burns shares and emits Transfer @@ -549,7 +549,7 @@ contract VaultTokenTest is Test, EthHelpers { // Test InvalidTokenMeta error for token name too long function test_invalidTokenMetaNameTooLong() public { // Create a new admin for this test - address newAdmin = makeAddr("newAdmin"); + address newAdmin = makeAddr("NewAdmin"); vm.deal(newAdmin, 10 ether); // Try to create vault with name longer than 30 characters @@ -576,7 +576,7 @@ contract VaultTokenTest is Test, EthHelpers { // Test InvalidTokenMeta error for token symbol too long function test_invalidTokenMetaSymbolTooLong() public { // Create a new admin for this test - address newAdmin = makeAddr("newAdmin"); + address newAdmin = makeAddr("NewAdmin"); vm.deal(newAdmin, 10 ether); // Try to create vault with symbol longer than 10 characters diff --git a/test/VaultValidators.t.sol b/test/VaultValidators.t.sol index d181a30b..c86d3f30 100644 --- a/test/VaultValidators.t.sol +++ b/test/VaultValidators.t.sol @@ -28,9 +28,9 @@ contract VaultValidatorsTest is Test, EthHelpers { contracts = _activateEthereumFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); - nonManager = makeAddr("nonManager"); + admin = makeAddr("Admin"); + user = makeAddr("User"); + nonManager = makeAddr("NonManager"); (validatorsManager, validatorsManagerPrivateKey) = makeAddrAndKey("validatorsManager"); // Fund accounts with ETH for testing @@ -533,8 +533,9 @@ contract VaultValidatorsTest is Test, EthHelpers { bytes memory validTopUpData = bytes.concat(publicKey, signature, depositDataRoot, bytes8(uint64(topUpAmount))); // Create validator manager signature - bytes32 message = - _getValidatorsManagerSigningMessage(address(vault), bytes32(vault.validatorsManagerNonce()), validTopUpData); + bytes32 message = _getValidatorsManagerSigningMessage( + address(vault), bytes32(vault.validatorsManagerNonce()), validTopUpData + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(validatorsManagerPrivateKey, message); bytes memory validatorManagerSignature = abi.encodePacked(r, s, v); @@ -625,8 +626,9 @@ contract VaultValidatorsTest is Test, EthHelpers { // Create invalid signature (wrong signer) (, uint256 wrongPrivateKey) = makeAddrAndKey("wrong"); - bytes32 message = - _getValidatorsManagerSigningMessage(address(vault), bytes32(vault.validatorsManagerNonce()), validTopUpData); + bytes32 message = _getValidatorsManagerSigningMessage( + address(vault), bytes32(vault.validatorsManagerNonce()), validTopUpData + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, message); bytes memory invalidSignature = abi.encodePacked(r, s, v); @@ -851,8 +853,9 @@ contract VaultValidatorsTest is Test, EthHelpers { bytes memory withdrawalData = abi.encodePacked(publicKey, bytes8(uint64(withdrawalAmount))); // 3. Create validator manager signature - bytes32 message = - _getValidatorsManagerSigningMessage(address(vault), bytes32(vault.validatorsManagerNonce()), withdrawalData); + bytes32 message = _getValidatorsManagerSigningMessage( + address(vault), bytes32(vault.validatorsManagerNonce()), withdrawalData + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(validatorsManagerPrivateKey, message); bytes memory signature = abi.encodePacked(r, s, v); @@ -910,8 +913,9 @@ contract VaultValidatorsTest is Test, EthHelpers { // 3. Create invalid signature (wrong signer) (, uint256 wrongPrivateKey) = makeAddrAndKey("wrong"); - bytes32 message = - _getValidatorsManagerSigningMessage(address(vault), bytes32(vault.validatorsManagerNonce()), withdrawalData); + bytes32 message = _getValidatorsManagerSigningMessage( + address(vault), bytes32(vault.validatorsManagerNonce()), withdrawalData + ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, message); bytes memory invalidSignature = abi.encodePacked(r, s, v); diff --git a/test/VaultVersion.t.sol b/test/VaultVersion.t.sol index 04e8b441..98314bcb 100644 --- a/test/VaultVersion.t.sol +++ b/test/VaultVersion.t.sol @@ -32,8 +32,8 @@ contract VaultVersionTest is Test, EthHelpers { vaultsRegistry = contracts.vaultsRegistry; // Set up accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); vm.deal(admin, 100 ether); vm.deal(user, 100 ether); diff --git a/test/VaultsRegistry.t.sol b/test/VaultsRegistry.t.sol index b5373223..0ef21767 100644 --- a/test/VaultsRegistry.t.sol +++ b/test/VaultsRegistry.t.sol @@ -23,11 +23,11 @@ contract VaultsRegistryTest is Test, EthHelpers { registry = contracts.vaultsRegistry; // Set up test accounts - owner = makeAddr("owner"); - nonOwner = makeAddr("nonOwner"); - mockFactory = makeAddr("mockFactory"); - mockVaultImpl = makeAddr("mockVaultImpl"); - mockVault = makeAddr("mockVault"); + owner = makeAddr("Owner"); + nonOwner = makeAddr("NonOwner"); + mockFactory = makeAddr("MockFactory"); + mockVaultImpl = makeAddr("MockVaultImpl"); + mockVault = makeAddr("MockVault"); // Since the registry is already deployed on the fork, we need to // impersonate its owner to perform ownership-restricted actions @@ -137,7 +137,7 @@ contract VaultsRegistryTest is Test, EthHelpers { // Deploy a new VaultsRegistry contract to test initialization VaultsRegistry newRegistry = new VaultsRegistry(); - address newOwner = makeAddr("newOwner"); + address newOwner = makeAddr("NewOwner"); vm.prank(newRegistry.owner()); _startSnapshotGas("VaultsRegistryTest_test_initialize"); @@ -211,8 +211,8 @@ contract VaultsRegistryTest is Test, EthHelpers { // Create a new registry and initialize it once VaultsRegistry newRegistry = new VaultsRegistry(); - address newOwner = makeAddr("newOwner"); - address anotherOwner = makeAddr("anotherOwner"); + address newOwner = makeAddr("NewOwner"); + address anotherOwner = makeAddr("AnotherOwner"); vm.prank(newRegistry.owner()); newRegistry.initialize(newOwner); diff --git a/test/gnosis/GnoBlocklistErc20Vault.t.sol b/test/gnosis/GnoBlocklistErc20Vault.t.sol index 2d14d821..c708abc6 100644 --- a/test/gnosis/GnoBlocklistErc20Vault.t.sol +++ b/test/gnosis/GnoBlocklistErc20Vault.t.sol @@ -29,11 +29,11 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - blocklistManager = makeAddr("blocklistManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + blocklistManager = makeAddr("BlocklistManager"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); diff --git a/test/gnosis/GnoBlocklistVault.t.sol b/test/gnosis/GnoBlocklistVault.t.sol index df08c052..e379ae0b 100644 --- a/test/gnosis/GnoBlocklistVault.t.sol +++ b/test/gnosis/GnoBlocklistVault.t.sol @@ -29,11 +29,11 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - blocklistManager = makeAddr("blocklistManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + blocklistManager = makeAddr("BlocklistManager"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); diff --git a/test/gnosis/GnoErc20Vault.t.sol b/test/gnosis/GnoErc20Vault.t.sol index 0fc508f8..68bf8a30 100644 --- a/test/gnosis/GnoErc20Vault.t.sol +++ b/test/gnosis/GnoErc20Vault.t.sol @@ -31,10 +31,10 @@ contract GnoErc20VaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); @@ -305,7 +305,7 @@ contract GnoErc20VaultTest is Test, GnoHelpers { function test_withdrawValidator_validatorsManager() public { // 1. Set validators manager - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); @@ -325,12 +325,12 @@ contract GnoErc20VaultTest is Test, GnoHelpers { } function test_withdrawValidator_unknown() public { - address validatorsManager = makeAddr("validatorsManager"); + address validatorsManager = makeAddr("ValidatorsManager"); vm.prank(admin); vault.setValidatorsManager(validatorsManager); // 1. Set unknown address - address unknown = makeAddr("unknown"); + address unknown = makeAddr("Unknown"); uint256 withdrawFee = 0.1 ether; vm.deal(unknown, withdrawFee); diff --git a/test/gnosis/GnoGenesisVault.t.sol b/test/gnosis/GnoGenesisVault.t.sol index 6d618428..7d8228c2 100644 --- a/test/gnosis/GnoGenesisVault.t.sol +++ b/test/gnosis/GnoGenesisVault.t.sol @@ -28,8 +28,8 @@ contract GnoGenesisVaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); // Provide GNO to the test accounts _mintGnoToken(admin, 100 ether); diff --git a/test/gnosis/GnoMetaVault.t.sol b/test/gnosis/GnoMetaVault.t.sol index a9145d47..d37e3271 100644 --- a/test/gnosis/GnoMetaVault.t.sol +++ b/test/gnosis/GnoMetaVault.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.22; -import {Test, stdStorage, StdStorage, console} from "forge-std/Test.sol"; +import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -10,17 +10,16 @@ import {IGnoVault} from "../../contracts/interfaces/IGnoVault.sol"; import {IVaultState} from "../../contracts/interfaces/IVaultState.sol"; import {IVaultSubVaults} from "../../contracts/interfaces/IVaultSubVaults.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/custom/GnoMetaVault.sol"; -import {GnoMetaVaultFactory} from "../../contracts/vaults/gnosis/custom/GnoMetaVaultFactory.sol"; +import {GnoMetaVault} from "../../contracts/vaults/gnosis/GnoMetaVault.sol"; +import {GnoMetaVaultFactory} from "../../contracts/vaults/gnosis/GnoMetaVaultFactory.sol"; import {BalancedCurator} from "../../contracts/curators/BalancedCurator.sol"; import {CuratorsRegistry} from "../../contracts/curators/CuratorsRegistry.sol"; import {GnoHelpers} from "../helpers/GnoHelpers.sol"; import {IKeeperRewards} from "../../contracts/interfaces/IKeeperRewards.sol"; contract GnoMetaVaultTest is Test, GnoHelpers { - using stdStorage for StdStorage; - ForkContracts public contracts; GnoMetaVault public metaVault; @@ -41,10 +40,10 @@ contract GnoMetaVaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - admin = makeAddr("admin"); - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - referrer = makeAddr("referrer"); + admin = makeAddr("Admin"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + referrer = makeAddr("Referrer"); // Mint GNO tokens to accounts _mintGnoToken(admin, 100 ether); @@ -60,7 +59,7 @@ contract GnoMetaVaultTest is Test, GnoHelpers { // Deploy meta vault bytes memory initParams = abi.encode( - IGnoMetaVault.GnoMetaVaultInitParams({ + IMetaVault.MetaVaultInitParams({ subVaultsCurator: curator, capacity: 1000 ether, feePercent: 1000, // 10% @@ -92,27 +91,10 @@ contract GnoMetaVaultTest is Test, GnoHelpers { return _createVault(VaultType.GnoVault, _admin, initParams, false); } - function test_deployWithZeroAdmin() public { - // Attempt to deploy with zero admin - bytes memory initParams = abi.encode( - IGnoMetaVault.GnoMetaVaultInitParams({ - subVaultsCurator: curator, - capacity: 1000 ether, - feePercent: 1000, // 10% - metadataIpfsHash: "bafkreidivzimqfqtoqxkrpge6bjyhlvxqs3rhe73owtmdulaxr5do5in7u" - }) - ); - - GnoMetaVaultFactory factory = _getOrCreateMetaFactory(VaultType.GnoMetaVault); - contracts.gnoToken.approve(address(factory), _securityDeposit); - vm.expectRevert(abi.encodeWithSelector(Errors.ZeroAddress.selector)); - factory.createVault(address(0), initParams); - } - function test_deployment() public view { // Verify the vault was deployed correctly assertEq(metaVault.vaultId(), keccak256("GnoMetaVault"), "Incorrect vault ID"); - assertEq(metaVault.version(), 3, "Incorrect version"); + assertEq(metaVault.version(), 4, "Incorrect version"); assertEq(metaVault.admin(), admin, "Incorrect admin"); assertEq(metaVault.subVaultsCurator(), curator, "Incorrect curator"); assertEq(metaVault.capacity(), 1000 ether, "Incorrect capacity"); @@ -468,22 +450,4 @@ contract GnoMetaVaultTest is Test, GnoHelpers { vm.expectRevert(Errors.InvalidAssets.selector); metaVault.donateAssets(0); } - - function _getEmptyHarvestParams() internal pure returns (IKeeperRewards.HarvestParams memory) { - bytes32[] memory emptyProof; - return - IKeeperRewards.HarvestParams({rewardsRoot: bytes32(0), proof: emptyProof, reward: 0, unlockedMevReward: 0}); - } - - function _setVaultRewardsNonce(address vault, uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewards(address)").with_key(vault).depth( - 1 - ).checked_write(rewardsNonce); - } - - function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { - stdstore.enable_packed_slots().target(address(contracts.keeper)).sig("rewardsNonce()").checked_write( - rewardsNonce - ); - } } diff --git a/test/gnosis/GnoOsTokenRedeemer.t.sol b/test/gnosis/GnoOsTokenRedeemer.t.sol index f746c18d..42a3a91d 100644 --- a/test/gnosis/GnoOsTokenRedeemer.t.sol +++ b/test/gnosis/GnoOsTokenRedeemer.t.sol @@ -33,10 +33,10 @@ contract GnoOsTokenRedeemerTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - owner = makeAddr("owner"); - positionsManager = makeAddr("positionsManager"); - user = makeAddr("user"); - redeemer = makeAddr("redeemer"); + owner = makeAddr("Owner"); + positionsManager = makeAddr("PositionsManager"); + user = makeAddr("User"); + redeemer = makeAddr("Redeemer"); // Fund accounts with GNO tokens and xDAI _mintGnoToken(user, 100 ether); @@ -48,6 +48,7 @@ contract GnoOsTokenRedeemerTest is Test, GnoHelpers { // Deploy GnoOsTokenRedeemer osTokenRedeemer = new GnoOsTokenRedeemer( address(contracts.gnoToken), + address(contracts.vaultsRegistry), _osToken, address(contracts.osTokenVaultController), owner, @@ -203,11 +204,8 @@ contract GnoOsTokenRedeemerTest is Test, GnoHelpers { IOsTokenRedeemer.RedeemablePositions memory redeemablePositions = IOsTokenRedeemer.RedeemablePositions({merkleRoot: leaf, ipfsHash: "QmTest123"}); - vm.prank(positionsManager); - osTokenRedeemer.proposeRedeemablePositions(redeemablePositions); - vm.prank(owner); - osTokenRedeemer.acceptRedeemablePositions(); + osTokenRedeemer.setRedeemablePositions(redeemablePositions); bytes32[] memory proof = new bytes32[](0); bool[] memory proofFlags = new bool[](0); @@ -221,7 +219,7 @@ contract GnoOsTokenRedeemerTest is Test, GnoHelpers { emit IOsTokenRedeemer.OsTokenPositionsRedeemed(expectedRedeemedShares, expectedRedeemedAssets); // Redeem position - vm.prank(user); + vm.prank(positionsManager); _startSnapshotGas("GnoOsTokenRedeemerTest_test_redeemOsTokenPositions_success_singlePosition"); osTokenRedeemer.redeemOsTokenPositions(positions, proof, proofFlags); _stopSnapshotGas(); diff --git a/test/gnosis/GnoOsTokenVaultEscrow.t.sol b/test/gnosis/GnoOsTokenVaultEscrow.t.sol index 6ac3b2ac..2c123fcf 100644 --- a/test/gnosis/GnoOsTokenVaultEscrow.t.sol +++ b/test/gnosis/GnoOsTokenVaultEscrow.t.sol @@ -29,8 +29,8 @@ contract GnoOsTokenVaultEscrowTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Setup addresses - user = makeAddr("user"); - admin = makeAddr("admin"); + user = makeAddr("User"); + admin = makeAddr("Admin"); // Fund accounts vm.deal(user, 1 ether); @@ -99,9 +99,10 @@ contract GnoOsTokenVaultEscrowTest is Test, GnoHelpers { vm.warp(timestamp + _exitingAssetsClaimDelay + 1); _startSnapshotGas("GnoOsTokenVaultEscrowTest_test_transferAssets_process"); - contracts.osTokenVaultEscrow.processExitedAssets( - address(vault), exitPositionTicket, timestamp, uint256(vault.getExitQueueIndex(exitPositionTicket)) - ); + contracts.osTokenVaultEscrow + .processExitedAssets( + address(vault), exitPositionTicket, timestamp, uint256(vault.getExitQueueIndex(exitPositionTicket)) + ); _stopSnapshotGas(); // User claims exited assets diff --git a/test/gnosis/GnoOwnMevEscrow.t.sol b/test/gnosis/GnoOwnMevEscrow.t.sol index 42e5ae3e..ebba5e04 100644 --- a/test/gnosis/GnoOwnMevEscrow.t.sol +++ b/test/gnosis/GnoOwnMevEscrow.t.sol @@ -19,8 +19,8 @@ contract GnoOwnMevEscrowTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - vault = makeAddr("vault"); - other = makeAddr("other"); + vault = makeAddr("Vault"); + other = makeAddr("Other"); vm.deal(other, 10 ether); // Give 'other' some xDAI // Deploy the contract diff --git a/test/gnosis/GnoPrivErc20Vault.t.sol b/test/gnosis/GnoPrivErc20Vault.t.sol index e54a37a9..b5405e96 100644 --- a/test/gnosis/GnoPrivErc20Vault.t.sol +++ b/test/gnosis/GnoPrivErc20Vault.t.sol @@ -29,11 +29,11 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - whitelister = makeAddr("whitelister"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + whitelister = makeAddr("Whitelister"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); diff --git a/test/gnosis/GnoPrivMetaVault.t.sol b/test/gnosis/GnoPrivMetaVault.t.sol new file mode 100644 index 00000000..29d1ba81 --- /dev/null +++ b/test/gnosis/GnoPrivMetaVault.t.sol @@ -0,0 +1,475 @@ +// 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/GnoPrivVault.t.sol b/test/gnosis/GnoPrivVault.t.sol index a82c0292..708f0b0f 100644 --- a/test/gnosis/GnoPrivVault.t.sol +++ b/test/gnosis/GnoPrivVault.t.sol @@ -29,11 +29,11 @@ contract GnoPrivVaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - other = makeAddr("other"); - whitelister = makeAddr("whitelister"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + other = makeAddr("Other"); + whitelister = makeAddr("Whitelister"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); @@ -309,7 +309,7 @@ contract GnoPrivVaultTest is Test, GnoHelpers { } function test_setWhitelister() public { - address newWhitelister = makeAddr("newWhitelister"); + address newWhitelister = makeAddr("NewWhitelister"); // Non-admin cannot set whitelister vm.prank(other); diff --git a/test/gnosis/GnoRewardSplitter.t.sol b/test/gnosis/GnoRewardSplitter.t.sol index 9c5579e0..3dec50f8 100644 --- a/test/gnosis/GnoRewardSplitter.t.sol +++ b/test/gnosis/GnoRewardSplitter.t.sol @@ -36,11 +36,11 @@ contract GnoRewardSplitterTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - admin = makeAddr("admin"); - shareholder1 = makeAddr("shareholder1"); - shareholder2 = makeAddr("shareholder2"); - depositor = makeAddr("depositor"); - claimer = makeAddr("claimer"); + admin = makeAddr("Admin"); + shareholder1 = makeAddr("Shareholder1"); + shareholder2 = makeAddr("Shareholder2"); + depositor = makeAddr("Depositor"); + claimer = makeAddr("Claimer"); // Fund accounts vm.deal(admin, 100 ether); @@ -317,9 +317,8 @@ contract GnoRewardSplitterTest is Test, GnoHelpers { int256 exitQueueIndex = IVaultEnterExit(vault).getExitQueueIndex(positionTicket); // Expected reward amount to be claimed - (,, uint256 exitedAssets) = IVaultEnterExit(vault).calculateExitedAssets( - address(rewardSplitter), positionTicket, timestamp, uint256(exitQueueIndex) - ); + (,, uint256 exitedAssets) = IVaultEnterExit(vault) + .calculateExitedAssets(address(rewardSplitter), positionTicket, timestamp, uint256(exitQueueIndex)); vm.prank(admin); vm.expectEmit(true, false, false, true); diff --git a/test/gnosis/GnoSharedMevEscrow.t.sol b/test/gnosis/GnoSharedMevEscrow.t.sol index dd3ba35f..de745dc8 100644 --- a/test/gnosis/GnoSharedMevEscrow.t.sol +++ b/test/gnosis/GnoSharedMevEscrow.t.sol @@ -22,11 +22,11 @@ contract GnoSharedMevEscrowTest is Test, GnoHelpers { sharedMevEscrow = new GnoSharedMevEscrow(address(contracts.vaultsRegistry)); // Set up test account - other = makeAddr("other"); + other = makeAddr("Other"); vm.deal(other, 10 ether); // Register a mock vault in the registry - mockVault = makeAddr("mockVault"); + mockVault = makeAddr("MockVault"); vm.prank(contracts.vaultsRegistry.owner()); contracts.vaultsRegistry.addVault(mockVault); } diff --git a/test/gnosis/GnoValidatorsChecker.t.sol b/test/gnosis/GnoValidatorsChecker.t.sol index 4301685e..7ec22e22 100644 --- a/test/gnosis/GnoValidatorsChecker.t.sol +++ b/test/gnosis/GnoValidatorsChecker.t.sol @@ -35,8 +35,8 @@ contract GnoValidatorsCheckerTest is Test, GnoHelpers { ); // Setup accounts - admin = makeAddr("admin"); - user = makeAddr("user"); + admin = makeAddr("Admin"); + user = makeAddr("User"); _mintGnoToken(user, 100 ether); _mintGnoToken(admin, 100 ether); @@ -73,6 +73,7 @@ contract GnoValidatorsCheckerTest is Test, GnoHelpers { uint256 missingAssets = validatorsChecker.getExitQueueMissingAssets( emptyVault, 0, // withdrawingAssets + 0, // No redemption assets cumulativeTickets // targetCumulativeTickets (same as current since queue is empty) ); @@ -95,6 +96,7 @@ contract GnoValidatorsCheckerTest is Test, GnoHelpers { uint256 initialMissingAssets = validatorsChecker.getExitQueueMissingAssets( prevVersionVault, 0, // withdrawingAssets + 0, // No redemption assets initialCumulativeTickets // targetCumulativeTickets ); @@ -108,6 +110,7 @@ contract GnoValidatorsCheckerTest is Test, GnoHelpers { uint256 updatedMissingAssets = validatorsChecker.getExitQueueMissingAssets( prevVersionVault, 0, // withdrawingAssets + 0, // No redemption assets initialCumulativeTickets // use same target as before for fair comparison ); diff --git a/test/gnosis/GnoVault.t.sol b/test/gnosis/GnoVault.t.sol index f4773f0c..8fb0d1ca 100644 --- a/test/gnosis/GnoVault.t.sol +++ b/test/gnosis/GnoVault.t.sol @@ -31,11 +31,11 @@ contract GnoVaultTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - referrer = makeAddr("referrer"); - validatorsManager = makeAddr("validatorsManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + referrer = makeAddr("Referrer"); + validatorsManager = makeAddr("ValidatorsManager"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); @@ -257,8 +257,7 @@ contract GnoVaultTest is Test, GnoHelpers { uint256 senderSharesBefore = vault.getShares(sender); ( uint128 queuedSharesBefore, - uint128 unclaimedAssetsBefore, - , + uint128 unclaimedAssetsBefore,, uint128 totalExitingAssetsBefore, uint256 totalTicketsBefore ) = vault.getExitQueueData(); @@ -275,8 +274,7 @@ contract GnoVaultTest is Test, GnoHelpers { ( uint128 queuedSharesAfter, - uint128 unclaimedAssetsAfter, - , + uint128 unclaimedAssetsAfter,, uint128 totalExitingAssetsAfter, uint256 totalTicketsAfter ) = vault.getExitQueueData(); @@ -370,7 +368,7 @@ contract GnoVaultTest is Test, GnoHelpers { function test_withdrawValidator_unknown() public { // Create an unknown address - address unknown = makeAddr("unknown"); + address unknown = makeAddr("Unknown"); // Fund the unknown account uint256 withdrawFee = 0.1 ether; diff --git a/test/gnosis/GnoVaultExitQueue.t.sol b/test/gnosis/GnoVaultExitQueue.t.sol index 5cd8528e..7c1bf524 100644 --- a/test/gnosis/GnoVaultExitQueue.t.sol +++ b/test/gnosis/GnoVaultExitQueue.t.sol @@ -44,10 +44,10 @@ contract GnoVaultExitQueueTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - admin = makeAddr("admin"); - user1 = makeAddr("user1"); - user2 = makeAddr("user2"); - user3 = makeAddr("user3"); + admin = makeAddr("Admin"); + user1 = makeAddr("User1"); + user2 = makeAddr("User2"); + user3 = makeAddr("User3"); // Fund accounts _mintGnoToken(admin, 100 ether); diff --git a/test/gnosis/VaultGnoStaking.t.sol b/test/gnosis/VaultGnoStaking.t.sol index edeb0ffb..ef083216 100644 --- a/test/gnosis/VaultGnoStaking.t.sol +++ b/test/gnosis/VaultGnoStaking.t.sol @@ -29,11 +29,11 @@ contract VaultGnoStakingTest is Test, GnoHelpers { contracts = _activateGnosisFork(); // Set up test accounts - sender = makeAddr("sender"); - receiver = makeAddr("receiver"); - admin = makeAddr("admin"); - referrer = makeAddr("referrer"); - validatorsManager = makeAddr("validatorsManager"); + sender = makeAddr("Sender"); + receiver = makeAddr("Receiver"); + admin = makeAddr("Admin"); + referrer = makeAddr("Referrer"); + validatorsManager = makeAddr("ValidatorsManager"); // Fund accounts with GNO for testing _mintGnoToken(sender, 100 ether); diff --git a/test/helpers/EthHelpers.sol b/test/helpers/EthHelpers.sol index 625e04de..186067f8 100644 --- a/test/helpers/EthHelpers.sol +++ b/test/helpers/EthHelpers.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; -import {Test} from "forge-std/Test.sol"; +import {Test, stdStorage, StdStorage} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IKeeperValidators} from "../../contracts/interfaces/IKeeperValidators.sol"; import {IOsTokenConfig} from "../../contracts/interfaces/IOsTokenConfig.sol"; @@ -12,6 +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 {IConsolidationsChecker} from "../../contracts/interfaces/IConsolidationsChecker.sol"; import {ConsolidationsChecker} from "../../contracts/validators/ConsolidationsChecker.sol"; import {EthBlocklistErc20Vault} from "../../contracts/vaults/ethereum/EthBlocklistErc20Vault.sol"; @@ -23,8 +24,9 @@ import {EthPrivVault} from "../../contracts/vaults/ethereum/EthPrivVault.sol"; import {EthVault, IEthVault} from "../../contracts/vaults/ethereum/EthVault.sol"; import {EthVaultFactory} from "../../contracts/vaults/ethereum/EthVaultFactory.sol"; import {IEthFoxVault, EthFoxVault} from "../../contracts/vaults/ethereum/custom/EthFoxVault.sol"; -import {IEthMetaVault, EthMetaVault} from "../../contracts/vaults/ethereum/custom/EthMetaVault.sol"; -import {EthMetaVaultFactory} from "../../contracts/vaults/ethereum/custom/EthMetaVaultFactory.sol"; +import {EthMetaVault} from "../../contracts/vaults/ethereum/EthMetaVault.sol"; +import {EthPrivMetaVault} from "../../contracts/vaults/ethereum/EthPrivMetaVault.sol"; +import {EthMetaVaultFactory} from "../../contracts/vaults/ethereum/EthMetaVaultFactory.sol"; import {Keeper} from "../../contracts/keeper/Keeper.sol"; import {ValidatorsConsolidationsMock} from "../../contracts/mocks/ValidatorsConsolidationsMock.sol"; import {ValidatorsHelpers} from "./ValidatorsHelpers.sol"; @@ -33,7 +35,9 @@ import {VaultsRegistry, IVaultsRegistry} from "../../contracts/vaults/VaultsRegi import {CuratorsRegistry} from "../../contracts/curators/CuratorsRegistry.sol"; abstract contract EthHelpers is Test, ValidatorsHelpers { - uint256 internal constant forkBlockNumber = 22100000; + using stdStorage for StdStorage; + + uint256 internal constant forkBlockNumber = 24235110; uint256 internal constant _securityDeposit = 1e9; address private constant _keeper = 0x6B5815467da09DaA7DC83Db21c9239d98Bb487b5; address private constant _validatorsRegistry = 0x00000000219ab540356cBB839Cbe05303d7705Fa; @@ -47,6 +51,9 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { address internal constant _depositDataRegistry = 0x75AB6DdCe07556639333d3Df1eaa684F5735223e; address internal constant _poolEscrow = 0x2296e122c1a20Fca3CAc3371357BdAd3be0dF079; address internal constant _rewardEthToken = 0x20BC832ca081b91433ff6c17f85701B6e92486c5; + address internal constant _consolidationsChecker = 0x033E5BaE5bdc459CBb7d388b41a9d62020Be810F; + address internal constant _curatorsRegistry = 0xa23F7c8d25f4503cA4cEd84d9CC2428e8745933C; + address internal constant _balancedCurator = 0xD30E7e4bDbd396cfBe72Ad2f4856769C54eA6b0b; uint256 internal constant _exitingAssetsClaimDelay = 15 hours; enum VaultType { @@ -58,7 +65,8 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { EthBlocklistErc20Vault, EthPrivErc20Vault, EthFoxVault, - EthMetaVault + EthMetaVault, + EthPrivMetaVault } struct ForkContracts { @@ -76,18 +84,14 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { mapping(VaultType vaultType => address vaultFactory) private _vaultFactories; mapping(VaultType vaultType => address vaultFactory) private _vaultPrevFactories; - address internal _consolidationsChecker; address internal _validatorsWithdrawals; address internal _validatorsConsolidations; - address internal _curatorsRegistry; function _activateEthereumFork() internal returns (ForkContracts memory) { vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), forkBlockNumber); _validatorsWithdrawals = address(new ValidatorsWithdrawalsMock()); _validatorsConsolidations = address(new ValidatorsConsolidationsMock()); - _consolidationsChecker = address(new ConsolidationsChecker(address(_keeper))); - _curatorsRegistry = address(new CuratorsRegistry()); return ForkContracts({ keeper: Keeper(_keeper), @@ -121,6 +125,10 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { vm.prank(currentAdmin); IEthVault(vault).setAdmin(admin); } + if (IEthVault(vault).feeRecipient() != admin) { + vm.prank(admin); + IEthVault(vault).setFeeRecipient(admin); + } } function _getOrCreateFactory(VaultType _vaultType) internal returns (EthVaultFactory) { @@ -145,7 +153,7 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { } address impl = _getOrCreateVaultImpl(_vaultType); - EthMetaVaultFactory factory = new EthMetaVaultFactory(address(this), impl, IVaultsRegistry(_vaultsRegistry)); + EthMetaVaultFactory factory = new EthMetaVaultFactory(impl, IVaultsRegistry(_vaultsRegistry)); _vaultFactories[_vaultType] = address(factory); @@ -174,6 +182,8 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { impl = 0x9488A7dd178F0D927707eEc61A7D8C0ae9558c88; } else if (_vaultType == VaultType.EthBlocklistErc20Vault) { impl = 0x84d44A696539B3eF4162184fb8ab97596A311e9E; + } else if (_vaultType == VaultType.EthMetaVault) { + impl = 0xD0D527B67186d8880f9427ea4Cf9847E89bcE764; } else { return EthVaultFactory(address(0)); } @@ -255,15 +265,19 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { // Update with actual deployed vault addresses for each type if (vaultType == VaultType.EthVault) { - return 0x8A93A876912c9F03F88Bc9114847cf5b63c89f56; + return 0x7Eed3ea8D83ba4Ccc1b20674F46825ece2fce594; } else if (vaultType == VaultType.EthPrivVault) { return 0xD66A71A68392767F26b7EE47e9a0293191A23072; } else if (vaultType == VaultType.EthErc20Vault) { - return 0x7106FA765d45dF6d5340972C58742fC54f0d1Ef9; + return 0x9c29c571847A68A947AceC8bacd303e36bC72ec5; } else if (vaultType == VaultType.EthPrivErc20Vault) { return 0xFB22Ded2bd69aff0907e195F23E448aB44E3cA97; - } else if (vaultType == VaultType.EthFoxVault) { - return 0x4FEF9D741011476750A243aC70b9789a63dd47Df; + } else if (vaultType == VaultType.EthBlocklistVault) { + return 0xf51033647a8ab632B80B69b1c680aaDcC8ADa048; + } else if (vaultType == VaultType.EthBlocklistErc20Vault) { + return 0x498399e4f5FDe641a43DCEAFc0aac858abaF2034; + } else if (vaultType == VaultType.EthMetaVault) { + return 0x34284C27A2304132aF751b0dEc5bBa2CF98eD039; } return address(0); } @@ -276,10 +290,10 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { // Update with actual values if needed for specific vaults if (vault == 0xAC0F906E433d58FA868F936E8A43230473652885) { // Genesis Vault - newTotalReward += 11492988394536925432019; - newUnlockedMevReward += 588134256533622872486; + newTotalReward += 15357936244318545414766; + newUnlockedMevReward += 954581796972242855233; } else if (vault == 0x4FEF9D741011476750A243aC70b9789a63dd47Df) { - newTotalReward += 242948554351000000000; + newTotalReward += 1097049381115000000000; } if (!vm.envBool("TEST_USE_FORK_VAULTS")) { @@ -287,11 +301,14 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { } // Add specific rewards for each vault type - if (vault == 0x8A93A876912c9F03F88Bc9114847cf5b63c89f56) { - newTotalReward += 39158842473943927643; - newUnlockedMevReward += 6210915181493109989; + if (vault == 0x7Eed3ea8D83ba4Ccc1b20674F46825ece2fce594) { + newTotalReward += 1835140592094467096; + newUnlockedMevReward += 246577230094467096; } else if (vault == 0xD66A71A68392767F26b7EE47e9a0293191A23072) { newTotalReward += 17651468000000000; + } else if (vault == 0x9c29c571847A68A947AceC8bacd303e36bC72ec5) { + newTotalReward += 1590862592749045978; + newUnlockedMevReward += 251734367749045978; } return (newTotalReward, newUnlockedMevReward); @@ -314,8 +331,9 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { address vaultAddress; if (vaultType == VaultType.EthMetaVault) { EthMetaVaultFactory factory = _getOrCreateMetaFactory(vaultType); - vm.deal(address(this), address(this).balance + _securityDeposit); - vaultAddress = factory.createVault{value: _securityDeposit}(admin, initParams); + vm.deal(admin, admin.balance + _securityDeposit); + vm.prank(admin); + vaultAddress = factory.createVault{value: _securityDeposit}(initParams); } else { EthVaultFactory factory = _getOrCreateFactory(vaultType); vm.deal(admin, admin.balance + _securityDeposit); @@ -339,10 +357,7 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { return vaultAddress; } - function _createV1EthVault(address admin, bytes memory initParams, bool isOwnMevEscrow) - internal - returns (address) - { + function _createV1EthVault(address admin, bytes memory initParams, bool isOwnMevEscrow) internal returns (address) { EthVaultFactory factory = EthVaultFactory(0xDada5a8E3703B1e3EA2bAe5Ab704627eb2659fCC); vm.prank(VaultsRegistry(_vaultsRegistry).owner()); @@ -362,6 +377,9 @@ 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) { + if (currentVersion == 6) return; + require(currentVersion == 5, "Invalid vault version"); } else { if (currentVersion == 5) return; require(currentVersion == 4, "Invalid vault version"); @@ -372,7 +390,7 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { vm.deal(admin, admin.balance + 1 ether); vm.prank(admin); - vaultContract.upgradeToAndCall(newImpl, "0x"); + vaultContract.upgradeToAndCall(newImpl, ""); } function _getOrCreateVaultImpl(VaultType _vaultType) internal returns (address impl) { @@ -438,8 +456,7 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { ); impl = address(new EthFoxVault(ethFoxVaultArgs)); } else if (_vaultType == VaultType.EthMetaVault) { - IEthMetaVault.EthMetaVaultConstructorArgs memory ethMetaVaultArgs = IEthMetaVault - .EthMetaVaultConstructorArgs( + IMetaVault.MetaVaultConstructorArgs memory ethMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( _keeper, _vaultsRegistry, _osTokenVaultController, @@ -449,6 +466,19 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { uint64(_exitingAssetsClaimDelay) ); impl = address(new EthMetaVault(ethMetaVaultArgs)); + } else if (_vaultType == VaultType.EthPrivMetaVault) { + IMetaVault.MetaVaultConstructorArgs memory ethMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( + _keeper, + _vaultsRegistry, + _osTokenVaultController, + _osTokenConfig, + _osTokenVaultEscrow, + _curatorsRegistry, + uint64(_exitingAssetsClaimDelay) + ); + impl = address(new EthPrivMetaVault(ethMetaVaultArgs)); + } else { + revert("Unsupported vault type"); } _vaultImplementations[_vaultType] = impl; @@ -458,4 +488,21 @@ abstract contract EthHelpers is Test, ValidatorsHelpers { return impl; } + + // ============ Shared Meta Vault Test Helpers ============ + + function _getEmptyHarvestParams() internal pure returns (IKeeperRewards.HarvestParams memory) { + bytes32[] memory emptyProof; + return + IKeeperRewards.HarvestParams({rewardsRoot: bytes32(0), proof: emptyProof, reward: 0, unlockedMevReward: 0}); + } + + function _setVaultRewardsNonce(address vault, uint64 rewardsNonce) internal { + stdstore.enable_packed_slots().target(_keeper).sig("rewards(address)").with_key(vault).depth(1) + .checked_write(rewardsNonce); + } + + function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { + stdstore.enable_packed_slots().target(_keeper).sig("rewardsNonce()").checked_write(rewardsNonce); + } } diff --git a/test/helpers/GnoHelpers.sol b/test/helpers/GnoHelpers.sol index 97a347fc..16b22812 100644 --- a/test/helpers/GnoHelpers.sol +++ b/test/helpers/GnoHelpers.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; -import {Test} from "forge-std/Test.sol"; +import {Test, stdStorage, StdStorage} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {IKeeperValidators} from "../../contracts/interfaces/IKeeperValidators.sol"; @@ -14,6 +14,7 @@ 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 {ConsolidationsChecker} from "../../contracts/validators/ConsolidationsChecker.sol"; import {GnoBlocklistErc20Vault} from "../../contracts/vaults/gnosis/GnoBlocklistErc20Vault.sol"; import {GnoBlocklistVault} from "../../contracts/vaults/gnosis/GnoBlocklistVault.sol"; @@ -22,8 +23,9 @@ import {GnoGenesisVault} from "../../contracts/vaults/gnosis/GnoGenesisVault.sol import {GnoPrivErc20Vault} from "../../contracts/vaults/gnosis/GnoPrivErc20Vault.sol"; import {GnoPrivVault} from "../../contracts/vaults/gnosis/GnoPrivVault.sol"; import {GnoVault, IGnoVault} from "../../contracts/vaults/gnosis/GnoVault.sol"; -import {IGnoMetaVault, GnoMetaVault} from "../../contracts/vaults/gnosis/custom/GnoMetaVault.sol"; -import {GnoMetaVaultFactory} from "../../contracts/vaults/gnosis/custom/GnoMetaVaultFactory.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 {Keeper} from "../../contracts/keeper/Keeper.sol"; import {ValidatorsConsolidationsMock} from "../../contracts/mocks/ValidatorsConsolidationsMock.sol"; @@ -38,6 +40,8 @@ interface IGnoToken { } abstract contract GnoHelpers is Test, ValidatorsHelpers { + using stdStorage for StdStorage; + uint256 internal constant forkBlockNumber = 40107000; uint256 internal constant _securityDeposit = 1e9; address private constant _keeper = 0xcAC0e3E35d3BA271cd2aaBE688ac9DB1898C26aa; @@ -64,7 +68,8 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { GnoErc20Vault, GnoBlocklistErc20Vault, GnoPrivErc20Vault, - GnoMetaVault + GnoMetaVault, + GnoPrivMetaVault } struct ForkContracts { @@ -82,6 +87,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { mapping(VaultType vaultType => address vaultImpl) private _vaultImplementations; mapping(VaultType vaultType => address vaultFactory) private _vaultFactories; + mapping(VaultType vaultType => address vaultFactory) private _vaultPrevFactories; address private _consolidationsChecker; address private _validatorsWithdrawals; @@ -204,8 +210,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { } address impl = _getOrCreateVaultImpl(_vaultType); - GnoMetaVaultFactory factory = - new GnoMetaVaultFactory(address(this), impl, IVaultsRegistry(_vaultsRegistry), _gnoToken); + GnoMetaVaultFactory factory = new GnoMetaVaultFactory(impl, IVaultsRegistry(_vaultsRegistry), _gnoToken); _vaultFactories[_vaultType] = address(factory); @@ -326,10 +331,12 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { returns (address) { address vaultAddress; - if (vaultType == VaultType.GnoMetaVault) { + if (vaultType == VaultType.GnoMetaVault || vaultType == VaultType.GnoPrivMetaVault) { GnoMetaVaultFactory factory = _getOrCreateMetaFactory(vaultType); + vm.startPrank(admin); IERC20(_gnoToken).approve(address(factory), _securityDeposit); - vaultAddress = factory.createVault(admin, initParams); + vaultAddress = factory.createVault(initParams); + vm.stopPrank(); } else { GnoVaultFactory factory = _getOrCreateFactory(vaultType); vm.startPrank(admin); @@ -361,6 +368,9 @@ 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) { + if (currentVersion == 4) return; + require(currentVersion == 3, "Invalid vault version"); } else { if (currentVersion == 3) return; require(currentVersion == 2, "Invalid vault version"); @@ -370,7 +380,7 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { vm.deal(admin, admin.balance + 1 ether); vm.prank(admin); - vaultContract.upgradeToAndCall(newImpl, "0x"); + vaultContract.upgradeToAndCall(newImpl, ""); } function _getOrCreateVaultImpl(VaultType _vaultType) internal returns (address impl) { @@ -425,22 +435,52 @@ abstract contract GnoHelpers is Test, ValidatorsHelpers { } else if (_vaultType == VaultType.GnoPrivErc20Vault) { impl = address(new GnoPrivErc20Vault(gnoErc20Args)); } else if (_vaultType == VaultType.GnoMetaVault) { - IGnoMetaVault.GnoMetaVaultConstructorArgs memory gnoMetaVaultArgs = IGnoMetaVault - .GnoMetaVaultConstructorArgs( + IMetaVault.MetaVaultConstructorArgs memory gnoMetaVaultArgs = IMetaVault.MetaVaultConstructorArgs( + _keeper, + _vaultsRegistry, + _osTokenVaultController, + _osTokenConfig, + _osTokenVaultEscrow, + _curatorsRegistry, + 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, - _gnoToken, uint64(_exitingAssetsClaimDelay) ); - impl = address(new GnoMetaVault(gnoMetaVaultArgs)); + impl = address(new GnoPrivMetaVault(_gnoToken, gnoMetaVaultArgs)); + } else { + revert("Unsupported vault type"); } _vaultImplementations[_vaultType] = impl; vm.prank(VaultsRegistry(_vaultsRegistry).owner()); VaultsRegistry(_vaultsRegistry).addVaultImpl(impl); + + return impl; + } + + // ============ Shared Meta Vault Test Helpers ============ + + function _getEmptyHarvestParams() internal pure returns (IKeeperRewards.HarvestParams memory) { + bytes32[] memory emptyProof; + return + IKeeperRewards.HarvestParams({rewardsRoot: bytes32(0), proof: emptyProof, reward: 0, unlockedMevReward: 0}); + } + + function _setVaultRewardsNonce(address vault, uint64 rewardsNonce) internal { + stdstore.enable_packed_slots().target(_keeper).sig("rewards(address)").with_key(vault).depth(1) + .checked_write(rewardsNonce); + } + + function _setKeeperRewardsNonce(uint64 rewardsNonce) internal { + stdstore.enable_packed_slots().target(_keeper).sig("rewardsNonce()").checked_write(rewardsNonce); } }