diff --git a/contracts/base/ERC20Upgradeable.sol b/contracts/base/ERC20Upgradeable.sol index b49bfe7d..4658dc18 100644 --- a/contracts/base/ERC20Upgradeable.sol +++ b/contracts/base/ERC20Upgradeable.sol @@ -7,6 +7,7 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IERC20Permit} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol'; import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {Errors} from '../libraries/Errors.sol'; +import {EIP712Utils} from '../libraries/EIP712Utils.sol'; /** * @title ERC20 Upgradeable @@ -128,18 +129,7 @@ abstract contract ERC20Upgradeable is Initializable, IERC20Permit, IERC20, IERC2 * @dev This function is used to compute the hash of the EIP712 typed data */ function _computeDomainSeparator() private view returns (bytes32) { - return - keccak256( - abi.encode( - keccak256( - 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' - ), - keccak256(bytes(name)), - keccak256('1'), - block.chainid, - address(this) - ) - ); + return EIP712Utils.computeDomainSeparator(name, address(this)); } /** diff --git a/contracts/interfaces/IVaultEthStaking.sol b/contracts/interfaces/IVaultEthStaking.sol index 07361e45..7202b5ca 100644 --- a/contracts/interfaces/IVaultEthStaking.sol +++ b/contracts/interfaces/IVaultEthStaking.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.22; -import {IVaultState} from './IVaultState.sol'; import {IVaultValidators} from './IVaultValidators.sol'; import {IVaultEnterExit} from './IVaultEnterExit.sol'; import {IKeeperRewards} from './IKeeperRewards.sol'; @@ -13,7 +12,7 @@ import {IVaultMev} from './IVaultMev.sol'; * @author StakeWise * @notice Defines the interface for the VaultEthStaking contract */ -interface IVaultEthStaking is IVaultState, IVaultValidators, IVaultEnterExit, IVaultMev { +interface IVaultEthStaking is IVaultValidators, IVaultEnterExit, IVaultMev { /** * @notice Deposit ETH to the Vault * @param receiver The address that will receive Vault's shares diff --git a/contracts/interfaces/IVaultState.sol b/contracts/interfaces/IVaultState.sol index 3023618b..87495e63 100644 --- a/contracts/interfaces/IVaultState.sol +++ b/contracts/interfaces/IVaultState.sol @@ -57,16 +57,21 @@ interface IVaultState is IVaultFee { function withdrawableAssets() external view returns (uint256); /** - * @notice Queued Shares - * @return The total number of shares queued for exit + * @notice Get exit queue data + * @return queuedShares The number of shares in the exit queue + * @return unclaimedAssets The amount of unclaimed assets in the exit queue + * @return totalExitingAssets The total amount of exiting assets + * @return totalTickets The total number of tickets in the exit queue */ - function queuedShares() external view returns (uint128); - - /** - * @notice Total Exiting Assets (deprecated) - * @return The total number of assets queued for exit - */ - function totalExitingAssets() external view returns (uint128); + function getExitQueueData() + external + view + returns ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ); /** * @notice Returns the number of shares held by an account diff --git a/contracts/libraries/EIP712Utils.sol b/contracts/libraries/EIP712Utils.sol new file mode 100644 index 00000000..2454a8c1 --- /dev/null +++ b/contracts/libraries/EIP712Utils.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +/** + * @title EIP712Utils + * @author StakeWise + * @notice Includes functionality for calculating EIP712 hashes + */ +library EIP712Utils { + /** + * @notice Computes the hash of the EIP712 typed data + * @dev This function is used to compute the hash of the EIP712 typed data + * @param name The name of the domain + * @param verifyingContract The address of the verifying contract + * @return The hash of the EIP712 typed data + */ + function computeDomainSeparator( + string memory name, + address verifyingContract + ) external view returns (bytes32) { + return + keccak256( + abi.encode( + keccak256( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + ), + keccak256(bytes(name)), + keccak256('1'), + block.chainid, + verifyingContract + ) + ); + } +} diff --git a/contracts/libraries/ExitQueue.sol b/contracts/libraries/ExitQueue.sol index 6e3ec21d..0a55f0b6 100644 --- a/contracts/libraries/ExitQueue.sol +++ b/contracts/libraries/ExitQueue.sol @@ -52,7 +52,7 @@ library ExitQueue { function getCheckpointIndex( History storage self, uint256 positionTicket - ) internal view returns (uint256) { + ) external view returns (uint256) { uint256 high = self.checkpoints.length; uint256 low; while (low < high) { @@ -83,7 +83,7 @@ library ExitQueue { uint256 checkpointIdx, uint256 positionTicket, uint256 positionShares - ) internal view returns (uint256 burnedShares, uint256 exitedAssets) { + ) external view returns (uint256 burnedShares, uint256 exitedAssets) { uint256 length = self.checkpoints.length; // there are no exited assets for such checkpoint index or no shares to burn if (checkpointIdx >= length || positionShares == 0) return (0, 0); diff --git a/contracts/libraries/OsTokenUtils.sol b/contracts/libraries/OsTokenUtils.sol new file mode 100644 index 00000000..dde7ec18 --- /dev/null +++ b/contracts/libraries/OsTokenUtils.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; +import {IOsTokenConfig} from '../interfaces/IOsTokenConfig.sol'; +import {IOsTokenVaultController} from '../interfaces/IOsTokenVaultController.sol'; +import {Errors} from './Errors.sol'; + +/** + * @title OsTokenUtils + * @author StakeWise + * @notice Includes functionality for handling osToken redemptions + */ +library OsTokenUtils { + uint256 private constant _wad = 1e18; + uint256 private constant _hfLiqThreshold = 1e18; + uint256 private constant _maxPercent = 1e18; + uint256 private constant _disabledLiqThreshold = type(uint64).max; + + /** + * @dev Struct for storing redemption data + * @param mintedAssets The amount of minted assets + * @param depositedAssets The amount of deposited assets + * @param redeemedOsTokenShares The amount of redeemed osToken shares + * @param availableAssets The amount of available assets + * @param isLiquidation Whether the redemption is a liquidation + */ + struct RedemptionData { + uint256 mintedAssets; + uint256 depositedAssets; + uint256 redeemedOsTokenShares; + uint256 availableAssets; + bool isLiquidation; + } + + /** + * @dev Calculates the amount of received assets during osToken redemption + * @param osTokenConfig The address of the osToken config contract + * @param osTokenVaultController The address of the osToken vault controller contract + * @param data The redemption data + * @return receivedAssets The amount of received assets + */ + function calculateReceivedAssets( + IOsTokenConfig osTokenConfig, + IOsTokenVaultController osTokenVaultController, + RedemptionData memory data + ) external view returns (uint256 receivedAssets) { + // SLOAD to memory + IOsTokenConfig.Config memory config = osTokenConfig.getConfig(address(this)); + if (data.isLiquidation && config.liqThresholdPercent == _disabledLiqThreshold) { + revert Errors.LiquidationDisabled(); + } + + // calculate received assets + if (data.isLiquidation) { + receivedAssets = Math.mulDiv( + osTokenVaultController.convertToAssets(data.redeemedOsTokenShares), + config.liqBonusPercent, + _maxPercent + ); + } else { + receivedAssets = osTokenVaultController.convertToAssets(data.redeemedOsTokenShares); + } + + { + // check whether received assets are valid + if (receivedAssets > data.depositedAssets || receivedAssets > data.availableAssets) { + revert Errors.InvalidReceivedAssets(); + } + + if (data.isLiquidation) { + // check health factor violation in case of liquidation + if ( + Math.mulDiv( + data.depositedAssets * _wad, + config.liqThresholdPercent, + data.mintedAssets * _maxPercent + ) >= _hfLiqThreshold + ) { + revert Errors.InvalidHealthFactor(); + } + } + } + } +} diff --git a/contracts/libraries/ValidatorUtils.sol b/contracts/libraries/ValidatorUtils.sol new file mode 100644 index 00000000..1c56dee9 --- /dev/null +++ b/contracts/libraries/ValidatorUtils.sol @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.22; + +import {Address} from '@openzeppelin/contracts/utils/Address.sol'; +import {MessageHashUtils} from '@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol'; +import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; +import {Errors} from './Errors.sol'; +import {IVaultValidators} from '../interfaces/IVaultValidators.sol'; + +/** + * @title ValidatorUtils + * @author StakeWise + * @notice Includes functionality for managing the validators + */ +library ValidatorUtils { + bytes32 private constant _validatorsManagerTypeHash = + keccak256('VaultValidators(bytes32 nonce,bytes validators)'); + uint256 private constant _validatorV1DepositLength = 176; + uint256 private constant _validatorV2DepositLength = 184; + uint256 private constant _validatorWithdrawalLength = 56; + uint256 private constant _validatorConsolidationLength = 96; + uint256 private constant _validatorMinEffectiveBalance = 32 ether; + uint256 private constant _validatorMaxEffectiveBalance = 2048 ether; + + /* + * @dev Struct to hold the validator registration data + * @param publicKey The public key of the validator + * @param signature The signature of the validator + * @param withdrawalCredentials The withdrawal credentials of the validator + * @param depositDataRoot The deposit data root of the validator + * @param depositAmount The deposit amount of the validator + */ + struct ValidatorDeposit { + bytes publicKey; + bytes signature; + bytes withdrawalCredentials; + bytes32 depositDataRoot; + uint256 depositAmount; + } + + /** + * @dev Function to check if the validator signature is valid + * @param nonce The nonce of the validator + * @param domainSeparator The domain separator of the validator + * @param validatorsManager The address of the validators manager + * @param validators The validators data + * @param signature The signature of the validator + * @return Whether the signature is valid + */ + function isValidManagerSignature( + bytes32 nonce, + bytes32 domainSeparator, + address validatorsManager, + bytes calldata validators, + bytes calldata signature + ) external view returns (bool) { + bytes32 messageHash = MessageHashUtils.toTypedDataHash( + domainSeparator, + keccak256(abi.encode(_validatorsManagerTypeHash, nonce, keccak256(validators))) + ); + return SignatureChecker.isValidSignatureNow(validatorsManager, messageHash, signature); + } + + /** + * @dev Function to get the validator registration data + * @param validator The validator data + * @param isV1Validator Whether the validator is a V1 validator + * @return validatorDeposit The validator registration data + */ + function getValidatorDeposit( + bytes calldata validator, + bool isV1Validator + ) internal view returns (ValidatorDeposit memory validatorDeposit) { + validatorDeposit.publicKey = validator[:48]; + validatorDeposit.signature = validator[48:144]; + validatorDeposit.depositDataRoot = bytes32(validator[144:176]); + + // get the deposit amount and withdrawal credentials prefix + bytes1 withdrawalCredsPrefix; + if (isV1Validator) { + withdrawalCredsPrefix = 0x01; + validatorDeposit.depositAmount = _validatorMinEffectiveBalance; + } else { + withdrawalCredsPrefix = 0x02; + // extract amount from data, convert gwei to wei by multiplying by 1 gwei + validatorDeposit.depositAmount = (uint256(uint64(bytes8(validator[176:184]))) * 1 gwei); + } + validatorDeposit.withdrawalCredentials = abi.encodePacked( + withdrawalCredsPrefix, + bytes11(0x0), + address(this) + ); + } + + /** + * @dev Function to get the type of validators + * @param validatorsLength The length of the validators data + * @return isV1Validators Whether the validators are V1 validators + */ + function getIsV1Validators(uint256 validatorsLength) internal pure returns (bool) { + bool isV1Validators = validatorsLength % _validatorV1DepositLength == 0; + bool isV2Validators = validatorsLength % _validatorV2DepositLength == 0; + if ( + validatorsLength == 0 || + (isV1Validators && isV2Validators) || + (!isV1Validators && !isV2Validators) + ) { + revert Errors.InvalidValidators(); + } + + return isV1Validators; + } + + /** + * @dev Function to get the validator registrations + * @param v2Validators The mapping of public key hashes to registration status + * @param validators The validators data + * @param isTopUp Whether the registration is a top-up + * @return validatorDeposits The array of validator registrations + */ + function getValidatorDeposits( + mapping(bytes32 publicKeyHash => bool isRegistered) storage v2Validators, + bytes calldata validators, + bool isTopUp + ) external returns (ValidatorDeposit[] memory validatorDeposits) { + // check validators length is valid + uint256 validatorsLength = validators.length; + bool isV1Validators = getIsV1Validators(validatorsLength); + + // top up is only allowed for V2 validators + if (isTopUp && isV1Validators) { + revert Errors.CannotTopUpV1Validators(); + } + + uint256 _validatorDepositLength = ( + isV1Validators ? _validatorV1DepositLength : _validatorV2DepositLength + ); + uint256 validatorsCount = validatorsLength / _validatorDepositLength; + + uint256 startIndex; + ValidatorDeposit memory valDeposit; + validatorDeposits = new ValidatorDeposit[](validatorsCount); + for (uint256 i = 0; i < validatorsCount; ) { + valDeposit = getValidatorDeposit( + validators[startIndex:startIndex + _validatorDepositLength], + isV1Validators + ); + + if (isTopUp) { + // check whether validator is tracked in case of the top-up + if (!v2Validators[keccak256(valDeposit.publicKey)]) { + revert Errors.InvalidValidators(); + } + // add registration data to the array + validatorDeposits[i] = valDeposit; + emit IVaultValidators.ValidatorFunded(valDeposit.publicKey, valDeposit.depositAmount); + unchecked { + // cannot realistically overflow + ++i; + startIndex += _validatorDepositLength; + } + continue; + } + + // check the registration amount + if ( + valDeposit.depositAmount > _validatorMaxEffectiveBalance || + valDeposit.depositAmount < _validatorMinEffectiveBalance + ) { + revert Errors.InvalidAssets(); + } + + // mark v2 validator public key as tracked + if (!isV1Validators) { + v2Validators[keccak256(valDeposit.publicKey)] = true; + emit IVaultValidators.V2ValidatorRegistered(valDeposit.publicKey, valDeposit.depositAmount); + } else { + emit IVaultValidators.ValidatorRegistered(valDeposit.publicKey); + } + + // add registration data to the array + validatorDeposits[i] = valDeposit; + + unchecked { + // cannot realistically overflow + ++i; + startIndex += _validatorDepositLength; + } + } + } + + /** + * @dev Function to withdraw the validators + * @param validators The validators data + * @param validatorsWithdrawals The address of the validators withdrawals contract + */ + function withdrawValidators(bytes calldata validators, address validatorsWithdrawals) external { + // check validators length is valid + uint256 validatorsCount = validators.length / _validatorWithdrawalLength; + unchecked { + if (validatorsCount == 0 || validators.length % _validatorWithdrawalLength != 0) { + revert Errors.InvalidValidators(); + } + } + + uint256 feePaid; + uint256 withdrawnAmount; + uint256 totalFeeAssets = msg.value; + bytes calldata publicKey; + bytes calldata validator; + uint256 startIndex; + for (uint256 i = 0; i < validatorsCount; ) { + validator = validators[startIndex:startIndex + _validatorWithdrawalLength]; + publicKey = validator[:48]; + + // convert gwei to wei by multiplying by 1 gwei + withdrawnAmount = (uint256(uint64(bytes8(validator[48:56]))) * 1 gwei); + feePaid = uint256(bytes32(Address.functionStaticCall(validatorsWithdrawals, ''))); + + // submit validator withdrawal + Address.functionCallWithValue(validatorsWithdrawals, validator, feePaid); + totalFeeAssets -= feePaid; + emit IVaultValidators.ValidatorWithdrawalSubmitted(publicKey, withdrawnAmount, feePaid); + + unchecked { + // cannot realistically overflow + ++i; + startIndex += _validatorWithdrawalLength; + } + } + + // send the remaining assets to the caller + if (totalFeeAssets > 0) { + Address.sendValue(payable(msg.sender), totalFeeAssets); + } + } + + /** + * @dev Internal function for consolidating validators + * @param validator The validator data + * @param validatorsConsolidations The address of the validators consolidations contract + * @param fromPublicKey The public key of the validator that was consolidated + * @param toPublicKey The public key of the validator that was consolidated to + * @param feePaid The amount of fee that was paid + */ + function consolidateValidator( + bytes calldata validator, + address validatorsConsolidations + ) internal returns (bytes calldata fromPublicKey, bytes calldata toPublicKey, uint256 feePaid) { + fromPublicKey = validator[:48]; + toPublicKey = validator[48:96]; + feePaid = uint256(bytes32(Address.functionStaticCall(validatorsConsolidations, ''))); + + Address.functionCallWithValue(validatorsConsolidations, validator, feePaid); + } + + /** + * @dev Function to consolidate the validators + * @param v2Validators The mapping of public key hashes to registration status + * @param validators The validators data + * @param consolidationsApproved Whether the consolidations are approved + * @param validatorsConsolidations The address of the validators consolidations contract + */ + function consolidateValidators( + mapping(bytes32 publicKeyHash => bool isRegistered) storage v2Validators, + bytes calldata validators, + bool consolidationsApproved, + address validatorsConsolidations + ) external { + // Check validators length is valid + uint256 validatorsCount = validators.length / _validatorConsolidationLength; + unchecked { + if (validatorsCount == 0 || validators.length % _validatorConsolidationLength != 0) { + revert Errors.InvalidValidators(); + } + } + + uint256 totalFeeAssets = msg.value; + + // Process each validator + bytes32 destPubKeyHash; + bytes calldata sourcePublicKey; + bytes calldata destPublicKey; + uint256 feePaid; + uint256 startIndex; + for (uint256 i = 0; i < validatorsCount; ) { + // consolidate validators + (sourcePublicKey, destPublicKey, feePaid) = consolidateValidator( + validators[startIndex:startIndex + _validatorConsolidationLength], + validatorsConsolidations + ); + + // check whether the destination public key is tracked or approved + destPubKeyHash = keccak256(destPublicKey); + if (consolidationsApproved) { + v2Validators[destPubKeyHash] = true; + } else if (!v2Validators[destPubKeyHash]) { + revert Errors.InvalidValidators(); + } + + // Update fees and emit event + unchecked { + // cannot realistically overflow + totalFeeAssets -= feePaid; + startIndex += _validatorConsolidationLength; + ++i; + } + + // emit event + emit IVaultValidators.ValidatorConsolidationSubmitted( + sourcePublicKey, + destPublicKey, + feePaid + ); + } + + // refund unused fees + if (totalFeeAssets > 0) { + Address.sendValue(payable(msg.sender), totalFeeAssets); + } + } +} diff --git a/contracts/validators/ValidatorsChecker.sol b/contracts/validators/ValidatorsChecker.sol index 84de72aa..a078f9b7 100644 --- a/contracts/validators/ValidatorsChecker.sol +++ b/contracts/validators/ValidatorsChecker.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.22; import {MerkleProof} from '@openzeppelin/contracts/utils/cryptography/MerkleProof.sol'; -import {MessageHashUtils} from '@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol'; -import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; import {IValidatorsRegistry} from '../interfaces/IValidatorsRegistry.sol'; import {IKeeper} from '../interfaces/IKeeper.sol'; import {IValidatorsChecker} from '../interfaces/IValidatorsChecker.sol'; @@ -13,7 +11,8 @@ import {IVaultVersion} from '../interfaces/IVaultVersion.sol'; import {IDepositDataRegistry} from '../interfaces/IDepositDataRegistry.sol'; import {IVaultsRegistry} from '../interfaces/IVaultsRegistry.sol'; import {IVaultValidators} from '../interfaces/IVaultValidators.sol'; -import {Errors} from '../libraries/Errors.sol'; +import {EIP712Utils} from '../libraries/EIP712Utils.sol'; +import {ValidatorUtils} from '../libraries/ValidatorUtils.sol'; interface IVaultValidatorsV1 { function validatorsRoot() external view returns (bytes32); @@ -28,9 +27,6 @@ interface IVaultValidatorsV1 { * * checking deposit data root */ abstract contract ValidatorsChecker is IValidatorsChecker { - bytes32 private constant _registerValidatorsTypeHash = - keccak256('VaultValidators(bytes32 validatorsRegistryRoot,bytes validators)'); - IValidatorsRegistry private immutable _validatorsRegistry; IKeeper private immutable _keeper; IVaultsRegistry private immutable _vaultsRegistry; @@ -76,17 +72,17 @@ abstract contract ValidatorsChecker is IValidatorsChecker { return (block.number, Status.INSUFFICIENT_ASSETS); } - // compose signing message - bytes32 message = _getValidatorsManagerMessageHash(vault, validatorsRegistryRoot, validators); + // validate signature + bool isValidSignature = ValidatorUtils.isValidManagerSignature( + validatorsRegistryRoot, + _computeVaultValidatorsDomain(vault), + IVaultValidators(vault).validatorsManager(), + validators, + signature + ); // verify validators manager ECDSA signature - if ( - !SignatureChecker.isValidSignatureNow( - IVaultValidators(vault).validatorsManager(), - message, - signature - ) - ) { + if (!isValidSignature) { return (block.number, Status.INVALID_SIGNATURE); } @@ -178,46 +174,13 @@ abstract contract ValidatorsChecker is IValidatorsChecker { return (block.number, Status.SUCCEEDED); } - /** - * @notice Get the hash to be signed by the validators manager - * @param vault The address of the vault - * @param validatorsRegistryRoot The validators registry root - * @param validators The concatenation of the validators' public key, deposit signature, deposit root - * @return The hash to be signed by the validators manager - */ - function _getValidatorsManagerMessageHash( - address vault, - bytes32 validatorsRegistryRoot, - bytes calldata validators - ) private view returns (bytes32) { - bytes32 domainSeparator = _computeVaultValidatorsDomain(vault); - return - MessageHashUtils.toTypedDataHash( - domainSeparator, - keccak256( - abi.encode(_registerValidatorsTypeHash, validatorsRegistryRoot, keccak256(validators)) - ) - ); - } - /** * @notice Computes the hash of the EIP712 typed data for the vault * @dev This function is used to compute the hash of the EIP712 typed data * @return The hash of the EIP712 typed data */ function _computeVaultValidatorsDomain(address vault) private view returns (bytes32) { - return - keccak256( - abi.encode( - keccak256( - 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' - ), - keccak256(bytes('VaultValidators')), - keccak256('1'), - block.chainid, - vault - ) - ); + return EIP712Utils.computeDomainSeparator('VaultValidators', vault); } /** diff --git a/contracts/vaults/ethereum/EthGenesisVault.sol b/contracts/vaults/ethereum/EthGenesisVault.sol index ef245f12..7c938744 100644 --- a/contracts/vaults/ethereum/EthGenesisVault.sol +++ b/contracts/vaults/ethereum/EthGenesisVault.sol @@ -10,6 +10,7 @@ import {IEthPoolEscrow} from '../../interfaces/IEthPoolEscrow.sol'; import {IEthGenesisVault} from '../../interfaces/IEthGenesisVault.sol'; import {IRewardEthToken} from '../../interfaces/IRewardEthToken.sol'; import {Errors} from '../../libraries/Errors.sol'; +import {ValidatorUtils} from '../../libraries/ValidatorUtils.sol'; import {VaultValidators} from '../modules/VaultValidators.sol'; import {VaultEnterExit} from '../modules/VaultEnterExit.sol'; import {VaultEthStaking} from '../modules/VaultEthStaking.sol'; @@ -153,17 +154,11 @@ contract EthGenesisVault is Initializable, EthVault, IEthGenesisVault { } /// @inheritdoc VaultValidators - function _registerValidator( - bytes calldata validator, - bool isV1Validator - ) - internal - virtual - override(VaultValidators, VaultEthStaking) - returns (uint256 depositAmount, bytes calldata publicKey) - { + function _registerValidators( + ValidatorUtils.ValidatorDeposit[] memory deposits + ) internal virtual override(VaultValidators, VaultEthStaking) { _pullWithdrawals(); - return super._registerValidator(validator, isV1Validator); + return super._registerValidators(deposits); } /** diff --git a/contracts/vaults/gnosis/GnoErc20Vault.sol b/contracts/vaults/gnosis/GnoErc20Vault.sol index 0d0e4bfe..b0df4e13 100644 --- a/contracts/vaults/gnosis/GnoErc20Vault.sol +++ b/contracts/vaults/gnosis/GnoErc20Vault.sol @@ -180,17 +180,6 @@ contract GnoErc20Vault is } } - /// @inheritdoc VaultValidators - function _withdrawValidator( - bytes calldata validator - ) - internal - override(VaultValidators, VaultGnoStaking) - returns (bytes calldata publicKey, uint256 withdrawnAmount, uint256 feePaid) - { - return super._withdrawValidator(validator); - } - /** * @dev Upgrades the GnoErc20Vault contract */ diff --git a/contracts/vaults/gnosis/GnoVault.sol b/contracts/vaults/gnosis/GnoVault.sol index f91f563e..5fb0bff2 100644 --- a/contracts/vaults/gnosis/GnoVault.sol +++ b/contracts/vaults/gnosis/GnoVault.sol @@ -128,17 +128,6 @@ contract GnoVault is } } - /// @inheritdoc VaultValidators - function _withdrawValidator( - bytes calldata validator - ) - internal - override(VaultValidators, VaultGnoStaking) - returns (bytes calldata publicKey, uint256 withdrawnAmount, uint256 feePaid) - { - return super._withdrawValidator(validator); - } - /** * @dev Upgrades the GnoVault contract */ diff --git a/contracts/vaults/modules/VaultEnterExit.sol b/contracts/vaults/modules/VaultEnterExit.sol index 04d692c4..20fc091b 100644 --- a/contracts/vaults/modules/VaultEnterExit.sol +++ b/contracts/vaults/modules/VaultEnterExit.sol @@ -181,10 +181,10 @@ abstract contract VaultEnterExit is VaultImmutables, Initializable, VaultState, } // SLOAD to memory - uint256 _queuedShares = queuedShares; + uint256 queuedShares = _queuedShares; // calculate position ticket - positionTicket = _exitQueue.getLatestTotalTickets() + _totalExitingTickets + _queuedShares; + positionTicket = _exitQueue.getLatestTotalTickets() + _totalExitingTickets + queuedShares; // add to the exit requests _exitRequests[keccak256(abi.encode(receiver, block.timestamp, positionTicket))] = shares; @@ -194,7 +194,7 @@ abstract contract VaultEnterExit is VaultImmutables, Initializable, VaultState, unchecked { // cannot overflow as it is capped with _totalShares - queuedShares = SafeCast.toUint128(_queuedShares + shares); + _queuedShares = SafeCast.toUint128(queuedShares + shares); } emit ExitQueueEntered(user, receiver, positionTicket, shares); diff --git a/contracts/vaults/modules/VaultEthStaking.sol b/contracts/vaults/modules/VaultEthStaking.sol index 30533ef5..47483136 100644 --- a/contracts/vaults/modules/VaultEthStaking.sol +++ b/contracts/vaults/modules/VaultEthStaking.sol @@ -8,6 +8,7 @@ import {IEthValidatorsRegistry} from '../../interfaces/IEthValidatorsRegistry.so import {IKeeperRewards} from '../../interfaces/IKeeperRewards.sol'; import {IVaultEthStaking} from '../../interfaces/IVaultEthStaking.sol'; import {Errors} from '../../libraries/Errors.sol'; +import {ValidatorUtils} from '../../libraries/ValidatorUtils.sol'; import {VaultValidators} from './VaultValidators.sol'; import {VaultState} from './VaultState.sol'; import {VaultEnterExit} from './VaultEnterExit.sol'; @@ -59,32 +60,30 @@ abstract contract VaultEthStaking is } /// @inheritdoc VaultValidators - function _registerValidator( - bytes calldata validator, - bool isV1Validator - ) internal virtual override returns (uint256 depositAmount, bytes calldata publicKey) { - publicKey = validator[:48]; - bytes calldata signature = validator[48:144]; - bytes32 depositDataRoot = bytes32(validator[144:176]); - - // get the deposit amount and withdrawal credentials prefix - bytes1 withdrawalCredsPrefix; - if (isV1Validator) { - withdrawalCredsPrefix = 0x01; - depositAmount = _validatorMinEffectiveBalance(); - } else { - withdrawalCredsPrefix = 0x02; - // extract amount from data, convert gwei to wei by multiplying by 1 gwei - depositAmount = (uint256(uint64(bytes8(validator[176:184]))) * 1 gwei); + function _registerValidators( + ValidatorUtils.ValidatorDeposit[] memory deposits + ) internal virtual override { + uint256 totalDeposits = deposits.length; + uint256 availableAssets = withdrawableAssets(); + ValidatorUtils.ValidatorDeposit memory depositData; + for (uint256 i = 0; i < totalDeposits; ) { + depositData = deposits[i]; + // deposit to the validators registry + IEthValidatorsRegistry(_validatorsRegistry).deposit{value: depositData.depositAmount}( + depositData.publicKey, + depositData.withdrawalCredentials, + depositData.signature, + depositData.depositDataRoot + ); + + // will revert if not enough assets + availableAssets -= depositData.depositAmount; + + unchecked { + // cannot realistically overflow + ++i; + } } - - // deposit to the validators registry - IEthValidatorsRegistry(_validatorsRegistry).deposit{value: depositAmount}( - publicKey, - abi.encodePacked(withdrawalCredsPrefix, bytes11(0x0), address(this)), - signature, - depositDataRoot - ); } /// @inheritdoc VaultState @@ -100,16 +99,6 @@ abstract contract VaultEthStaking is return Address.sendValue(payable(receiver), assets); } - /// @inheritdoc VaultValidators - function _validatorMinEffectiveBalance() internal pure virtual override returns (uint256) { - return 32 ether; - } - - /// @inheritdoc VaultValidators - function _validatorMaxEffectiveBalance() internal pure virtual override returns (uint256) { - return 2048 ether; - } - /** * @dev Initializes the VaultEthStaking contract */ diff --git a/contracts/vaults/modules/VaultGnoStaking.sol b/contracts/vaults/modules/VaultGnoStaking.sol index 8f483237..84aacd6e 100644 --- a/contracts/vaults/modules/VaultGnoStaking.sol +++ b/contracts/vaults/modules/VaultGnoStaking.sol @@ -8,7 +8,7 @@ import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {IGnoValidatorsRegistry} from '../../interfaces/IGnoValidatorsRegistry.sol'; import {IVaultGnoStaking} from '../../interfaces/IVaultGnoStaking.sol'; import {IGnoDaiDistributor} from '../../interfaces/IGnoDaiDistributor.sol'; -import {Errors} from '../../libraries/Errors.sol'; +import {ValidatorUtils} from '../../libraries/ValidatorUtils.sol'; import {VaultAdmin} from './VaultAdmin.sol'; import {VaultState} from './VaultState.sol'; import {VaultValidators} from './VaultValidators.sol'; @@ -72,51 +72,38 @@ abstract contract VaultGnoStaking is receive() external payable {} /// @inheritdoc VaultValidators - function _registerValidator( - bytes calldata validator, - bool isV1Validator - ) internal virtual override returns (uint256 depositAmount, bytes calldata publicKey) { + function _registerValidators( + ValidatorUtils.ValidatorDeposit[] memory deposits + ) internal virtual override { // pull withdrawals from the deposit contract _pullWithdrawals(); - publicKey = validator[:48]; - bytes calldata signature = validator[48:144]; - bytes32 depositDataRoot = bytes32(validator[144:176]); - - // get the deposit amount and withdrawal credentials prefix - bytes1 withdrawalCredsPrefix; - if (isV1Validator) { - withdrawalCredsPrefix = 0x01; - depositAmount = _validatorMinEffectiveBalance(); - } else { - withdrawalCredsPrefix = 0x02; - // extract amount from data, convert gwei to wei by multiplying by 1 gwei + uint256 totalDeposits = deposits.length; + uint256 availableAssets = withdrawableAssets(); + ValidatorUtils.ValidatorDeposit memory depositData; + for (uint256 i = 0; i < totalDeposits; ) { + depositData = deposits[i]; + // divide by 32 to convert mGNO to GNO - depositAmount = (uint256(uint64(bytes8(validator[176:184]))) * 1 gwei) / 32; + depositData.depositAmount /= 32; + + // deposit GNO tokens to the validators registry + IGnoValidatorsRegistry(_validatorsRegistry).deposit( + depositData.publicKey, + depositData.withdrawalCredentials, + depositData.signature, + depositData.depositDataRoot, + depositData.depositAmount + ); + + // will revert if not enough assets + availableAssets -= depositData.depositAmount; + + unchecked { + // cannot realistically overflow + ++i; + } } - - // deposit GNO tokens to the validators registry - IGnoValidatorsRegistry(_validatorsRegistry).deposit( - publicKey, - abi.encodePacked(withdrawalCredsPrefix, bytes11(0x0), address(this)), - signature, - depositDataRoot, - depositAmount - ); - } - - /// @inheritdoc VaultValidators - function _withdrawValidator( - bytes calldata validator - ) - internal - virtual - override - returns (bytes calldata publicKey, uint256 withdrawnAmount, uint256 feePaid) - { - (publicKey, withdrawnAmount, feePaid) = super._withdrawValidator(validator); - // convert mGNO to GNO - withdrawnAmount /= 32; } /// @inheritdoc VaultState @@ -137,16 +124,6 @@ abstract contract VaultGnoStaking is SafeERC20.safeTransfer(_gnoToken, receiver, assets); } - /// @inheritdoc VaultValidators - function _validatorMinEffectiveBalance() internal pure override returns (uint256) { - return 1 ether; - } - - /// @inheritdoc VaultValidators - function _validatorMaxEffectiveBalance() internal pure override returns (uint256) { - return 64 ether; - } - /** * @dev Pulls assets from withdrawal contract */ diff --git a/contracts/vaults/modules/VaultOsToken.sol b/contracts/vaults/modules/VaultOsToken.sol index 5c077ace..2bc30b78 100644 --- a/contracts/vaults/modules/VaultOsToken.sol +++ b/contracts/vaults/modules/VaultOsToken.sol @@ -12,6 +12,7 @@ import {Errors} from '../../libraries/Errors.sol'; import {VaultImmutables} from './VaultImmutables.sol'; import {VaultEnterExit, IVaultEnterExit} from './VaultEnterExit.sol'; import {VaultState} from './VaultState.sol'; +import {OsTokenUtils} from '../../libraries/OsTokenUtils.sol'; /** * @title VaultOsToken @@ -19,10 +20,7 @@ import {VaultState} from './VaultState.sol'; * @notice Defines the functionality for minting OsToken */ abstract contract VaultOsToken is VaultImmutables, VaultState, VaultEnterExit, IVaultOsToken { - uint256 private constant _wad = 1e18; - uint256 private constant _hfLiqThreshold = 1e18; uint256 private constant _maxPercent = 1e18; - uint256 private constant _disabledLiqThreshold = type(uint64).max; /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IOsTokenVaultController private immutable _osTokenVaultController; @@ -254,44 +252,18 @@ abstract contract VaultOsToken is VaultImmutables, VaultState, VaultEnterExit, I if (position.shares == 0) revert Errors.InvalidPosition(); _syncPositionFee(position); - // SLOAD to memory - IOsTokenConfig.Config memory osTokenConfig = _osTokenConfig.getConfig(address(this)); - if (isLiquidation && osTokenConfig.liqThresholdPercent == _disabledLiqThreshold) { - revert Errors.LiquidationDisabled(); - } - // calculate received assets - if (isLiquidation) { - receivedAssets = Math.mulDiv( - _osTokenVaultController.convertToAssets(osTokenShares), - osTokenConfig.liqBonusPercent, - _maxPercent - ); - } else { - receivedAssets = _osTokenVaultController.convertToAssets(osTokenShares); - } - - { - // check whether received assets are valid - uint256 depositedAssets = convertToAssets(_balances[owner]); - if (receivedAssets > depositedAssets || receivedAssets > withdrawableAssets()) { - revert Errors.InvalidReceivedAssets(); - } - - uint256 mintedAssets = _osTokenVaultController.convertToAssets(position.shares); - if (isLiquidation) { - // check health factor violation in case of liquidation - if ( - Math.mulDiv( - depositedAssets * _wad, - osTokenConfig.liqThresholdPercent, - mintedAssets * _maxPercent - ) >= _hfLiqThreshold - ) { - revert Errors.InvalidHealthFactor(); - } - } - } + receivedAssets = OsTokenUtils.calculateReceivedAssets( + _osTokenConfig, + _osTokenVaultController, + OsTokenUtils.RedemptionData({ + mintedAssets: _osTokenVaultController.convertToAssets(position.shares), + depositedAssets: convertToAssets(_balances[owner]), + redeemedOsTokenShares: osTokenShares, + availableAssets: withdrawableAssets(), + isLiquidation: isLiquidation + }) + ); // reduce osToken supply _osTokenVaultController.burnShares(msg.sender, osTokenShares); diff --git a/contracts/vaults/modules/VaultState.sol b/contracts/vaults/modules/VaultState.sol index c8a03618..759803dd 100644 --- a/contracts/vaults/modules/VaultState.sol +++ b/contracts/vaults/modules/VaultState.sol @@ -23,8 +23,7 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault uint128 internal _totalShares; uint128 internal _totalAssets; - /// @inheritdoc IVaultState - uint128 public override queuedShares; + uint128 internal _queuedShares; uint128 internal _unclaimedAssets; ExitQueue.History internal _exitQueue; @@ -33,8 +32,7 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault uint256 private _capacity; - /// @inheritdoc IVaultState - uint128 public override totalExitingAssets; // deprecated + uint128 internal _totalExitingAssets; // deprecated uint128 internal _totalExitingTickets; // deprecated uint256 internal _totalExitedTickets; // deprecated @@ -48,6 +46,26 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault return _totalAssets; } + /// @inheritdoc IVaultState + function getExitQueueData() + external + view + override + returns ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) + { + return ( + _queuedShares, + _unclaimedAssets, + _totalExitingAssets, + ExitQueue.getLatestTotalTickets(_exitQueue) + ); + } + /// @inheritdoc IVaultState function getShares(address account) external view override returns (uint256) { return _balances[account]; @@ -79,8 +97,8 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault unchecked { // calculate assets that are reserved by users who queued for exit // cannot overflow as it is capped with underlying asset total supply - uint256 reservedAssets = convertToAssets(queuedShares) + - totalExitingAssets + + uint256 reservedAssets = convertToAssets(_queuedShares) + + _totalExitingAssets + _unclaimedAssets; return vaultAssets > reservedAssets ? vaultAssets - reservedAssets : 0; } @@ -119,21 +137,21 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault uint256 penalty = uint256(-totalAssetsDelta); // SLOAD to memory - uint256 _totalExitingAssets = totalExitingAssets; - if (_totalExitingAssets > 0) { + uint256 totalExitingAssets = _totalExitingAssets; + if (totalExitingAssets > 0) { // apply penalty to exiting assets uint256 exitingAssetsPenalty = Math.mulDiv( penalty, - _totalExitingAssets, - _totalExitingAssets + newTotalAssets + totalExitingAssets, + totalExitingAssets + newTotalAssets ); // apply penalty to total exiting assets unchecked { // cannot underflow as exitingAssetsPenalty <= penalty penalty -= exitingAssetsPenalty; - // cannot underflow as exitingAssetsPenalty <= _totalExitingAssets - totalExitingAssets = SafeCast.toUint128(_totalExitingAssets - exitingAssetsPenalty); + // cannot underflow as exitingAssetsPenalty <= totalExitingAssets + _totalExitingAssets = SafeCast.toUint128(totalExitingAssets - exitingAssetsPenalty); } emit ExitingAssetsPenalized(exitingAssetsPenalty); } @@ -192,35 +210,35 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault if (availableAssets == 0) return 0; // SLOAD to memory - uint256 _totalExitingAssets = totalExitingAssets; - if (_totalExitingAssets > 0) { + uint256 totalExitingAssets = _totalExitingAssets; + if (totalExitingAssets > 0) { // wait for all the exiting assets from v2 to be processed - if (availableAssets < _totalExitingAssets) return 0; + if (availableAssets < totalExitingAssets) return 0; // SLOAD to memory - uint256 totalExitingTickets = _totalExitingTickets; + uint256 totalExitingTickets = totalExitingAssets; // push checkpoint so that exited assets could be claimed - _exitQueue.push(totalExitingTickets, _totalExitingAssets); - emit CheckpointCreated(totalExitingTickets, _totalExitingAssets); + _exitQueue.push(totalExitingTickets, totalExitingAssets); + emit CheckpointCreated(totalExitingTickets, totalExitingAssets); unchecked { // cannot underflow as _totalExitingAssets <= availableAssets - availableAssets -= _totalExitingAssets; + availableAssets -= totalExitingAssets; } // update state - _unclaimedAssets += SafeCast.toUint128(_totalExitingAssets); + _unclaimedAssets += SafeCast.toUint128(totalExitingAssets); _totalExitingTickets = 0; - totalExitingAssets = 0; + _totalExitingAssets = 0; } // SLOAD to memory - uint256 _queuedShares = queuedShares; - if (_queuedShares == 0 || availableAssets == 0) return 0; + uint256 queuedShares = _queuedShares; + if (queuedShares == 0 || availableAssets == 0) return 0; // calculate the amount of assets that can be exited - uint256 exitedAssets = Math.min(availableAssets, convertToAssets(_queuedShares)); + uint256 exitedAssets = Math.min(availableAssets, convertToAssets(queuedShares)); if (exitedAssets == 0) return 0; // calculate the amount of shares that can be burned @@ -228,7 +246,7 @@ abstract contract VaultState is VaultImmutables, Initializable, VaultFee, IVault if (burnedShares == 0) return 0; // update queued shares and unclaimed assets - queuedShares = SafeCast.toUint128(_queuedShares - burnedShares); + _queuedShares = SafeCast.toUint128(queuedShares - burnedShares); _unclaimedAssets += SafeCast.toUint128(exitedAssets); // push checkpoint so that exited assets could be claimed diff --git a/contracts/vaults/modules/VaultValidators.sol b/contracts/vaults/modules/VaultValidators.sol index 34c6f29b..07cfad67 100644 --- a/contracts/vaults/modules/VaultValidators.sol +++ b/contracts/vaults/modules/VaultValidators.sol @@ -2,16 +2,15 @@ pragma solidity ^0.8.22; -import {Address} from '@openzeppelin/contracts/utils/Address.sol'; import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; import {ReentrancyGuardUpgradeable} from '@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol'; -import {MessageHashUtils} from '@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol'; -import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol'; import {IKeeperValidators} from '../../interfaces/IKeeperValidators.sol'; import {IVaultValidators} from '../../interfaces/IVaultValidators.sol'; import {IConsolidationsChecker} from '../../interfaces/IConsolidationsChecker.sol'; import {IDepositDataRegistry} from '../../interfaces/IDepositDataRegistry.sol'; import {Errors} from '../../libraries/Errors.sol'; +import {ValidatorUtils} from '../../libraries/ValidatorUtils.sol'; +import {EIP712Utils} from '../../libraries/EIP712Utils.sol'; import {VaultImmutables} from './VaultImmutables.sol'; import {VaultAdmin} from './VaultAdmin.sol'; import {VaultState} from './VaultState.sol'; @@ -29,13 +28,6 @@ abstract contract VaultValidators is VaultState, IVaultValidators { - bytes32 private constant _validatorsManagerTypeHash = - keccak256('VaultValidators(bytes32 validatorsRegistryRoot,bytes validators)'); - uint256 internal constant _validatorV1DepositLength = 176; - uint256 internal constant _validatorV2DepositLength = 184; - uint256 private constant _validatorWithdrawalLength = 56; - uint256 private constant _validatorConsolidationLength = 96; - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address private immutable _depositDataRegistry; @@ -111,12 +103,27 @@ abstract contract VaultValidators is ) external override { // check whether oracles have approve validators registration IKeeperValidators(_keeper).approveValidators(keeperParams); - _registerValidators( - keeperParams.validators, - keeperParams.validatorsRegistryRoot, - validatorsManagerSignature, - false - ); + + // check vault is up to date + _checkHarvested(); + + // check access + if ( + !_isValidatorsManager( + keeperParams.validators, + keeperParams.validatorsRegistryRoot, + validatorsManagerSignature + ) + ) { + revert Errors.AccessDenied(); + } + + // get validator deposits + ValidatorUtils.ValidatorDeposit[] memory validatorDeposits = ValidatorUtils + .getValidatorDeposits(v2Validators, keeperParams.validators, false); + + // register validators + _registerValidators(validatorDeposits); } /// @inheritdoc IVaultValidators @@ -124,12 +131,22 @@ abstract contract VaultValidators is bytes calldata validators, bytes calldata validatorsManagerSignature ) external override { - _registerValidators( - validators, - bytes32(validatorsManagerNonce), - validatorsManagerSignature, - true - ); + // check vault is up to date + _checkHarvested(); + + // check access + if ( + !_isValidatorsManager(validators, bytes32(validatorsManagerNonce), validatorsManagerSignature) + ) { + revert Errors.AccessDenied(); + } + + // get validator deposits + ValidatorUtils.ValidatorDeposit[] memory validatorDeposits = ValidatorUtils + .getValidatorDeposits(v2Validators, validators, true); + + // top up validators + _registerValidators(validatorDeposits); } /// @inheritdoc IVaultValidators @@ -139,37 +156,7 @@ abstract contract VaultValidators is ) external payable override nonReentrant { _checkCollateralized(); _checkCanWithdrawValidators(validators, validatorsManagerSignature); - - // check validators length is valid - uint256 validatorsCount = validators.length / _validatorWithdrawalLength; - unchecked { - if (validatorsCount == 0 || validators.length % _validatorWithdrawalLength != 0) { - revert Errors.InvalidValidators(); - } - } - - uint256 feePaid; - uint256 withdrawnAmount; - uint256 totalFeeAssets = msg.value; - bytes calldata publicKey; - uint256 startIndex; - for (uint256 i = 0; i < validatorsCount; ) { - (publicKey, withdrawnAmount, feePaid) = _withdrawValidator( - validators[startIndex:startIndex + _validatorWithdrawalLength] - ); - totalFeeAssets -= feePaid; - emit ValidatorWithdrawalSubmitted(publicKey, withdrawnAmount, feePaid); - - unchecked { - // cannot realistically overflow - ++i; - startIndex += _validatorWithdrawalLength; - } - } - - if (totalFeeAssets > 0) { - Address.sendValue(payable(msg.sender), totalFeeAssets); - } + ValidatorUtils.withdrawValidators(validators, _validatorsWithdrawals); } /// @inheritdoc IVaultValidators @@ -177,7 +164,7 @@ abstract contract VaultValidators is bytes calldata validators, bytes calldata validatorsManagerSignature, bytes calldata oracleSignatures - ) external payable override { + ) external payable override nonReentrant { _checkCollateralized(); if ( !_isValidatorsManager(validators, bytes32(validatorsManagerNonce), validatorsManagerSignature) @@ -185,14 +172,6 @@ abstract contract VaultValidators is revert Errors.AccessDenied(); } - // Check validators length is valid - uint256 validatorsCount = validators.length / _validatorConsolidationLength; - unchecked { - if (validatorsCount == 0 || validators.length % _validatorConsolidationLength != 0) { - revert Errors.InvalidValidators(); - } - } - // Check for oracle approval if signatures provided bool consolidationsApproved = false; if (oracleSignatures.length > 0) { @@ -205,58 +184,12 @@ abstract contract VaultValidators is consolidationsApproved = true; } - // Process the consolidation in smaller batches to avoid stack depth issues - _processConsolidation(validators, validatorsCount, consolidationsApproved); - } - - /** - * @dev Internal function to process validator consolidations - * @param validators The concatenated validators data - * @param validatorsCount The number of validators to consolidate - * @param consolidationsApproved Whether the consolidations are approved by oracles - */ - function _processConsolidation( - bytes calldata validators, - uint256 validatorsCount, - bool consolidationsApproved - ) private nonReentrant { - uint256 totalFeeAssets = msg.value; - - // Process each validator - bytes32 destPubKeyHash; - bytes calldata sourcePublicKey; - bytes calldata destPublicKey; - uint256 feePaid; - uint256 startIndex; - for (uint256 i = 0; i < validatorsCount; ) { - // consolidate validators - (sourcePublicKey, destPublicKey, feePaid) = _consolidateValidator( - validators[startIndex:startIndex + _validatorConsolidationLength] - ); - - // check whether the destination public key is tracked or approved - destPubKeyHash = keccak256(destPublicKey); - if (consolidationsApproved) { - v2Validators[destPubKeyHash] = true; - } else if (!v2Validators[destPubKeyHash]) { - revert Errors.InvalidValidators(); - } - - // Update fees and emit event - unchecked { - // cannot realistically overflow - totalFeeAssets -= feePaid; - startIndex += _validatorConsolidationLength; - ++i; - } - - emit ValidatorConsolidationSubmitted(sourcePublicKey, destPublicKey, feePaid); - } - - // refund unused fees - if (totalFeeAssets > 0) { - Address.sendValue(payable(msg.sender), totalFeeAssets); - } + ValidatorUtils.consolidateValidators( + v2Validators, + validators, + consolidationsApproved, + _validatorsConsolidations + ); } /// @inheritdoc IVaultValidators @@ -267,153 +200,11 @@ abstract contract VaultValidators is emit ValidatorsManagerUpdated(msg.sender, validatorsManager_); } - /** - * @dev Internal function for registering validator - * @param validator The validator registration data - * @param isV1Validator Whether the validator is V1 or V2 - * @return depositAmount The amount of assets that was deposited - * @return publicKey The public key of the registered validator - */ - function _registerValidator( - bytes calldata validator, - bool isV1Validator - ) internal virtual returns (uint256 depositAmount, bytes calldata publicKey); - - /** - * @dev Internal function for withdrawing validator - * @param validator The validator withdrawal data - * @return publicKey The public key of the withdrawn validator - * @return withdrawnAmount The amount of assets that was withdrawn - * @return feePaid The amount of fee that was paid - */ - function _withdrawValidator( - bytes calldata validator - ) internal virtual returns (bytes calldata publicKey, uint256 withdrawnAmount, uint256 feePaid) { - publicKey = validator[:48]; - // convert gwei to wei by multiplying by 1 gwei - withdrawnAmount = (uint256(uint64(bytes8(validator[48:56]))) * 1 gwei); - feePaid = uint256(bytes32(Address.functionStaticCall(_validatorsWithdrawals, ''))); - - Address.functionCallWithValue(_validatorsWithdrawals, validator, feePaid); - } - - /** - * @dev Internal function for consolidating validators - * @param fromPublicKey The public key of the validator that was consolidated - * @param toPublicKey The public key of the validator that was consolidated to - * @param feePaid The amount of fee that was paid - */ - function _consolidateValidator( - bytes calldata validator - ) private returns (bytes calldata fromPublicKey, bytes calldata toPublicKey, uint256 feePaid) { - fromPublicKey = validator[:48]; - toPublicKey = validator[48:96]; - feePaid = uint256(bytes32(Address.functionStaticCall(_validatorsConsolidations, ''))); - - Address.functionCallWithValue(_validatorsConsolidations, validator, feePaid); - } - - /** - * @dev Internal function for fetching validator minimum effective balance - * @return The minimum effective balance for the validator - */ - function _validatorMinEffectiveBalance() internal pure virtual returns (uint256); - - /** - * @dev Internal function for fetching validator maximum effective balance - * @return The maximum effective balance for the validator - */ - function _validatorMaxEffectiveBalance() internal pure virtual returns (uint256); - /** * @dev Internal function for registering validators - * @param validators The concatenated validators data - * @param nonce The nonce of the signature - * @param validatorsManagerSignature The optional signature from the validators manager - * @param isTopUp Whether the registration is a balance top-up + * @param deposits The validators registration data */ - function _registerValidators( - bytes calldata validators, - bytes32 nonce, - bytes calldata validatorsManagerSignature, - bool isTopUp - ) private { - // check vault is up to date - _checkHarvested(); - - // check access - if (!_isValidatorsManager(validators, nonce, validatorsManagerSignature)) { - revert Errors.AccessDenied(); - } - - // check validators length is valid - uint256 validatorsLength = validators.length; - bool isV1Validators = validatorsLength % _validatorV1DepositLength == 0; - bool isV2Validators = validatorsLength % _validatorV2DepositLength == 0; - if ( - validatorsLength == 0 || - (isV1Validators && isV2Validators) || - (!isV1Validators && !isV2Validators) - ) { - revert Errors.InvalidValidators(); - } - - // top up is only allowed for V2 validators - if (isTopUp && isV1Validators) { - revert Errors.CannotTopUpV1Validators(); - } - - uint256 _validatorDepositLength = ( - isV1Validators ? _validatorV1DepositLength : _validatorV2DepositLength - ); - uint256 validatorsCount = validatorsLength / _validatorDepositLength; - - uint256 startIndex; - uint256 availableDeposits = withdrawableAssets(); - bytes calldata validators_ = validators; // push down the stack - for (uint256 i = 0; i < validatorsCount; ) { - (uint256 depositAmount, bytes calldata publicKey) = _registerValidator( - validators_[startIndex:startIndex + _validatorDepositLength], - isV1Validators - ); - availableDeposits -= depositAmount; - - bytes32 publicKeyHash = keccak256(publicKey); - if (isTopUp) { - // check whether validator is tracked in case of the top-up - if (!v2Validators[publicKeyHash]) revert Errors.InvalidValidators(); - emit ValidatorFunded(publicKey, depositAmount); - unchecked { - // cannot realistically overflow - ++i; - startIndex += _validatorDepositLength; - } - continue; - } - - // check the registration amount - if ( - depositAmount > _validatorMaxEffectiveBalance() || - depositAmount < _validatorMinEffectiveBalance() - ) { - revert Errors.InvalidAssets(); - } - - // mark v2 validator public key as tracked - if (!isV1Validators) { - v2Validators[publicKeyHash] = true; - emit V2ValidatorRegistered(publicKey, depositAmount); - } else { - emit ValidatorRegistered(publicKey); - } - - unchecked { - // cannot realistically overflow - ++i; - startIndex += _validatorDepositLength; - } - } - } + function _registerValidators(ValidatorUtils.ValidatorDeposit[] memory deposits) internal virtual; /** * @dev Internal function for checking whether the caller can withdraw validators @@ -450,9 +241,14 @@ abstract contract VaultValidators is } // check signature - bool isValidSignature = SignatureChecker.isValidSignatureNow( + bytes32 domainSeparator = block.chainid == _initialChainId + ? _initialDomainSeparator + : _computeVaultValidatorsDomain(); + bool isValidSignature = ValidatorUtils.isValidManagerSignature( + nonce, + domainSeparator, validatorsManager_, - _getValidatorsManagerSigningMessage(nonce, validators), + validators, validatorsManagerSignature ); @@ -467,45 +263,13 @@ abstract contract VaultValidators is return isValidSignature; } - /** - * @notice Get the message to be signed by the validators manager - * @param nonce The nonce of the message - * @param validators The concatenated validators data - * @return The message to be signed - */ - function _getValidatorsManagerSigningMessage( - bytes32 nonce, - bytes calldata validators - ) private view returns (bytes32) { - bytes32 domainSeparator = block.chainid == _initialChainId - ? _initialDomainSeparator - : _computeVaultValidatorsDomain(); - - return - MessageHashUtils.toTypedDataHash( - domainSeparator, - keccak256(abi.encode(_validatorsManagerTypeHash, nonce, keccak256(validators))) - ); - } - /** * @notice Computes the hash of the EIP712 typed data * @dev This function is used to compute the hash of the EIP712 typed data * @return The hash of the EIP712 typed data */ function _computeVaultValidatorsDomain() private view returns (bytes32) { - return - keccak256( - abi.encode( - keccak256( - 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' - ), - keccak256(bytes('VaultValidators')), - keccak256('1'), - block.chainid, - address(this) - ) - ); + return EIP712Utils.computeDomainSeparator('VaultValidators', address(this)); } /** @@ -534,11 +298,9 @@ abstract contract VaultValidators is /** * @dev Initializes the VaultValidators contract - * @dev NB! This initializer must be called after VaultState initializer */ function __VaultValidators_init() internal onlyInitializing { __ReentrancyGuard_init(); - if (capacity() < _validatorMinEffectiveBalance()) revert Errors.InvalidCapacity(); _initialDomainSeparator = _computeVaultValidatorsDomain(); } diff --git a/test/DepositDataRegistry.t.sol b/test/DepositDataRegistry.t.sol index 15326e21..fd7f52da 100644 --- a/test/DepositDataRegistry.t.sol +++ b/test/DepositDataRegistry.t.sol @@ -51,10 +51,13 @@ contract DepositDataRegistryTest is Test, EthHelpers { ), false ); + (uint128 queuedShares, uint128 unclaimedAssets, uint128 totalExitingAssets, ) = IEthVault( + validVault + ).getExitQueueData(); exitingAssets = - IEthVault(validVault).totalExitingAssets() + - IEthVault(validVault).convertToAssets(IEthVault(validVault).queuedShares()) + - address(validVault).balance; + totalExitingAssets + + IEthVault(validVault).convertToAssets(queuedShares) + + unclaimedAssets; invalidVault = makeAddr('invalidVault'); nonAdmin = makeAddr('nonAdmin'); diff --git a/test/EthBlocklistErc20Vault.t.sol b/test/EthBlocklistErc20Vault.t.sol index 907c8d32..320929ee 100644 --- a/test/EthBlocklistErc20Vault.t.sol +++ b/test/EthBlocklistErc20Vault.t.sol @@ -9,6 +9,11 @@ import {IEthErc20Vault} from '../contracts/interfaces/IEthErc20Vault.sol'; import {EthBlocklistErc20Vault} from '../contracts/vaults/ethereum/EthBlocklistErc20Vault.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthBlocklistErc20VaultTest is Test, EthHelpers { ForkContracts public contracts; EthBlocklistErc20Vault public vault; @@ -257,6 +262,12 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { _stopSnapshotGas(); EthBlocklistErc20Vault blocklistVault = EthBlocklistErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistErc20Vault')); assertEq(blocklistVault.version(), 5); assertEq(blocklistVault.admin(), admin); @@ -265,10 +276,12 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), 0); assertEq(blocklistVault.totalShares(), _securityDeposit); assertEq(blocklistVault.totalAssets(), _securityDeposit); - assertEq(blocklistVault.totalExitingAssets(), 0); + assertEq(queuedShares, 0); + assertEq(totalTickets, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.totalSupply(), _securityDeposit); assertEq(blocklistVault.symbol(), 'SW-ETH-1'); @@ -302,8 +315,8 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { uint256 totalSharesBefore = blocklistVault.totalShares(); uint256 totalAssetsBefore = blocklistVault.totalAssets(); - uint256 totalExitingAssetsBefore = blocklistVault.totalExitingAssets(); - uint256 queuedSharesBefore = blocklistVault.queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(blocklistVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV4(address(blocklistVault)).queuedShares(); uint256 senderBalanceBefore = blocklistVault.getShares(sender); assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistErc20Vault')); @@ -313,6 +326,7 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthBlocklistErc20Vault, address(blocklistVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistErc20Vault')); assertEq(blocklistVault.version(), 5); assertEq(blocklistVault.admin(), admin); @@ -321,10 +335,10 @@ contract EthBlocklistErc20VaultTest is Test, EthHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), queuedSharesBefore); + assertEq(queuedShares, queuedSharesBefore); assertEq(blocklistVault.totalShares(), totalSharesBefore); assertEq(blocklistVault.totalAssets(), totalAssetsBefore); - assertEq(blocklistVault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.getShares(sender), senderBalanceBefore); assertEq(blocklistVault.totalSupply(), totalSharesBefore); diff --git a/test/EthBlocklistVault.t.sol b/test/EthBlocklistVault.t.sol index c8364af4..4390f41b 100644 --- a/test/EthBlocklistVault.t.sol +++ b/test/EthBlocklistVault.t.sol @@ -10,6 +10,11 @@ import {IKeeperRewards} from '../contracts/interfaces/IKeeperRewards.sol'; import {EthBlocklistVault} from '../contracts/vaults/ethereum/EthBlocklistVault.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthBlocklistVaultTest is Test, EthHelpers { ForkContracts public contracts; EthBlocklistVault public vault; @@ -224,6 +229,12 @@ contract EthBlocklistVaultTest is Test, EthHelpers { _stopSnapshotGas(); EthBlocklistVault blocklistVault = EthBlocklistVault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistVault')); assertEq(blocklistVault.version(), 5); assertEq(blocklistVault.admin(), admin); @@ -232,11 +243,13 @@ contract EthBlocklistVaultTest is Test, EthHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), 0); assertEq(blocklistVault.totalShares(), _securityDeposit); assertEq(blocklistVault.totalAssets(), _securityDeposit); - assertEq(blocklistVault.totalExitingAssets(), 0); assertEq(blocklistVault.validatorsManagerNonce(), 0); + assertEq(queuedShares, 0); + assertEq(totalExitingAssets, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -259,8 +272,8 @@ contract EthBlocklistVaultTest is Test, EthHelpers { uint256 totalSharesBefore = blocklistVault.totalShares(); uint256 totalAssetsBefore = blocklistVault.totalAssets(); - uint256 totalExitingAssetsBefore = blocklistVault.totalExitingAssets(); - uint256 queuedSharesBefore = blocklistVault.queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(blocklistVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV4(address(blocklistVault)).queuedShares(); uint256 senderSharesBefore = blocklistVault.getShares(sender); assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistVault')); @@ -270,6 +283,8 @@ contract EthBlocklistVaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthBlocklistVault, address(blocklistVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = blocklistVault.getExitQueueData(); + assertEq(blocklistVault.vaultId(), keccak256('EthBlocklistVault')); assertEq(blocklistVault.version(), 5); assertEq(blocklistVault.admin(), admin); @@ -278,10 +293,10 @@ contract EthBlocklistVaultTest is Test, EthHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), queuedSharesBefore); + assertEq(queuedShares, queuedSharesBefore); assertEq(blocklistVault.totalShares(), totalSharesBefore); assertEq(blocklistVault.totalAssets(), totalAssetsBefore); - assertEq(blocklistVault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.getShares(sender), senderSharesBefore); } diff --git a/test/EthErc20Vault.t.sol b/test/EthErc20Vault.t.sol index a0c402d5..02182d99 100644 --- a/test/EthErc20Vault.t.sol +++ b/test/EthErc20Vault.t.sol @@ -11,6 +11,11 @@ import {Errors} from '../contracts/libraries/Errors.sol'; import {EthErc20Vault} from '../contracts/vaults/ethereum/EthErc20Vault.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthErc20VaultTest is Test, EthHelpers { ForkContracts public contracts; EthErc20Vault public vault; @@ -72,6 +77,12 @@ contract EthErc20VaultTest is Test, EthHelpers { _stopSnapshotGas(); EthErc20Vault erc20Vault = EthErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = erc20Vault.getExitQueueData(); assertEq(erc20Vault.vaultId(), keccak256('EthErc20Vault')); assertEq(erc20Vault.version(), 5); assertEq(erc20Vault.admin(), admin); @@ -79,14 +90,16 @@ contract EthErc20VaultTest is Test, EthHelpers { assertEq(erc20Vault.feePercent(), 1000); assertEq(erc20Vault.feeRecipient(), admin); assertEq(erc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(erc20Vault.queuedShares(), 0); assertEq(erc20Vault.totalShares(), _securityDeposit); assertEq(erc20Vault.totalAssets(), _securityDeposit); - assertEq(erc20Vault.totalExitingAssets(), 0); assertEq(erc20Vault.validatorsManagerNonce(), 0); assertEq(erc20Vault.totalSupply(), _securityDeposit); assertEq(erc20Vault.symbol(), 'SW-ETH-1'); assertEq(erc20Vault.name(), 'SW ETH Vault'); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -116,9 +129,9 @@ contract EthErc20VaultTest is Test, EthHelpers { uint256 totalSharesBefore = erc20Vault.totalShares(); uint256 totalAssetsBefore = erc20Vault.totalAssets(); - uint256 totalExitingAssetsBefore = erc20Vault.totalExitingAssets(); - uint256 queuedSharesBefore = erc20Vault.queuedShares(); uint256 senderBalanceBefore = erc20Vault.getShares(sender); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(erc20Vault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV4(address(erc20Vault)).queuedShares(); assertEq(erc20Vault.vaultId(), keccak256('EthErc20Vault')); assertEq(erc20Vault.version(), 4); @@ -127,6 +140,8 @@ contract EthErc20VaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthErc20Vault, address(erc20Vault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = erc20Vault.getExitQueueData(); + assertEq(erc20Vault.vaultId(), keccak256('EthErc20Vault')); assertEq(erc20Vault.version(), 5); assertEq(erc20Vault.admin(), admin); @@ -134,10 +149,10 @@ contract EthErc20VaultTest is Test, EthHelpers { assertEq(erc20Vault.feePercent(), 1000); assertEq(erc20Vault.feeRecipient(), admin); assertEq(erc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(erc20Vault.queuedShares(), queuedSharesBefore); + assertEq(queuedShares, queuedSharesBefore); assertEq(erc20Vault.totalShares(), totalSharesBefore); assertEq(erc20Vault.totalAssets(), totalAssetsBefore); - assertEq(erc20Vault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq(erc20Vault.validatorsManagerNonce(), 0); assertEq(erc20Vault.getShares(sender), senderBalanceBefore); assertEq(erc20Vault.totalSupply(), totalSharesBefore); @@ -465,7 +480,8 @@ contract EthErc20VaultTest is Test, EthHelpers { // Verify the exit queue was processed correctly // The queue might not be fully processed in one update, so we'll check that progress was made - assertLt(vault.queuedShares(), exitShares, 'Exit queue should be at least partially processed'); + (uint128 queuedShares, , , ) = vault.getExitQueueData(); + assertLt(queuedShares, exitShares, 'Exit queue should be at least partially processed'); } // Helper function to deposit ETH to the vault diff --git a/test/EthFoxVault.t.sol b/test/EthFoxVault.t.sol index d1250dad..588385bf 100644 --- a/test/EthFoxVault.t.sol +++ b/test/EthFoxVault.t.sol @@ -51,7 +51,8 @@ contract EthFoxVaultTest is Test, EthHelpers { ); address _vault = _getOrCreateVault(VaultType.EthFoxVault, admin, initParams, false); vault = EthFoxVault(payable(_vault)); - exitingAssets = vault.convertToAssets(vault.queuedShares()) + address(vault).balance; + (uint128 queuedShares, , , ) = vault.getExitQueueData(); + exitingAssets = vault.convertToAssets(queuedShares) + address(vault).balance; } function test_deployFails() public { @@ -194,7 +195,7 @@ contract EthFoxVaultTest is Test, EthHelpers { // Deposit ETH to get vault shares _depositToVault(address(vault), amount, sender, sender); - uint256 queueSharesBefore = vault.queuedShares(); + (uint256 queuedSharesBefore, , , ) = vault.getExitQueueData(); // Set blocklist manager vm.prank(admin); @@ -214,7 +215,8 @@ contract EthFoxVaultTest is Test, EthHelpers { // User's shares should be in exit queue assertEq(vault.getShares(sender), 0); - assertApproxEqAbs(vault.queuedShares(), queueSharesBefore + shares, 1); + (uint256 queuedShares, , , ) = vault.getExitQueueData(); + assertApproxEqAbs(queuedShares, queuedSharesBefore + shares, 1); } function test_ejectUserWithNoShares() public { @@ -222,7 +224,7 @@ contract EthFoxVaultTest is Test, EthHelpers { vm.prank(admin); vault.setBlocklistManager(blocklistManager); - uint256 queueSharesBefore = vault.queuedShares(); + (uint256 queuedSharesBefore, , , ) = vault.getExitQueueData(); // Eject user with no shares _startSnapshotGas('EthFoxVaultTest_test_ejectUserWithNoShares'); @@ -234,7 +236,8 @@ contract EthFoxVaultTest is Test, EthHelpers { assertTrue(vault.blockedAccounts(sender)); // No shares should be in exit queue - assertEq(vault.queuedShares(), queueSharesBefore); + (uint256 queuedShares, , , ) = vault.getExitQueueData(); + assertEq(queuedShares, queuedSharesBefore); } function test_ejectUserFailsFromNonBlocklistManager() public { diff --git a/test/EthGenesisVault.t.sol b/test/EthGenesisVault.t.sol index 923eb186..7069b027 100644 --- a/test/EthGenesisVault.t.sol +++ b/test/EthGenesisVault.t.sol @@ -12,6 +12,11 @@ import {EthGenesisVault} from '../contracts/vaults/ethereum/EthGenesisVault.sol' import {Errors} from '../contracts/libraries/Errors.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthGenesisVaultTest is Test, EthHelpers { ForkContracts public contracts; address public admin; @@ -63,8 +68,8 @@ contract EthGenesisVaultTest is Test, EthHelpers { vm.deal( vaultAddr, - existingVault.totalExitingAssets() + - existingVault.convertToAssets(existingVault.queuedShares()) + + IVaultStateV4(address(existingVault)).totalExitingAssets() + + existingVault.convertToAssets(IVaultStateV4(address(existingVault)).queuedShares()) + vaultAddr.balance ); _depositToVault(address(existingVault), 40 ether, user, user); @@ -76,14 +81,14 @@ contract EthGenesisVaultTest is Test, EthHelpers { // Record initial state uint256 initialTotalAssets = existingVault.totalAssets(); uint256 initialTotalShares = existingVault.totalShares(); - uint256 totalExitingAssetsBefore = existingVault.totalExitingAssets(); - uint256 queuedSharesBefore = existingVault.queuedShares(); 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); @@ -92,6 +97,8 @@ contract EthGenesisVaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthGenesisVault, address(existingVault)); _stopSnapshotGas(); + (uint128 queuedSharesAfter, , uint128 totalExitingAssetsAfter, ) = existingVault + .getExitQueueData(); assertEq(existingVault.vaultId(), keccak256('EthGenesisVault')); assertEq(existingVault.version(), 5); assertEq(existingVault.admin(), adminBefore); @@ -99,10 +106,10 @@ contract EthGenesisVaultTest is Test, EthHelpers { assertEq(existingVault.feePercent(), initialFeePercent); assertEq(existingVault.feeRecipient(), feeRecipient); assertEq(existingVault.validatorsManager(), validatorsManager); - assertEq(existingVault.queuedShares(), queuedSharesBefore); + assertEq(queuedSharesAfter, queuedSharesBefore); assertEq(existingVault.totalShares(), initialTotalShares); assertEq(existingVault.totalAssets(), initialTotalAssets); - assertEq(existingVault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssetsAfter, totalExitingAssetsBefore); assertEq(existingVault.validatorsManagerNonce(), 0); assertEq(existingVault.getShares(user), senderBalanceBefore); } @@ -255,11 +262,10 @@ contract EthGenesisVaultTest is Test, EthHelpers { // Add some ETH to the pool escrow uint256 escrowAmount = 40 ether; vm.deal(poolEscrow, poolEscrow.balance + escrowAmount); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); vm.deal( address(vault), - address(vault).balance + - vault.convertToAssets(vault.queuedShares()) + - vault.totalExitingAssets() + address(vault).balance + vault.convertToAssets(queuedShares) + totalExitingAssets ); // Record initial balances @@ -335,9 +341,10 @@ contract EthGenesisVaultTest is Test, EthHelpers { address vaultAddr = _getOrCreateVault(VaultType.EthGenesisVault, admin, initParams, false); EthGenesisVault vault = EthGenesisVault(payable(vaultAddr)); - uint256 vaultAmount = vault.totalExitingAssets() + - vault.convertToAssets(vault.queuedShares()) + - vaultAddr.balance; + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); + uint256 vaultAmount = address(vault).balance + + vault.convertToAssets(queuedShares) + + totalExitingAssets; uint256 depositAmount = 3 ether; uint256 shares = vault.convertToShares(depositAmount); diff --git a/test/EthOsTokenVaultEscrow.t.sol b/test/EthOsTokenVaultEscrow.t.sol index 0a179e8d..9f46a540 100644 --- a/test/EthOsTokenVaultEscrow.t.sol +++ b/test/EthOsTokenVaultEscrow.t.sol @@ -60,11 +60,10 @@ contract EthOsTokenVaultEscrowTest is Test, EthHelpers { vault = IEthVault(_vault); // Ensure the vault has enough ETH to process exit requests + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); vm.deal( address(vault), - address(vault).balance + - vault.totalExitingAssets() + - vault.convertToAssets(vault.queuedShares()) + address(vault).balance + vault.convertToAssets(queuedShares) + totalExitingAssets ); } diff --git a/test/EthPrivErc20Vault.t.sol b/test/EthPrivErc20Vault.t.sol index de082269..a5b72a09 100644 --- a/test/EthPrivErc20Vault.t.sol +++ b/test/EthPrivErc20Vault.t.sol @@ -10,6 +10,11 @@ import {IEthErc20Vault} from '../contracts/interfaces/IEthErc20Vault.sol'; import {EthPrivErc20Vault} from '../contracts/vaults/ethereum/EthPrivErc20Vault.sol'; import {IKeeperRewards} from '../contracts/interfaces/IKeeperRewards.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthPrivErc20VaultTest is Test, EthHelpers { ForkContracts public contracts; EthPrivErc20Vault public vault; @@ -364,6 +369,12 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { address _vault = _createVault(VaultType.EthPrivErc20Vault, admin, initParams, true); _stopSnapshotGas(); EthPrivErc20Vault privErc20Vault = EthPrivErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = privErc20Vault.getExitQueueData(); assertEq(privErc20Vault.vaultId(), keccak256('EthPrivErc20Vault')); assertEq(privErc20Vault.version(), 5); @@ -373,14 +384,16 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { assertEq(privErc20Vault.feePercent(), 1000); assertEq(privErc20Vault.feeRecipient(), admin); assertEq(privErc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(privErc20Vault.queuedShares(), 0); assertEq(privErc20Vault.totalShares(), _securityDeposit); assertEq(privErc20Vault.totalAssets(), _securityDeposit); - assertEq(privErc20Vault.totalExitingAssets(), 0); assertEq(privErc20Vault.validatorsManagerNonce(), 0); assertEq(privErc20Vault.totalSupply(), _securityDeposit); assertEq(privErc20Vault.symbol(), 'SW-ETH-1'); assertEq(privErc20Vault.name(), 'SW ETH Vault'); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -409,8 +422,8 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { uint256 totalSharesBefore = privErc20Vault.totalShares(); uint256 totalAssetsBefore = privErc20Vault.totalAssets(); - uint256 totalExitingAssetsBefore = privErc20Vault.totalExitingAssets(); - uint256 queuedSharesBefore = privErc20Vault.queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(privErc20Vault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV4(address(privErc20Vault)).queuedShares(); uint256 senderBalanceBefore = privErc20Vault.getShares(sender); assertEq(privErc20Vault.vaultId(), keccak256('EthPrivErc20Vault')); @@ -420,6 +433,7 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthPrivErc20Vault, address(privErc20Vault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = privErc20Vault.getExitQueueData(); assertEq(privErc20Vault.vaultId(), keccak256('EthPrivErc20Vault')); assertEq(privErc20Vault.version(), 5); assertEq(privErc20Vault.admin(), admin); @@ -428,14 +442,14 @@ contract EthPrivErc20VaultTest is Test, EthHelpers { assertEq(privErc20Vault.feePercent(), 1000); assertEq(privErc20Vault.feeRecipient(), admin); assertEq(privErc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(privErc20Vault.queuedShares(), queuedSharesBefore); assertEq(privErc20Vault.totalShares(), totalSharesBefore); assertEq(privErc20Vault.totalAssets(), totalAssetsBefore); - assertEq(privErc20Vault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(privErc20Vault.validatorsManagerNonce(), 0); assertEq(privErc20Vault.getShares(sender), senderBalanceBefore); assertEq(privErc20Vault.totalSupply(), totalSharesBefore); assertEq(privErc20Vault.symbol(), 'SW-ETH-1'); assertEq(privErc20Vault.name(), 'SW ETH Vault'); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); } } diff --git a/test/EthPrivVault.t.sol b/test/EthPrivVault.t.sol index ee901bf1..557e271b 100644 --- a/test/EthPrivVault.t.sol +++ b/test/EthPrivVault.t.sol @@ -10,6 +10,11 @@ import {IKeeperRewards} from '../contracts/interfaces/IKeeperRewards.sol'; import {EthPrivVault} from '../contracts/vaults/ethereum/EthPrivVault.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthPrivVaultTest is Test, EthHelpers { ForkContracts public contracts; EthPrivVault public vault; @@ -305,6 +310,13 @@ contract EthPrivVaultTest is Test, EthHelpers { _stopSnapshotGas(); EthPrivVault privVault = EthPrivVault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = privVault.getExitQueueData(); + assertEq(privVault.vaultId(), keccak256('EthPrivVault')); assertEq(privVault.version(), 5); assertEq(privVault.admin(), admin); @@ -313,11 +325,13 @@ contract EthPrivVaultTest is Test, EthHelpers { assertEq(privVault.feePercent(), 1000); assertEq(privVault.feeRecipient(), admin); assertEq(privVault.validatorsManager(), _depositDataRegistry); - assertEq(privVault.queuedShares(), 0); assertEq(privVault.totalShares(), _securityDeposit); assertEq(privVault.totalAssets(), _securityDeposit); - assertEq(privVault.totalExitingAssets(), 0); assertEq(privVault.validatorsManagerNonce(), 0); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -348,10 +362,10 @@ contract EthPrivVaultTest is Test, EthHelpers { uint256 totalSharesBefore = privVault.totalShares(); uint256 totalAssetsBefore = privVault.totalAssets(); - uint256 totalExitingAssetsBefore = privVault.totalExitingAssets(); - uint256 queuedSharesBefore = privVault.queuedShares(); uint256 senderSharesBefore = privVault.getShares(sender); bool senderWhitelistedBefore = privVault.whitelistedAccounts(sender); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(privVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV4(address(privVault)).queuedShares(); assertEq(privVault.vaultId(), keccak256('EthPrivVault')); assertEq(privVault.version(), 4); @@ -360,6 +374,7 @@ contract EthPrivVaultTest is Test, EthHelpers { _upgradeVault(VaultType.EthPrivVault, address(privVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = privVault.getExitQueueData(); assertEq(privVault.vaultId(), keccak256('EthPrivVault')); assertEq(privVault.version(), 5); assertEq(privVault.admin(), admin); @@ -368,13 +383,13 @@ contract EthPrivVaultTest is Test, EthHelpers { assertEq(privVault.feePercent(), 1000); assertEq(privVault.feeRecipient(), admin); assertEq(privVault.validatorsManager(), _depositDataRegistry); - assertEq(privVault.queuedShares(), queuedSharesBefore); assertEq(privVault.totalShares(), totalSharesBefore); assertEq(privVault.totalAssets(), totalAssetsBefore); - assertEq(privVault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(privVault.validatorsManagerNonce(), 0); assertEq(privVault.getShares(sender), senderSharesBefore); assertEq(privVault.whitelistedAccounts(sender), senderWhitelistedBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); + assertEq(queuedShares, queuedSharesBefore); } function test_setWhitelister() public { diff --git a/test/EthValidatorsChecker.t.sol b/test/EthValidatorsChecker.t.sol index 5cac4900..ae3d4acd 100644 --- a/test/EthValidatorsChecker.t.sol +++ b/test/EthValidatorsChecker.t.sol @@ -396,7 +396,7 @@ contract EthValidatorsCheckerTest is Test, EthHelpers { // 5. Create the message hash that matches the contract's implementation bytes32 validatorsManagerTypeHash = keccak256( - 'VaultValidators(bytes32 validatorsRegistryRoot,bytes validators)' + 'VaultValidators(bytes32 nonce,bytes validators)' ); bytes32 messageHash = keccak256( abi.encode(validatorsManagerTypeHash, validRegistryRoot, keccak256(validatorData)) diff --git a/test/EthVault.t.sol b/test/EthVault.t.sol index f57ee489..0cfbfc1f 100644 --- a/test/EthVault.t.sol +++ b/test/EthVault.t.sol @@ -7,11 +7,15 @@ import {Initializable} from '@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IKeeperRewards} from '../contracts/interfaces/IKeeperRewards.sol'; import {IEthVault} from '../contracts/interfaces/IEthVault.sol'; -import {IOsTokenConfig} from '../contracts/interfaces/IOsTokenConfig.sol'; import {Errors} from '../contracts/libraries/Errors.sol'; import {EthVault} from '../contracts/vaults/ethereum/EthVault.sol'; import {EthHelpers} from './helpers/EthHelpers.sol'; +interface IVaultStateV4 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract EthVaultTest is Test, EthHelpers { ForkContracts public contracts; EthVault public vault; @@ -54,9 +58,10 @@ contract EthVaultTest is Test, EthHelpers { vm.prank(admin); vault.setValidatorsManager(validatorsManager); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = IEthVault(vault).getExitQueueData(); exitingAssets = - vault.totalExitingAssets() + - vault.convertToAssets(vault.queuedShares()) + + totalExitingAssets + + IEthVault(vault).convertToAssets(queuedShares) + vaultAddr.balance; } @@ -81,6 +86,12 @@ contract EthVaultTest is Test, EthHelpers { _stopSnapshotGas(); EthVault newVault = EthVault(payable(vaultAddr)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = newVault.getExitQueueData(); // Verify the vault was deployed correctly assertEq(newVault.vaultId(), keccak256('EthVault')); @@ -90,10 +101,12 @@ contract EthVaultTest is Test, EthHelpers { assertEq(newVault.feePercent(), 1000); assertEq(newVault.feeRecipient(), admin); assertEq(newVault.validatorsManager(), _depositDataRegistry); - assertEq(newVault.queuedShares(), 0); + assertEq(queuedShares, 0); + assertEq(totalTickets, 0); + assertEq(unclaimedAssets, 0); assertEq(newVault.totalShares(), _securityDeposit); assertEq(newVault.totalAssets(), _securityDeposit); - assertEq(newVault.totalExitingAssets(), 0); + assertEq(totalExitingAssets, 0); assertEq(newVault.validatorsManagerNonce(), 0); } @@ -122,9 +135,9 @@ contract EthVaultTest is Test, EthHelpers { // Record state before upgrade uint256 totalSharesBefore = prevVault.totalShares(); uint256 totalAssetsBefore = prevVault.totalAssets(); - uint256 totalExitingAssetsBefore = prevVault.totalExitingAssets(); - uint256 queuedSharesBefore = prevVault.queuedShares(); uint256 senderBalanceBefore = prevVault.getShares(sender); + uint256 queuedSharesBefore = IVaultStateV4(address(prevVault)).queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV4(address(prevVault)).totalExitingAssets(); // Verify current version assertEq(prevVault.vaultId(), keccak256('EthVault')); @@ -136,6 +149,7 @@ contract EthVaultTest is Test, EthHelpers { _stopSnapshotGas(); // Check that the vault was upgraded correctly + (uint128 queuedShares, , uint128 totalExitingAssets, ) = prevVault.getExitQueueData(); assertEq(prevVault.vaultId(), keccak256('EthVault')); assertEq(prevVault.version(), 5); assertEq(prevVault.admin(), admin); @@ -145,12 +159,12 @@ contract EthVaultTest is Test, EthHelpers { assertEq(prevVault.validatorsManager(), _depositDataRegistry); // State should be preserved - assertEq(prevVault.queuedShares(), queuedSharesBefore); assertEq(prevVault.totalShares(), totalSharesBefore); assertEq(prevVault.totalAssets(), totalAssetsBefore); - assertEq(prevVault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(prevVault.validatorsManagerNonce(), 0); assertEq(prevVault.getShares(sender), senderBalanceBefore); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); } function test_exitQueue_works() public { @@ -163,7 +177,12 @@ contract EthVaultTest is Test, EthHelpers { // Get initial state uint256 senderSharesBefore = vault.getShares(sender); - uint256 queuedSharesBefore = vault.queuedShares(); + ( + uint128 queuedSharesBefore, + uint128 unclaimedAssetsBefore, + uint128 totalExitingAssetsBefore, + uint256 totalTicketsBefore + ) = vault.getExitQueueData(); // Amount to exit with uint256 exitAmount = senderSharesBefore / 2; @@ -175,14 +194,56 @@ contract EthVaultTest is Test, EthHelpers { uint256 positionTicket = vault.enterExitQueue(exitAmount, receiver); _stopSnapshotGas(); + ( + uint128 queuedSharesAfter, + uint128 unclaimedAssetsAfter, + uint128 totalExitingAssetsAfter, + uint256 totalTicketsAfter + ) = vault.getExitQueueData(); + // Check state after entering exit queue assertEq(vault.getShares(sender), senderSharesBefore - exitAmount, 'Sender shares not reduced'); - assertEq(vault.queuedShares(), queuedSharesBefore + exitAmount, 'Queued shares not increased'); + assertEq(queuedSharesAfter, queuedSharesBefore + exitAmount, 'Queued shares not increased'); + assertEq(unclaimedAssetsAfter, unclaimedAssetsBefore, 'Unclaimed assets should not change'); + assertEq( + totalExitingAssetsAfter, + totalExitingAssetsBefore, + 'Total exiting assets should not change' + ); + assertEq(totalTicketsAfter, totalTicketsBefore, 'Total tickets should not change'); + + queuedSharesBefore = queuedSharesAfter; + unclaimedAssetsBefore = unclaimedAssetsAfter; + totalExitingAssetsBefore = totalExitingAssetsAfter; + totalTicketsBefore = totalTicketsAfter; // Process exit queue by updating state IKeeperRewards.HarvestParams memory harvestParams = _setEthVaultReward(address(vault), 0, 0); vault.updateState(harvestParams); + (queuedSharesAfter, unclaimedAssetsAfter, totalExitingAssetsAfter, totalTicketsAfter) = vault + .getExitQueueData(); + assertLt( + queuedSharesAfter, + queuedSharesBefore, + 'Queued shares should be reduced after processing exit queue' + ); + assertGt( + unclaimedAssetsAfter, + unclaimedAssetsBefore, + 'Unclaimed assets should increase after processing exit queue' + ); + assertEq( + totalExitingAssetsAfter, + totalExitingAssetsBefore, + 'Total exiting assets should not change after processing exit queue' + ); + assertGt( + totalTicketsAfter, + totalTicketsBefore, + 'Total tickets should increase after processing exit queue' + ); + // Check that position can be found in exit queue int256 exitQueueIndex = vault.getExitQueueIndex(positionTicket); assertGt(exitQueueIndex, -1, 'Exit queue index not found'); @@ -344,7 +405,12 @@ contract EthVaultTest is Test, EthHelpers { _stopSnapshotGas(); // Verify sender got vault shares - assertApproxEqAbs(vault.getShares(sender), depositShares, 1, 'Incorrect amount of vault shares'); + assertApproxEqAbs( + vault.getShares(sender), + depositShares, + 1, + 'Incorrect amount of vault shares' + ); // Verify osToken position uint128 osTokenShares = vault.osTokenPositions(sender); diff --git a/test/VaultAdmin.t.sol b/test/VaultAdmin.t.sol index c2b96a4f..0ad25ce9 100644 --- a/test/VaultAdmin.t.sol +++ b/test/VaultAdmin.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.22; -import {IEthVault} from "../contracts/interfaces/IEthVault.sol"; -import {IVaultAdmin} from "../contracts/interfaces/IVaultAdmin.sol"; -import {Errors} from "../contracts/libraries/Errors.sol"; -import {CommonBase} from "../lib/forge-std/src/Base.sol"; -import {StdAssertions} from "../lib/forge-std/src/StdAssertions.sol"; -import {StdChains} from "../lib/forge-std/src/StdChains.sol"; -import {StdCheats, StdCheatsSafe} from "../lib/forge-std/src/StdCheats.sol"; -import {StdUtils} from "../lib/forge-std/src/StdUtils.sol"; -import {Test} from "../lib/forge-std/src/Test.sol"; -import {EthHelpers} from "./helpers/EthHelpers.sol"; +import {IEthVault} from '../contracts/interfaces/IEthVault.sol'; +import {IVaultAdmin} from '../contracts/interfaces/IVaultAdmin.sol'; +import {Errors} from '../contracts/libraries/Errors.sol'; +import {CommonBase} from '../lib/forge-std/src/Base.sol'; +import {StdAssertions} from '../lib/forge-std/src/StdAssertions.sol'; +import {StdChains} from '../lib/forge-std/src/StdChains.sol'; +import {StdCheats, StdCheatsSafe} from '../lib/forge-std/src/StdCheats.sol'; +import {StdUtils} from '../lib/forge-std/src/StdUtils.sol'; +import {Test} from '../lib/forge-std/src/Test.sol'; +import {EthHelpers} from './helpers/EthHelpers.sol'; contract VaultAdminTest is Test, EthHelpers { ForkContracts public contracts; diff --git a/test/VaultEnterExit.t.sol b/test/VaultEnterExit.t.sol index c6fe789d..f71e3264 100644 --- a/test/VaultEnterExit.t.sol +++ b/test/VaultEnterExit.t.sol @@ -47,9 +47,10 @@ contract VaultEnterExitTest is Test, EthHelpers { address vaultAddr = _getOrCreateVault(VaultType.EthVault, admin, initParams, false); vault = EthVault(payable(vaultAddr)); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); vm.deal( - vaultAddr, - vault.convertToAssets(vault.queuedShares()) + vault.totalExitingAssets() + vaultAddr.balance + address(vault), + address(vault).balance + vault.convertToAssets(queuedShares) + totalExitingAssets ); } @@ -263,7 +264,7 @@ contract VaultEnterExitTest is Test, EthHelpers { // 3. Enter exit queue uint256 shares = vault.getShares(sender); - uint256 queuedSharesBefore = vault.queuedShares(); + (uint128 queuedSharesBefore, , , ) = vault.getExitQueueData(); vm.prank(sender); uint256 timestamp = vm.getBlockTimestamp(); @@ -272,8 +273,9 @@ contract VaultEnterExitTest is Test, EthHelpers { _stopSnapshotGas(); // 4. Verify the position ticket was created and shares moved to the queue + (uint128 queuedShares, , , ) = vault.getExitQueueData(); assertEq( - vault.queuedShares(), + queuedShares, queuedSharesBefore + shares, 'Queued shares should equal the shares sent to exit queue' ); @@ -480,7 +482,8 @@ contract VaultEnterExitTest is Test, EthHelpers { // 3. Both users enter exit queue uint256 shares1 = vault.getShares(sender); uint256 shares2 = vault.getShares(sender2); - uint256 queuedSharesBefore = vault.queuedShares(); + + (uint128 queuedSharesBefore, , , ) = vault.getExitQueueData(); vm.prank(sender); uint256 timestamp1 = vm.getBlockTimestamp(); @@ -495,8 +498,9 @@ contract VaultEnterExitTest is Test, EthHelpers { _stopSnapshotGas(); // 4. Verify the queued shares + (uint128 queuedShares, , , ) = vault.getExitQueueData(); assertEq( - vault.queuedShares(), + queuedShares, queuedSharesBefore + shares1 + shares2, 'Queued shares should equal the sum of all shares in exit queue' ); @@ -568,7 +572,7 @@ contract VaultEnterExitTest is Test, EthHelpers { // 2. Collateralize the vault _collateralizeEthVault(address(vault)); - uint256 queuedSharesBefore = vault.queuedShares(); + (uint128 queuedSharesBefore, , , ) = vault.getExitQueueData(); // 3. Enter exit queue with half of the shares uint256 totalShares = vault.getShares(sender); @@ -582,8 +586,9 @@ contract VaultEnterExitTest is Test, EthHelpers { _stopSnapshotGas(); // 4. Verify the position ticket and remaining shares + (uint128 queuedShares, , , ) = vault.getExitQueueData(); assertEq( - vault.queuedShares(), + queuedShares, queuedSharesBefore + halfShares, 'Queued shares should equal the half shares sent to exit queue' ); diff --git a/test/VaultEthStaking.t.sol b/test/VaultEthStaking.t.sol index a662e2cc..cc274dc0 100644 --- a/test/VaultEthStaking.t.sol +++ b/test/VaultEthStaking.t.sol @@ -421,8 +421,9 @@ contract VaultEthStakingTest is Test, EthHelpers { _collateralizeEthVault(address(vault)); // Deposit ETH to the vault - uint256 senderDeposit = vault.convertToAssets(vault.queuedShares()) + - vault.totalExitingAssets() + + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); + uint256 senderDeposit = vault.convertToAssets(queuedShares) + + totalExitingAssets + depositAmount; _depositToVault(address(vault), senderDeposit, sender, sender); diff --git a/test/VaultState.t.sol b/test/VaultState.t.sol index 562c0e2c..b2618aea 100644 --- a/test/VaultState.t.sol +++ b/test/VaultState.t.sol @@ -47,7 +47,12 @@ contract VaultStateTest is Test, EthHelpers { address vaultAddr = _getOrCreateVault(VaultType.EthVault, admin, initParams, false); vault = EthVault(payable(vaultAddr)); - vm.deal(vaultAddr, vault.totalExitingAssets() + vault.convertToAssets(vault.queuedShares())); + (uint128 queuedShares, uint128 unclaimedAssets, uint128 totalExitingAssets, ) = vault + .getExitQueueData(); + vm.deal( + address(vault), + unclaimedAssets + vault.convertToAssets(queuedShares) + totalExitingAssets + ); // Initial deposit to the vault _depositToVault(address(vault), initialDeposit, owner, owner); @@ -274,7 +279,7 @@ contract VaultStateTest is Test, EthHelpers { // Record owner's ETH balance before uint256 ownerBalanceBefore = owner.balance; - uint256 queuedSharesBefore = vault.queuedShares(); + (uint128 queuedSharesBefore, , , ) = vault.getExitQueueData(); vm.expectEmit(true, true, true, false); emit IVaultEnterExit.ExitQueueEntered(owner, owner, 0, exitShares); @@ -293,8 +298,9 @@ contract VaultStateTest is Test, EthHelpers { ); // Verify queued shares increased + (uint128 queuedShares, , , ) = vault.getExitQueueData(); assertEq( - vault.queuedShares(), + queuedShares, queuedSharesBefore + exitShares, 'Queued shares should match exit amount' ); @@ -476,10 +482,11 @@ contract VaultStateTest is Test, EthHelpers { // Step 2: User enters exit queue with all shares uint256 user1Shares = vault.getShares(user1); - uint256 vaultBalance = vault.totalExitingAssets() + - vault.convertToAssets(vault.queuedShares()) + - address(vault).balance - - vault.withdrawableAssets(); + (uint128 queuedShares, uint128 unclaimedAssets, uint128 totalExitingAssets, ) = vault + .getExitQueueData(); + uint256 vaultBalance = totalExitingAssets + + vault.convertToAssets(queuedShares) + + unclaimedAssets; vm.expectEmit(true, true, true, false); emit IVaultEnterExit.ExitQueueEntered(user1, user1, 0, user1Shares); diff --git a/test/VaultToken.t.sol b/test/VaultToken.t.sol index 32407ae2..4b38adf6 100644 --- a/test/VaultToken.t.sol +++ b/test/VaultToken.t.sol @@ -346,7 +346,8 @@ contract VaultTokenTest is Test, EthHelpers { assertEq(ownerFinalBalance, ownerInitialBalance - exitShares, 'Owner balance should decrease'); // Verify queued shares - assertEq(vault.queuedShares(), exitShares, 'Queued shares should match exit amount'); + (uint128 queuedShares, , , ) = vault.getExitQueueData(); + assertEq(queuedShares, exitShares, 'Queued shares should match exit amount'); } // Test _updateExitQueue burns shares and emits Transfer diff --git a/test/VaultValidators.t.sol b/test/VaultValidators.t.sol index 741decca..1ccc9b9e 100644 --- a/test/VaultValidators.t.sol +++ b/test/VaultValidators.t.sol @@ -403,10 +403,7 @@ contract VaultValidatorsTest is Test, EthHelpers { // Extract the public key from validators data bytes memory publicKey = _extractBytes(approvalParams.validators, 0, 48); - vm.assertFalse( - vault.v2Validators(keccak256(publicKey)), - 'Validator should not be tracked' - ); + vm.assertFalse(vault.v2Validators(keccak256(publicKey)), 'Validator should not be tracked'); // For V1 validators, the event is emitted without deposit amount vm.expectEmit(true, true, true, false); @@ -421,10 +418,7 @@ contract VaultValidatorsTest is Test, EthHelpers { // Cleanup _stopOracleImpersonate(address(contracts.keeper)); - vm.assertFalse( - vault.v2Validators(keccak256(publicKey)), - 'Validator should not be tracked' - ); + vm.assertFalse(vault.v2Validators(keccak256(publicKey)), 'Validator should not be tracked'); } // Test V2 validator registration (with 0x02 prefix) @@ -442,10 +436,7 @@ contract VaultValidatorsTest is Test, EthHelpers { // Extract the public key from validators data bytes memory publicKey = _extractBytes(approvalParams.validators, 0, 48); - vm.assertFalse( - vault.v2Validators(keccak256(publicKey)), - 'Validator should not be tracked' - ); + vm.assertFalse(vault.v2Validators(keccak256(publicKey)), 'Validator should not be tracked'); // For V2 validators, the event includes deposit amount vm.expectEmit(true, true, true, true); @@ -537,7 +528,7 @@ contract VaultValidatorsTest is Test, EthHelpers { bytes32 structHash = keccak256( abi.encode( - keccak256('VaultValidators(bytes32 validatorsRegistryRoot,bytes validators)'), + keccak256('VaultValidators(bytes32 nonce,bytes validators)'), validatorsRegistryRoot, keccak256(validators) ) diff --git a/test/gnosis/GnoBlocklistErc20Vault.t.sol b/test/gnosis/GnoBlocklistErc20Vault.t.sol index 6621fc6a..8c9b6d11 100644 --- a/test/gnosis/GnoBlocklistErc20Vault.t.sol +++ b/test/gnosis/GnoBlocklistErc20Vault.t.sol @@ -8,6 +8,11 @@ import {Errors} from '../../contracts/libraries/Errors.sol'; import {IGnoErc20Vault} from '../../contracts/interfaces/IGnoErc20Vault.sol'; import {GnoBlocklistErc20Vault} from '../../contracts/vaults/gnosis/GnoBlocklistErc20Vault.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoBlocklistErc20Vault public vault; @@ -241,6 +246,12 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { _stopSnapshotGas(); GnoBlocklistErc20Vault blocklistVault = GnoBlocklistErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistErc20Vault')); assertEq(blocklistVault.version(), 3); assertEq(blocklistVault.admin(), admin); @@ -249,10 +260,12 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), 0); assertEq(blocklistVault.totalShares(), _securityDeposit); assertEq(blocklistVault.totalAssets(), _securityDeposit); - assertEq(blocklistVault.totalExitingAssets(), 0); + assertEq(queuedShares, 0); + assertEq(totalExitingAssets, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalTickets, 0); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.totalSupply(), _securityDeposit); assertEq(blocklistVault.symbol(), 'SW-GNO-1'); @@ -286,9 +299,9 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { uint256 totalSharesBefore = blocklistVault.totalShares(); uint256 totalAssetsBefore = blocklistVault.totalAssets(); - uint256 totalExitingAssetsBefore = blocklistVault.totalExitingAssets(); - uint256 queuedSharesBefore = blocklistVault.queuedShares(); uint256 senderBalanceBefore = blocklistVault.getShares(sender); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(blocklistVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(blocklistVault)).queuedShares(); assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistErc20Vault')); assertEq(blocklistVault.version(), 2); @@ -301,6 +314,7 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoBlocklistErc20Vault, address(blocklistVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistErc20Vault')); assertEq(blocklistVault.version(), 3); assertEq(blocklistVault.admin(), admin); @@ -309,10 +323,10 @@ contract GnoBlocklistErc20VaultTest is Test, GnoHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), queuedSharesBefore); + assertEq(queuedShares, queuedSharesBefore); assertEq(blocklistVault.totalShares(), totalSharesBefore); assertEq(blocklistVault.totalAssets(), totalAssetsBefore); - assertEq(blocklistVault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.getShares(sender), senderBalanceBefore); assertEq( diff --git a/test/gnosis/GnoBlocklistVault.t.sol b/test/gnosis/GnoBlocklistVault.t.sol index 382fd1f0..ad3f05fc 100644 --- a/test/gnosis/GnoBlocklistVault.t.sol +++ b/test/gnosis/GnoBlocklistVault.t.sol @@ -8,6 +8,11 @@ import {Errors} from '../../contracts/libraries/Errors.sol'; import {IGnoVault} from '../../contracts/interfaces/IGnoVault.sol'; import {GnoBlocklistVault} from '../../contracts/vaults/gnosis/GnoBlocklistVault.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoBlocklistVaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoBlocklistVault public vault; @@ -168,6 +173,13 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { _stopSnapshotGas(); GnoBlocklistVault blocklistVault = GnoBlocklistVault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = blocklistVault.getExitQueueData(); + assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistVault')); assertEq(blocklistVault.version(), 3); assertEq(blocklistVault.admin(), admin); @@ -176,11 +188,13 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), 0); assertEq(blocklistVault.totalShares(), _securityDeposit); assertEq(blocklistVault.totalAssets(), _securityDeposit); - assertEq(blocklistVault.totalExitingAssets(), 0); assertEq(blocklistVault.validatorsManagerNonce(), 0); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -203,8 +217,8 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { uint256 totalSharesBefore = blocklistVault.totalShares(); uint256 totalAssetsBefore = blocklistVault.totalAssets(); - uint256 totalExitingAssetsBefore = blocklistVault.totalExitingAssets(); - uint256 queuedSharesBefore = blocklistVault.queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(blocklistVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(blocklistVault)).queuedShares(); uint256 senderBalanceBefore = blocklistVault.getShares(sender); assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistVault')); @@ -218,6 +232,7 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoBlocklistVault, address(blocklistVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = blocklistVault.getExitQueueData(); assertEq(blocklistVault.vaultId(), keccak256('GnoBlocklistVault')); assertEq(blocklistVault.version(), 3); assertEq(blocklistVault.admin(), admin); @@ -226,12 +241,12 @@ contract GnoBlocklistVaultTest is Test, GnoHelpers { assertEq(blocklistVault.feePercent(), 1000); assertEq(blocklistVault.feeRecipient(), admin); assertEq(blocklistVault.validatorsManager(), _depositDataRegistry); - assertEq(blocklistVault.queuedShares(), queuedSharesBefore); assertEq(blocklistVault.totalShares(), totalSharesBefore); assertEq(blocklistVault.totalAssets(), totalAssetsBefore); - assertEq(blocklistVault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(blocklistVault.validatorsManagerNonce(), 0); assertEq(blocklistVault.getShares(sender), senderBalanceBefore); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq( contracts.gnoToken.allowance(address(blocklistVault), address(contracts.validatorsRegistry)), type(uint256).max diff --git a/test/gnosis/GnoErc20Vault.t.sol b/test/gnosis/GnoErc20Vault.t.sol index ffb19f84..a87b6bb2 100644 --- a/test/gnosis/GnoErc20Vault.t.sol +++ b/test/gnosis/GnoErc20Vault.t.sol @@ -11,6 +11,11 @@ import {Errors} from '../../contracts/libraries/Errors.sol'; import {GnoErc20Vault} from '../../contracts/vaults/gnosis/GnoErc20Vault.sol'; import {GnoHelpers} from '../helpers/GnoHelpers.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoErc20VaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoErc20Vault public vault; @@ -72,6 +77,13 @@ contract GnoErc20VaultTest is Test, GnoHelpers { _stopSnapshotGas(); GnoErc20Vault erc20Vault = GnoErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = erc20Vault.getExitQueueData(); + assertEq(erc20Vault.vaultId(), keccak256('GnoErc20Vault')); assertEq(erc20Vault.version(), 3); assertEq(erc20Vault.admin(), admin); @@ -79,14 +91,16 @@ contract GnoErc20VaultTest is Test, GnoHelpers { assertEq(erc20Vault.feePercent(), 1000); assertEq(erc20Vault.feeRecipient(), admin); assertEq(erc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(erc20Vault.queuedShares(), 0); assertEq(erc20Vault.totalShares(), _securityDeposit); assertEq(erc20Vault.totalAssets(), _securityDeposit); - assertEq(erc20Vault.totalExitingAssets(), 0); assertEq(erc20Vault.validatorsManagerNonce(), 0); assertEq(erc20Vault.totalSupply(), _securityDeposit); assertEq(erc20Vault.symbol(), 'SW-GNO-1'); assertEq(erc20Vault.name(), 'SW GNO Vault'); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -111,9 +125,9 @@ contract GnoErc20VaultTest is Test, GnoHelpers { uint256 totalSharesBefore = erc20Vault.totalShares(); uint256 totalAssetsBefore = erc20Vault.totalAssets(); - uint256 totalExitingAssetsBefore = erc20Vault.totalExitingAssets(); - uint256 queuedSharesBefore = erc20Vault.queuedShares(); uint256 senderBalanceBefore = erc20Vault.getShares(sender); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(erc20Vault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(erc20Vault)).queuedShares(); assertEq(erc20Vault.vaultId(), keccak256('GnoErc20Vault')); assertEq(erc20Vault.version(), 2); @@ -126,6 +140,7 @@ contract GnoErc20VaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoErc20Vault, address(erc20Vault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = erc20Vault.getExitQueueData(); assertEq(erc20Vault.vaultId(), keccak256('GnoErc20Vault')); assertEq(erc20Vault.version(), 3); assertEq(erc20Vault.admin(), admin); @@ -133,10 +148,8 @@ contract GnoErc20VaultTest is Test, GnoHelpers { assertEq(erc20Vault.feePercent(), 1000); assertEq(erc20Vault.feeRecipient(), admin); assertEq(erc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(erc20Vault.queuedShares(), queuedSharesBefore); assertEq(erc20Vault.totalShares(), totalSharesBefore); assertEq(erc20Vault.totalAssets(), totalAssetsBefore); - assertEq(erc20Vault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(erc20Vault.validatorsManagerNonce(), 0); assertEq(erc20Vault.getShares(sender), senderBalanceBefore); assertEq( @@ -146,6 +159,8 @@ contract GnoErc20VaultTest is Test, GnoHelpers { assertEq(erc20Vault.totalSupply(), totalSharesBefore); assertEq(erc20Vault.symbol(), 'SW-GNO-1'); assertEq(erc20Vault.name(), 'SW GNO Vault'); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); } function test_deposit_emitsTransfer() public { diff --git a/test/gnosis/GnoGenesisVault.t.sol b/test/gnosis/GnoGenesisVault.t.sol index 016bebab..ef6c6325 100644 --- a/test/gnosis/GnoGenesisVault.t.sol +++ b/test/gnosis/GnoGenesisVault.t.sol @@ -10,6 +10,11 @@ import {GnoGenesisVault} from '../../contracts/vaults/gnosis/GnoGenesisVault.sol import {Errors} from '../../contracts/libraries/Errors.sol'; import {GnoHelpers} from '../helpers/GnoHelpers.sol'; +interface IVaultStateV3 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoGenesisVaultTest is Test, GnoHelpers { ForkContracts public contracts; address public admin; @@ -66,10 +71,10 @@ contract GnoGenesisVaultTest is Test, GnoHelpers { existingVault.enterExitQueue(10 ether, user); // Record initial state + uint256 totalExitingAssetsBefore = IVaultStateV3(address(existingVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV3(address(existingVault)).queuedShares(); uint256 initialTotalAssets = existingVault.totalAssets(); uint256 initialTotalShares = existingVault.totalShares(); - uint256 totalExitingAssetsBefore = existingVault.totalExitingAssets(); - uint256 queuedSharesBefore = existingVault.queuedShares(); uint256 senderBalanceBefore = existingVault.getShares(user); uint256 initialCapacity = existingVault.capacity(); uint256 initialFeePercent = existingVault.feePercent(); @@ -84,6 +89,7 @@ contract GnoGenesisVaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoGenesisVault, address(existingVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = existingVault.getExitQueueData(); assertEq(existingVault.vaultId(), keccak256('GnoGenesisVault')); assertEq(existingVault.version(), 4); assertEq(existingVault.admin(), adminBefore); @@ -91,10 +97,10 @@ contract GnoGenesisVaultTest is Test, GnoHelpers { assertEq(existingVault.feePercent(), initialFeePercent); assertEq(existingVault.feeRecipient(), feeRecipient); assertEq(existingVault.validatorsManager(), validatorsManager); - assertEq(existingVault.queuedShares(), queuedSharesBefore); + assertEq(queuedShares, queuedSharesBefore); assertEq(existingVault.totalShares(), initialTotalShares); assertEq(existingVault.totalAssets(), initialTotalAssets); - assertEq(existingVault.totalExitingAssets(), totalExitingAssetsBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); assertEq(existingVault.validatorsManagerNonce(), 0); assertEq(existingVault.getShares(user), senderBalanceBefore); assertEq( diff --git a/test/gnosis/GnoOsTokenVaultEscrow.t.sol b/test/gnosis/GnoOsTokenVaultEscrow.t.sol index 94caaccb..40068e90 100644 --- a/test/gnosis/GnoOsTokenVaultEscrow.t.sol +++ b/test/gnosis/GnoOsTokenVaultEscrow.t.sol @@ -93,10 +93,8 @@ contract GnoOsTokenVaultEscrowTest is Test, GnoHelpers { assertEq(escrowOsTokenShares, osTokenShares, 'Incorrect osToken shares in escrow'); IKeeperRewards.HarvestParams memory harvestParams = _setGnoVaultReward(address(vault), 0, 0); - _mintGnoToken( - address(vault), - vault.totalExitingAssets() + vault.convertToAssets(vault.queuedShares()) - ); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); + _mintGnoToken(address(vault), totalExitingAssets + vault.convertToAssets(queuedShares)); vault.updateState(harvestParams); vm.warp(timestamp + _exitingAssetsClaimDelay + 1); diff --git a/test/gnosis/GnoOwnMevEscrow.t.sol b/test/gnosis/GnoOwnMevEscrow.t.sol index 0637e5e0..2b82fa27 100644 --- a/test/gnosis/GnoOwnMevEscrow.t.sol +++ b/test/gnosis/GnoOwnMevEscrow.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; import {Test} from 'forge-std/Test.sol'; -import {GnoHelpers} from "../helpers/GnoHelpers.sol"; +import {GnoHelpers} from '../helpers/GnoHelpers.sol'; import {Errors} from '../../contracts/libraries/Errors.sol'; import {IOwnMevEscrow} from '../../contracts/interfaces/IOwnMevEscrow.sol'; diff --git a/test/gnosis/GnoPrivErc20Vault.t.sol b/test/gnosis/GnoPrivErc20Vault.t.sol index 36cf17b7..82aef29e 100644 --- a/test/gnosis/GnoPrivErc20Vault.t.sol +++ b/test/gnosis/GnoPrivErc20Vault.t.sol @@ -8,6 +8,11 @@ import {Errors} from '../../contracts/libraries/Errors.sol'; import {IGnoErc20Vault} from '../../contracts/interfaces/IGnoErc20Vault.sol'; import {GnoPrivErc20Vault} from '../../contracts/vaults/gnosis/GnoPrivErc20Vault.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoPrivErc20VaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoPrivErc20Vault public vault; @@ -263,6 +268,12 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { _stopSnapshotGas(); GnoPrivErc20Vault privErc20Vault = GnoPrivErc20Vault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = privErc20Vault.getExitQueueData(); assertEq(privErc20Vault.vaultId(), keccak256('GnoPrivErc20Vault')); assertEq(privErc20Vault.version(), 3); assertEq(privErc20Vault.admin(), admin); @@ -271,14 +282,16 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { assertEq(privErc20Vault.feePercent(), 1000); assertEq(privErc20Vault.feeRecipient(), admin); assertEq(privErc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(privErc20Vault.queuedShares(), 0); assertEq(privErc20Vault.totalShares(), _securityDeposit); assertEq(privErc20Vault.totalAssets(), _securityDeposit); - assertEq(privErc20Vault.totalExitingAssets(), 0); assertEq(privErc20Vault.validatorsManagerNonce(), 0); assertEq(privErc20Vault.totalSupply(), _securityDeposit); assertEq(privErc20Vault.symbol(), 'SW-GNO-1'); assertEq(privErc20Vault.name(), 'SW GNO Vault'); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -307,9 +320,9 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { uint256 totalSharesBefore = privErc20Vault.totalShares(); uint256 totalAssetsBefore = privErc20Vault.totalAssets(); - uint256 totalExitingAssetsBefore = privErc20Vault.totalExitingAssets(); - uint256 queuedSharesBefore = privErc20Vault.queuedShares(); uint256 senderBalanceBefore = privErc20Vault.getShares(sender); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(privErc20Vault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(privErc20Vault)).queuedShares(); assertEq(privErc20Vault.vaultId(), keccak256('GnoPrivErc20Vault')); assertEq(privErc20Vault.version(), 2); @@ -322,6 +335,7 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoPrivErc20Vault, address(privErc20Vault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = privErc20Vault.getExitQueueData(); assertEq(privErc20Vault.vaultId(), keccak256('GnoPrivErc20Vault')); assertEq(privErc20Vault.version(), 3); assertEq(privErc20Vault.admin(), admin); @@ -330,10 +344,8 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { assertEq(privErc20Vault.feePercent(), 1000); assertEq(privErc20Vault.feeRecipient(), admin); assertEq(privErc20Vault.validatorsManager(), _depositDataRegistry); - assertEq(privErc20Vault.queuedShares(), queuedSharesBefore); assertEq(privErc20Vault.totalShares(), totalSharesBefore); assertEq(privErc20Vault.totalAssets(), totalAssetsBefore); - assertEq(privErc20Vault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(privErc20Vault.validatorsManagerNonce(), 0); assertEq(privErc20Vault.getShares(sender), senderBalanceBefore); assertEq( @@ -343,6 +355,8 @@ contract GnoPrivErc20VaultTest is Test, GnoHelpers { assertEq(privErc20Vault.totalSupply(), totalSharesBefore); assertEq(privErc20Vault.symbol(), 'SW-GNO-1'); assertEq(privErc20Vault.name(), 'SW GNO Vault'); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); } // Helper function to deposit GNO to the vault diff --git a/test/gnosis/GnoPrivVault.t.sol b/test/gnosis/GnoPrivVault.t.sol index 4284f28a..8f3a8f11 100644 --- a/test/gnosis/GnoPrivVault.t.sol +++ b/test/gnosis/GnoPrivVault.t.sol @@ -8,6 +8,11 @@ import {Errors} from '../../contracts/libraries/Errors.sol'; import {IGnoVault} from '../../contracts/interfaces/IGnoVault.sol'; import {GnoPrivVault} from '../../contracts/vaults/gnosis/GnoPrivVault.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoPrivVaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoPrivVault public vault; @@ -220,6 +225,12 @@ contract GnoPrivVaultTest is Test, GnoHelpers { _stopSnapshotGas(); GnoPrivVault privVault = GnoPrivVault(payable(_vault)); + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = privVault.getExitQueueData(); assertEq(privVault.vaultId(), keccak256('GnoPrivVault')); assertEq(privVault.version(), 3); assertEq(privVault.admin(), admin); @@ -228,11 +239,13 @@ contract GnoPrivVaultTest is Test, GnoHelpers { assertEq(privVault.feePercent(), 1000); assertEq(privVault.feeRecipient(), admin); assertEq(privVault.validatorsManager(), _depositDataRegistry); - assertEq(privVault.queuedShares(), 0); assertEq(privVault.totalShares(), _securityDeposit); assertEq(privVault.totalAssets(), _securityDeposit); - assertEq(privVault.totalExitingAssets(), 0); assertEq(privVault.validatorsManagerNonce(), 0); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -263,10 +276,10 @@ contract GnoPrivVaultTest is Test, GnoHelpers { uint256 totalSharesBefore = privVault.totalShares(); uint256 totalAssetsBefore = privVault.totalAssets(); - uint256 totalExitingAssetsBefore = privVault.totalExitingAssets(); - uint256 queuedSharesBefore = privVault.queuedShares(); uint256 senderBalanceBefore = privVault.getShares(sender); bool senderWhitelistedBefore = privVault.whitelistedAccounts(sender); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(privVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(privVault)).queuedShares(); assertEq(privVault.vaultId(), keccak256('GnoPrivVault')); assertEq(privVault.version(), 2); @@ -279,6 +292,7 @@ contract GnoPrivVaultTest is Test, GnoHelpers { _upgradeVault(VaultType.GnoPrivVault, address(privVault)); _stopSnapshotGas(); + (uint128 queuedShares, , uint128 totalExitingAssets, ) = privVault.getExitQueueData(); assertEq(privVault.vaultId(), keccak256('GnoPrivVault')); assertEq(privVault.version(), 3); assertEq(privVault.admin(), admin); @@ -287,10 +301,8 @@ contract GnoPrivVaultTest is Test, GnoHelpers { assertEq(privVault.feePercent(), 1000); assertEq(privVault.feeRecipient(), admin); assertEq(privVault.validatorsManager(), _depositDataRegistry); - assertEq(privVault.queuedShares(), queuedSharesBefore); assertEq(privVault.totalShares(), totalSharesBefore); assertEq(privVault.totalAssets(), totalAssetsBefore); - assertEq(privVault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(privVault.validatorsManagerNonce(), 0); assertEq(privVault.getShares(sender), senderBalanceBefore); assertEq(privVault.whitelistedAccounts(sender), senderWhitelistedBefore); @@ -298,6 +310,8 @@ contract GnoPrivVaultTest is Test, GnoHelpers { contracts.gnoToken.allowance(address(privVault), address(contracts.validatorsRegistry)), type(uint256).max ); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); } function test_setWhitelister() public { diff --git a/test/gnosis/GnoRewardSplitter.t.sol b/test/gnosis/GnoRewardSplitter.t.sol index b3e969aa..493b5d0c 100644 --- a/test/gnosis/GnoRewardSplitter.t.sol +++ b/test/gnosis/GnoRewardSplitter.t.sol @@ -173,7 +173,11 @@ contract GnoRewardSplitterTest is Test, GnoHelpers { _stopSnapshotGas(); // Verify shareholder2 received vault tokens - assertGt(IVaultState(vault).getShares(shareholder2), 0, 'Shareholder2 should receive vault tokens directly'); + assertGt( + IVaultState(vault).getShares(shareholder2), + 0, + 'Shareholder2 should receive vault tokens directly' + ); } function test_maxWithdrawal() public { @@ -431,8 +435,16 @@ contract GnoRewardSplitterTest is Test, GnoHelpers { uint256 newSharesShareholder1 = rewardSplitter.sharesOf(shareholder1); uint256 newTotalShares = rewardSplitter.totalShares(); - assertEq(newSharesShareholder1, initialSharesShareholder1 + increaseAmount, "Shares should be increased correctly"); - assertEq(newTotalShares, initialTotalShares + increaseAmount, "Total shares should be increased correctly"); + assertEq( + newSharesShareholder1, + initialSharesShareholder1 + increaseAmount, + 'Shares should be increased correctly' + ); + assertEq( + newTotalShares, + initialTotalShares + increaseAmount, + 'Total shares should be increased correctly' + ); // Test decrease shares vm.prank(admin); @@ -444,8 +456,16 @@ contract GnoRewardSplitterTest is Test, GnoHelpers { uint256 finalSharesShareholder1 = rewardSplitter.sharesOf(shareholder1); uint256 finalTotalShares = rewardSplitter.totalShares(); - assertEq(finalSharesShareholder1, initialSharesShareholder1, "Shares should be decreased back to original amount"); - assertEq(finalTotalShares, initialTotalShares, "Total shares should be decreased back to original amount"); + assertEq( + finalSharesShareholder1, + initialSharesShareholder1, + 'Shares should be decreased back to original amount' + ); + assertEq( + finalTotalShares, + initialTotalShares, + 'Total shares should be decreased back to original amount' + ); } function test_syncRewards() public { diff --git a/test/gnosis/GnoSharedMevEscrow.t.sol b/test/gnosis/GnoSharedMevEscrow.t.sol index 29dc23d7..ad2aeee3 100644 --- a/test/gnosis/GnoSharedMevEscrow.t.sol +++ b/test/gnosis/GnoSharedMevEscrow.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; import {Test} from 'forge-std/Test.sol'; -import {GnoHelpers} from "../helpers/GnoHelpers.sol"; +import {GnoHelpers} from '../helpers/GnoHelpers.sol'; import {Errors} from '../../contracts/libraries/Errors.sol'; import {ISharedMevEscrow} from '../../contracts/interfaces/ISharedMevEscrow.sol'; diff --git a/test/gnosis/GnoVault.t.sol b/test/gnosis/GnoVault.t.sol index 1c8efca4..ae2ed1d4 100644 --- a/test/gnosis/GnoVault.t.sol +++ b/test/gnosis/GnoVault.t.sol @@ -2,16 +2,19 @@ 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 {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; import {IKeeperRewards} from '../../contracts/interfaces/IKeeperRewards.sol'; import {IGnoVault} from '../../contracts/interfaces/IGnoVault.sol'; -import {IOsTokenConfig} from '../../contracts/interfaces/IOsTokenConfig.sol'; import {Errors} from '../../contracts/libraries/Errors.sol'; import {GnoVault} from '../../contracts/vaults/gnosis/GnoVault.sol'; import {GnoHelpers} from '../helpers/GnoHelpers.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoVaultTest is Test, GnoHelpers { ForkContracts public contracts; GnoVault public vault; @@ -77,6 +80,12 @@ contract GnoVaultTest is Test, GnoHelpers { GnoVault newVault = GnoVault(payable(vaultAddr)); // Verify the vault was deployed correctly + ( + uint128 queuedShares, + uint128 unclaimedAssets, + uint128 totalExitingAssets, + uint256 totalTickets + ) = newVault.getExitQueueData(); assertEq(newVault.vaultId(), keccak256('GnoVault')); assertEq(newVault.version(), 3); assertEq(newVault.admin(), admin); @@ -84,11 +93,13 @@ contract GnoVaultTest is Test, GnoHelpers { assertEq(newVault.feePercent(), 1000); assertEq(newVault.feeRecipient(), admin); assertEq(newVault.validatorsManager(), _depositDataRegistry); - assertEq(newVault.queuedShares(), 0); assertEq(newVault.totalShares(), _securityDeposit); assertEq(newVault.totalAssets(), _securityDeposit); - assertEq(newVault.totalExitingAssets(), 0); assertEq(newVault.validatorsManagerNonce(), 0); + assertEq(queuedShares, 0); + assertEq(unclaimedAssets, 0); + assertEq(totalExitingAssets, 0); + assertEq(totalTickets, 0); } function test_upgradesCorrectly() public { @@ -116,8 +127,8 @@ contract GnoVaultTest is Test, GnoHelpers { // Record state before upgrade uint256 totalSharesBefore = prevVault.totalShares(); uint256 totalAssetsBefore = prevVault.totalAssets(); - uint256 totalExitingAssetsBefore = prevVault.totalExitingAssets(); - uint256 queuedSharesBefore = prevVault.queuedShares(); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(prevVault)).totalExitingAssets(); + uint256 queuedSharesBefore = IVaultStateV2(address(prevVault)).queuedShares(); uint256 senderBalanceBefore = prevVault.getShares(sender); // Verify current version @@ -136,6 +147,7 @@ contract GnoVaultTest is Test, GnoHelpers { _stopSnapshotGas(); // Check that the vault was upgraded correctly + (uint128 queuedShares, , uint128 totalExitingAssets, ) = prevVault.getExitQueueData(); assertEq(prevVault.vaultId(), keccak256('GnoVault')); assertEq(prevVault.version(), 3); assertEq(prevVault.admin(), admin); @@ -145,12 +157,12 @@ contract GnoVaultTest is Test, GnoHelpers { assertEq(prevVault.validatorsManager(), _depositDataRegistry); // State should be preserved - assertEq(prevVault.queuedShares(), queuedSharesBefore); assertEq(prevVault.totalShares(), totalSharesBefore); assertEq(prevVault.totalAssets(), totalAssetsBefore); - assertEq(prevVault.totalExitingAssets(), totalExitingAssetsBefore); assertEq(prevVault.validatorsManagerNonce(), 0); assertEq(prevVault.getShares(sender), senderBalanceBefore); + assertEq(queuedShares, queuedSharesBefore); + assertEq(totalExitingAssets, totalExitingAssetsBefore); // Allowance should be set after upgrade assertEq( @@ -169,7 +181,12 @@ contract GnoVaultTest is Test, GnoHelpers { // Get initial state uint256 senderSharesBefore = vault.getShares(sender); - uint256 queuedSharesBefore = vault.queuedShares(); + ( + uint128 queuedSharesBefore, + uint128 unclaimedAssetsBefore, + uint128 totalExitingAssetsBefore, + uint256 totalTicketsBefore + ) = vault.getExitQueueData(); // Amount to exit with uint256 exitAmount = senderSharesBefore / 2; @@ -181,19 +198,61 @@ contract GnoVaultTest is Test, GnoHelpers { uint256 positionTicket = vault.enterExitQueue(exitAmount, receiver); _stopSnapshotGas(); + ( + uint128 queuedSharesAfter, + uint128 unclaimedAssetsAfter, + uint128 totalExitingAssetsAfter, + uint256 totalTicketsAfter + ) = vault.getExitQueueData(); + // Check state after entering exit queue assertEq(vault.getShares(sender), senderSharesBefore - exitAmount, 'Sender shares not reduced'); - assertEq(vault.queuedShares(), queuedSharesBefore + exitAmount, 'Queued shares not increased'); + assertEq(queuedSharesAfter, queuedSharesBefore + exitAmount, 'Queued shares not increased'); + assertEq(unclaimedAssetsAfter, unclaimedAssetsBefore, 'Unclaimed assets should not change'); + assertEq( + totalExitingAssetsAfter, + totalExitingAssetsBefore, + 'Total exiting assets should not change' + ); + assertEq(totalTicketsAfter, totalTicketsBefore, 'Total tickets should not change'); + + queuedSharesBefore = queuedSharesAfter; + unclaimedAssetsBefore = unclaimedAssetsAfter; + totalExitingAssetsBefore = totalExitingAssetsAfter; + totalTicketsBefore = totalTicketsAfter; _mintGnoToken( address(vault), - vault.totalExitingAssets() + vault.convertToAssets(vault.queuedShares()) + totalExitingAssetsAfter + vault.convertToAssets(queuedSharesAfter) ); // Process exit queue by updating state IKeeperRewards.HarvestParams memory harvestParams = _setGnoVaultReward(address(vault), 0, 0); vault.updateState(harvestParams); + (queuedSharesAfter, unclaimedAssetsAfter, totalExitingAssetsAfter, totalTicketsAfter) = vault + .getExitQueueData(); + assertLt( + queuedSharesAfter, + queuedSharesBefore, + 'Queued shares should be reduced after processing exit queue' + ); + assertGt( + unclaimedAssetsAfter, + unclaimedAssetsBefore, + 'Unclaimed assets should increase after processing exit queue' + ); + assertLe( + totalExitingAssetsAfter, + totalExitingAssetsBefore, + 'Total exiting assets should not change after processing exit queue' + ); + assertGt( + totalTicketsAfter, + totalTicketsBefore, + 'Total tickets should increase after processing exit queue' + ); + // Check that position can be found in exit queue int256 exitQueueIndex = vault.getExitQueueIndex(positionTicket); assertGt(exitQueueIndex, -1, 'Exit queue index not found'); diff --git a/test/gnosis/GnoVaultExitQueue.t.sol b/test/gnosis/GnoVaultExitQueue.t.sol index 13831b8b..5c036cbd 100644 --- a/test/gnosis/GnoVaultExitQueue.t.sol +++ b/test/gnosis/GnoVaultExitQueue.t.sol @@ -8,6 +8,11 @@ import {IGnoVault} from '../../contracts/interfaces/IGnoVault.sol'; import {IVaultState} from '../../contracts/interfaces/IVaultState.sol'; import {GnoVault} from '../../contracts/vaults/gnosis/GnoVault.sol'; +interface IVaultStateV2 { + function totalExitingAssets() external view returns (uint128); + function queuedShares() external view returns (uint128); +} + contract GnoVaultExitQueueTest is Test, GnoHelpers { ForkContracts public contracts; GnoVault public vault; @@ -92,7 +97,7 @@ contract GnoVaultExitQueueTest is Test, GnoHelpers { // Step 2: Add 3 exit requests to the vault uint256 exitShares = vault.convertToShares(exitAmount); - uint256 totalExitingAssetsBefore = vault.totalExitingAssets(); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(vault)).totalExitingAssets(); timestamp1 = vm.getBlockTimestamp(); vm.prank(user1); @@ -108,7 +113,7 @@ contract GnoVaultExitQueueTest is Test, GnoHelpers { // Verify exit requests are in the queue assertEq( - vault.totalExitingAssets(), + IVaultStateV2(address(vault)).totalExitingAssets(), totalExitingAssetsBefore + exitAmount * 3, 'Exit requests not added to queue' ); @@ -287,15 +292,16 @@ contract GnoVaultExitQueueTest is Test, GnoHelpers { vm.prank(user1); vault.enterExitQueue(exitAmount, user1); - uint256 totalExitingAssetsBefore = vault.totalExitingAssets(); + uint256 totalExitingAssetsBefore = IVaultStateV2(address(vault)).totalExitingAssets(); // Upgrade the vault to v3 _upgradeVault(VaultType.GnoVault, vaultAddr); // Calculate what the penalty should be + (, , uint128 totalExitingAssets, ) = vault.getExitQueueData(); int256 penalty = -1 ether; // 1 GNO worth of penalty - uint256 expectedPenalty = (uint256(-penalty) * uint256(vault.totalExitingAssets())) / - (uint256(vault.totalExitingAssets()) + uint256(vault.totalAssets())); + uint256 expectedPenalty = (uint256(-penalty) * uint256(totalExitingAssets)) / + (uint256(totalExitingAssets) + uint256(vault.totalAssets())); // Set a negative reward (penalty) and update the vault state IKeeperRewards.HarvestParams memory harvestParams = _setGnoVaultReward( @@ -313,8 +319,9 @@ contract GnoVaultExitQueueTest is Test, GnoHelpers { _stopSnapshotGas(); // Verify the exiting assets were penalized + (, , totalExitingAssets, ) = vault.getExitQueueData(); assertLt( - vault.totalExitingAssets(), + totalExitingAssets, totalExitingAssetsBefore + exitAmount, 'Exiting assets should be reduced by the penalty' ); diff --git a/test/gnosis/VaultGnoStaking.t.sol b/test/gnosis/VaultGnoStaking.t.sol index b2780b22..f6329982 100644 --- a/test/gnosis/VaultGnoStaking.t.sol +++ b/test/gnosis/VaultGnoStaking.t.sol @@ -189,8 +189,11 @@ contract VaultGnoStakingTest is Test, GnoHelpers { function test_vaultAssets() public { // Initial check uint256 initialAssets = vault.totalAssets(); - uint256 senderDeposit = vault.convertToAssets(vault.queuedShares()) + - vault.totalExitingAssets() + + (uint128 queuedShares, uint128 unclaimedAssets, uint128 totalExitingAssets, ) = vault + .getExitQueueData(); + uint256 senderDeposit = vault.convertToAssets(queuedShares) + + totalExitingAssets + + unclaimedAssets + 1 ether; // Deposit GNO @@ -455,7 +458,7 @@ contract VaultGnoStakingTest is Test, GnoHelpers { // Check for ValidatorFunded event vm.expectEmit(true, true, true, true); - emit IVaultValidators.ValidatorFunded(publicKey, 1 ether); + emit IVaultValidators.ValidatorFunded(publicKey, 32 ether); vm.prank(validatorsManager); _startSnapshotGas('VaultGnoStakingCoverageTest_test_registerValidator_topUp_valid'); @@ -490,8 +493,11 @@ contract VaultGnoStakingTest is Test, GnoHelpers { _collateralizeGnoVault(address(vault)); // Deposit GNO to the vault - uint256 senderDeposit = vault.convertToAssets(vault.queuedShares()) + - vault.totalExitingAssets() + + (uint128 queuedShares, uint128 unclaimedAssets, uint128 totalExitingAssets, ) = vault + .getExitQueueData(); + uint256 senderDeposit = vault.convertToAssets(queuedShares) + + totalExitingAssets + + unclaimedAssets + depositAmount; _mintGnoToken(sender, senderDeposit); _depositGno(senderDeposit, sender, sender);