diff --git a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol index 1c4f44f02d..75a6f6cc4e 100644 --- a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol @@ -82,7 +82,8 @@ contract NativeStakingSSVStrategy is } /// @dev Convert accumulated ETH to WETH and send to the Harvester. - function _collectRewardTokens() internal override { + /// Will revert if the strategy is paused for accounting. + function _collectRewardTokens() internal override whenNotPaused { // collect ETH from execution rewards from the fee accumulator uint256 executionRewards = FeeAccumulator(FEE_ACCUMULATOR_ADDRESS) .collect(); @@ -114,19 +115,23 @@ contract NativeStakingSSVStrategy is } } - /// @notice Deposit asset into the underlying platform - /// @param _asset Address of asset to deposit - /// @param _amount Amount of assets to deposit + /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. + /// It just checks the asset is WETH and emits the Deposit event. + /// To deposit WETH into validators `registerSsvValidator` and `stakeEth` must be used. + /// Will NOT revert if the strategy is paused from an accounting failure. + /// @param _asset Address of asset to deposit. Has to be WETH. + /// @param _amount Amount of assets that were transferred to the strategy by the vault. function deposit(address _asset, uint256 _amount) external override onlyVault nonReentrant { + require(_asset == WETH_TOKEN_ADDRESS, "Unsupported asset"); _deposit(_asset, _amount); } - /// @dev Deposit WETH to this contract to enable automated action to stake it + /// @dev Deposit WETH to this strategy so it can later be staked into a validator. /// @param _asset Address of WETH /// @param _amount Amount of WETH to deposit function _deposit(address _asset, uint256 _amount) internal { @@ -143,7 +148,10 @@ contract NativeStakingSSVStrategy is emit Deposit(_asset, address(0), _amount); } - /// @notice Deposit the entire balance of WETH asset in the strategy into the underlying platform + /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. + /// It just emits the Deposit event. + /// To deposit WETH into validators `registerSsvValidator` and `stakeEth` must be used. + /// Will NOT revert if the strategy is paused from an accounting failure. function depositAll() external override onlyVault nonReentrant { uint256 wethBalance = IERC20(WETH_TOKEN_ADDRESS).balanceOf( address(this) @@ -157,6 +165,7 @@ contract NativeStakingSSVStrategy is /// can happen when: /// - the deposit was not a multiple of 32 WETH /// - someone sent WETH directly to this contract + /// Will NOT revert if the strategy is paused from an accounting failure. /// @param _recipient Address to receive withdrawn assets /// @param _asset WETH to withdraw /// @param _amount Amount of WETH to withdraw @@ -180,7 +189,14 @@ contract NativeStakingSSVStrategy is IERC20(_asset).safeTransfer(_recipient, _amount); } - /// @notice Remove all supported assets from the underlying platform and send them to Vault contract. + /// @notice transfer all WETH deposits back to the vault. + /// This does not withdraw from the validators. That has to be done separately with the + /// `exitSsvValidator` and `removeSsvValidator` operations. + /// This does not withdraw any execution rewards from the FeeAccumulator or + /// consensus rewards in this strategy. + /// Any ETH in this strategy that was swept from a full validator withdrawal will not be withdrawn. + /// ETH from full validator withdrawals is sent to the Vault using `doAccounting`. + /// Will NOT revert if the strategy is paused from an accounting failure. function withdrawAll() external override onlyVaultOrGovernor nonReentrant { uint256 wethBalance = IERC20(WETH_TOKEN_ADDRESS).balanceOf( address(this) @@ -234,24 +250,6 @@ contract NativeStakingSSVStrategy is ); } - /// @notice Deposits more SSV Tokens to the SSV Network contract which is used to pay the SSV Operators. - /// @dev A SSV cluster is defined by the SSVOwnerAddress and the set of operatorIds. - /// uses "onlyStrategist" modifier so continuous front-running can't DOS our maintenance service - /// that tries to top up SSV tokens. - /// @param cluster The SSV cluster details that must be derived from emitted events from the SSVNetwork contract. - function depositSSV( - uint64[] memory operatorIds, - uint256 amount, - Cluster memory cluster - ) external onlyStrategist { - ISSVNetwork(SSV_NETWORK_ADDRESS).deposit( - address(this), - operatorIds, - amount, - cluster - ); - } - /** * @notice Only accept ETH from the FeeAccumulator * @dev don't want to receive donations from anyone else as this will diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index 91182386c8..2f1843ee56 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; import { ValidatorRegistrator } from "./ValidatorRegistrator.sol"; -import { IVault } from "../../interfaces/IVault.sol"; import { IWETH9 } from "../../interfaces/IWETH9.sol"; /// @title Validator Accountant @@ -15,8 +14,6 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { /// @notice The maximum amount of ETH that can be staked by a validator /// @dev this can change in the future with EIP-7251, Increase the MAX_EFFECTIVE_BALANCE uint256 public constant MAX_STAKE = 32 ether; - /// @notice Address of the OETH Vault proxy contract - address public immutable VAULT_ADDRESS; /// @notice Keeps track of the total consensus rewards swept from the beacon chain uint256 public consensusRewards = 0; @@ -61,15 +58,6 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { _; } - /// @dev Throws if called by any account other than the Strategist - modifier onlyStrategist() { - require( - msg.sender == IVault(VAULT_ADDRESS).strategistAddr(), - "Caller is not the Strategist" - ); - _; - } - /// @param _wethAddress Address of the Erc20 WETH Token contract /// @param _vaultAddress Address of the Vault /// @param _beaconChainDepositContract Address of the beacon chain deposit contract @@ -82,12 +70,11 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { ) ValidatorRegistrator( _wethAddress, + _vaultAddress, _beaconChainDepositContract, _ssvNetwork ) - { - VAULT_ADDRESS = _vaultAddress; - } + {} function setAccountingGovernor(address _address) external onlyGovernor { emit AccountingGovernorChanged(_address); @@ -128,6 +115,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { function doAccounting() external onlyRegistrator + whenNotPaused returns (bool accountingValid) { if (address(this).balance < consensusRewards) { @@ -172,7 +160,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { emit AccountingConsensusRewards(ethRemaining); } // Beacon chain consensus rewards swept but also a slashed validator fully exited - else if (ethRemaining >= fuseIntervalEnd) { + else if (ethRemaining > fuseIntervalEnd) { IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: ethRemaining }(); IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, ethRemaining); activeDepositedValidators -= 1; @@ -207,9 +195,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { uint256 _consensusRewards, uint256 _ethThresholdCheck, uint256 _wethThresholdCheck - ) external onlyAccountingGovernor { - require(paused(), "not paused"); - + ) external onlyAccountingGovernor whenPaused { uint256 ethBalance = address(this).balance; uint256 wethBalance = IWETH9(WETH_TOKEN_ADDRESS).balanceOf( address(this) diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol index 0f3cc8a46a..d609efe6d6 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; import { Governable } from "../../governance/Governable.sol"; import { IDepositContract } from "../../interfaces/IDepositContract.sol"; +import { IVault } from "../../interfaces/IVault.sol"; import { IWETH9 } from "../../interfaces/IWETH9.sol"; import { ISSVNetwork, Cluster } from "../../interfaces/ISSVNetwork.sol"; @@ -25,6 +26,8 @@ abstract contract ValidatorRegistrator is Governable, Pausable { address public immutable BEACON_CHAIN_DEPOSIT_CONTRACT; /// @notice The address of the SSV Network contract used to interface with address public immutable SSV_NETWORK_ADDRESS; + /// @notice Address of the OETH Vault proxy contract + address public immutable VAULT_ADDRESS; /// @notice Address of the registrator - allowed to register, exit and remove validators address public validatorRegistrator; @@ -60,17 +63,29 @@ abstract contract ValidatorRegistrator is Governable, Pausable { _; } + /// @dev Throws if called by any account other than the Strategist + modifier onlyStrategist() { + require( + msg.sender == IVault(VAULT_ADDRESS).strategistAddr(), + "Caller is not the Strategist" + ); + _; + } + /// @param _wethAddress Address of the Erc20 WETH Token contract + /// @param _vaultAddress Address of the Vault /// @param _beaconChainDepositContract Address of the beacon chain deposit contract /// @param _ssvNetwork Address of the SSV Network contract constructor( address _wethAddress, + address _vaultAddress, address _beaconChainDepositContract, address _ssvNetwork ) { WETH_TOKEN_ADDRESS = _wethAddress; BEACON_CHAIN_DEPOSIT_CONTRACT = _beaconChainDepositContract; SSV_NETWORK_ADDRESS = _ssvNetwork; + VAULT_ADDRESS = _vaultAddress; } /// @notice Set the address of the registrator which can register, exit and remove validators @@ -201,4 +216,22 @@ abstract contract ValidatorRegistrator is Governable, Pausable { validatorsStates[keccak256(publicKey)] = VALIDATOR_STATE.EXIT_COMPLETE; } + + /// @notice Deposits more SSV Tokens to the SSV Network contract which is used to pay the SSV Operators. + /// @dev A SSV cluster is defined by the SSVOwnerAddress and the set of operatorIds. + /// uses "onlyStrategist" modifier so continuous front-running can't DOS our maintenance service + /// that tries to top up SSV tokens. + /// @param cluster The SSV cluster details that must be derived from emitted events from the SSVNetwork contract. + function depositSSV( + uint64[] memory operatorIds, + uint256 amount, + Cluster memory cluster + ) external onlyStrategist { + ISSVNetwork(SSV_NETWORK_ADDRESS).deposit( + address(this), + operatorIds, + amount, + cluster + ); + } } diff --git a/contracts/deploy/091_native_ssv_staking.js b/contracts/deploy/091_native_ssv_staking.js index 37d46cc697..9b4748ad3b 100644 --- a/contracts/deploy/091_native_ssv_staking.js +++ b/contracts/deploy/091_native_ssv_staking.js @@ -11,7 +11,7 @@ module.exports = deploymentWithGovernanceProposal( // "", }, async ({ deployWithConfirmation, ethers, getTxOpts, withConfirmation }) => { - const { deployerAddr, strategistAddr } = await getNamedAccounts(); + const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); // Current contracts diff --git a/contracts/docs/NativeStakingSSVStrategySquashed.svg b/contracts/docs/NativeStakingSSVStrategySquashed.svg index 53d7e4d623..392788f64f 100644 --- a/contracts/docs/NativeStakingSSVStrategySquashed.svg +++ b/contracts/docs/NativeStakingSSVStrategySquashed.svg @@ -4,143 +4,142 @@ - - + + UmlClassDiagram - + 280 - -NativeStakingSSVStrategy -../contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol - -Private: -   governorPosition: bytes32 <<Governable>> -   pendingGovernorPosition: bytes32 <<Governable>> -   reentryStatusPosition: bytes32 <<Governable>> -   _paused: bool <<Pausable>> -   __gap: uint256[50] <<ValidatorRegistrator>> -   __gap: uint256[50] <<ValidatorAccountant>> -   initialized: bool <<Initializable>> -   initializing: bool <<Initializable>> -   ______gap: uint256[50] <<Initializable>> -   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> -   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> -   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> -   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> -   _reserved: int256[98] <<InitializableAbstractStrategy>> -   __gap: uint256[50] <<NativeStakingSSVStrategy>> -Internal: -   assetsMapped: address[] <<InitializableAbstractStrategy>> -Public: -   _NOT_ENTERED: uint256 <<Governable>> -   _ENTERED: uint256 <<Governable>> -   WETH_TOKEN_ADDRESS: address <<ValidatorRegistrator>> -   BEACON_CHAIN_DEPOSIT_CONTRACT: address <<ValidatorRegistrator>> -   SSV_NETWORK_ADDRESS: address <<ValidatorRegistrator>> -   validatorRegistrator: address <<ValidatorRegistrator>> -   activeDepositedValidators: uint256 <<ValidatorRegistrator>> -   validatorsStates: mapping(bytes32=>VALIDATOR_STATE) <<ValidatorRegistrator>> -   MAX_STAKE: uint256 <<ValidatorAccountant>> -   VAULT_ADDRESS: address <<ValidatorAccountant>> -   consensusRewards: uint256 <<ValidatorAccountant>> -   fuseIntervalStart: uint256 <<ValidatorAccountant>> -   fuseIntervalEnd: uint256 <<ValidatorAccountant>> -   accountingGovernor: address <<ValidatorAccountant>> -   platformAddress: address <<InitializableAbstractStrategy>> -   vaultAddress: address <<InitializableAbstractStrategy>> -   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> -   harvesterAddress: address <<InitializableAbstractStrategy>> -   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> -   SSV_TOKEN_ADDRESS: address <<NativeStakingSSVStrategy>> -   FEE_ACCUMULATOR_ADDRESS: address <<NativeStakingSSVStrategy>> - -Internal: -    _governor(): (governorOut: address) <<Governable>> -    _pendingGovernor(): (pendingGovernor: address) <<Governable>> -    _setGovernor(newGovernor: address) <<Governable>> -    _setPendingGovernor(newGovernor: address) <<Governable>> -    _changeGovernor(_newGovernor: address) <<Governable>> -    _msgSender(): address <<Context>> -    _msgData(): bytes <<Context>> -    _pause() <<whenNotPaused>> <<Pausable>> -    _unpause() <<whenPaused>> <<Pausable>> -    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> -    _collectRewardTokens() <<NativeStakingSSVStrategy>> -    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    _abstractSetPToken(_asset: address, address) <<NativeStakingSSVStrategy>> -    _deposit(_asset: address, _amount: uint256) <<NativeStakingSSVStrategy>> -    _withdraw(_recipient: address, _asset: address, _amount: uint256) <<NativeStakingSSVStrategy>> -External: -    <<payable>> null() <<NativeStakingSSVStrategy>> -    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> -    claimGovernance() <<Governable>> -    setRegistrator(_address: address) <<onlyGovernor>> <<ValidatorRegistrator>> -    stakeEth(validators: ValidatorStakeData[]) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> -    registerSsvValidator(publicKey: bytes, operatorIds: uint64[], sharesData: bytes, amount: uint256, cluster: Cluster) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> -    exitSsvValidator(publicKey: bytes, operatorIds: uint64[]) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> -    removeSsvValidator(publicKey: bytes, operatorIds: uint64[], cluster: Cluster) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> -    setAccountingGovernor(_address: address) <<onlyGovernor>> <<ValidatorAccountant>> -    setFuseInterval(_fuseIntervalStart: uint256, _fuseIntervalEnd: uint256) <<onlyGovernor>> <<ValidatorAccountant>> -    doAccounting(): (accountingValid: bool) <<onlyRegistrator>> <<ValidatorAccountant>> -    manuallyFixAccounting(_activeDepositedValidators: uint256, _ethToWeth: uint256, _wethToBeSentToVault: uint256, _consensusRewards: uint256, _ethThresholdCheck: uint256, _wethThresholdCheck: uint256) <<onlyAccountingGovernor>> <<ValidatorAccountant>> -    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<InitializableAbstractStrategy>> -    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> -    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> -    safeApproveAllTokens() <<NativeStakingSSVStrategy>> -    deposit(_asset: address, _amount: uint256) <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> -    depositAll() <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> -    withdraw(_recipient: address, _asset: address, _amount: uint256) <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> -    withdrawAll() <<onlyVaultOrGovernor, nonReentrant>> <<NativeStakingSSVStrategy>> -    checkBalance(_asset: address): (balance: uint256) <<NativeStakingSSVStrategy>> -    initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<onlyGovernor, initializer>> <<NativeStakingSSVStrategy>> -    pause() <<onlyStrategist>> <<NativeStakingSSVStrategy>> -    depositSSV(operatorIds: uint64[], amount: uint256, cluster: Cluster) <<onlyStrategist>> <<NativeStakingSSVStrategy>> -Public: -    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> -    <<event>> Paused(account: address) <<Pausable>> -    <<event>> Unpaused(account: address) <<Pausable>> -    <<event>> RegistratorChanged(newAddress: address) <<ValidatorRegistrator>> -    <<event>> ETHStaked(pubkey: bytes, amount: uint256, withdrawal_credentials: bytes) <<ValidatorRegistrator>> -    <<event>> SSVValidatorRegistered(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> -    <<event>> SSVValidatorExitInitiated(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> -    <<event>> SSVValidatorExitCompleted(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> -    <<event>> FuseIntervalUpdated(start: uint256, end: uint256) <<ValidatorAccountant>> -    <<event>> AccountingFullyWithdrawnValidator(noOfValidators: uint256, remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>> -    <<event>> AccountingValidatorSlashed(remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>> -    <<event>> AccountingGovernorChanged(newAddress: address) <<ValidatorAccountant>> -    <<event>> AccountingConsensusRewards(amount: uint256) <<ValidatorAccountant>> -    <<event>> AccountingManuallyFixed(oldActiveDepositedValidators: uint256, activeDepositedValidators: uint256, oldBeaconChainRewards: uint256, beaconChainRewards: uint256, ethToWeth: uint256, wethToBeSentToVault: uint256) <<ValidatorAccountant>> -    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> -    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> -    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> -    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> -    <<modifier>> onlyGovernor() <<Governable>> -    <<modifier>> nonReentrant() <<Governable>> -    <<modifier>> whenNotPaused() <<Pausable>> -    <<modifier>> whenPaused() <<Pausable>> -    <<modifier>> onlyRegistrator() <<ValidatorRegistrator>> -    <<modifier>> onlyAccountingGovernor() <<ValidatorAccountant>> -    <<modifier>> onlyStrategist() <<ValidatorAccountant>> -    <<modifier>> initializer() <<Initializable>> -    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> -    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> -    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> -    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> -    constructor() <<Pausable>> -    governor(): address <<Governable>> -    isGovernor(): bool <<Governable>> -    paused(): bool <<Pausable>> -    constructor(_wethAddress: address, _beaconChainDepositContract: address, _ssvNetwork: address) <<ValidatorRegistrator>> + +NativeStakingSSVStrategy +../contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol + +Private: +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _paused: bool <<Pausable>> +   __gap: uint256[50] <<ValidatorRegistrator>> +   __gap: uint256[50] <<ValidatorAccountant>> +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> +   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> +   _reserved: int256[98] <<InitializableAbstractStrategy>> +   __gap: uint256[50] <<NativeStakingSSVStrategy>> +Internal: +   assetsMapped: address[] <<InitializableAbstractStrategy>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   WETH_TOKEN_ADDRESS: address <<ValidatorRegistrator>> +   BEACON_CHAIN_DEPOSIT_CONTRACT: address <<ValidatorRegistrator>> +   SSV_NETWORK_ADDRESS: address <<ValidatorRegistrator>> +   VAULT_ADDRESS: address <<ValidatorRegistrator>> +   validatorRegistrator: address <<ValidatorRegistrator>> +   activeDepositedValidators: uint256 <<ValidatorRegistrator>> +   validatorsStates: mapping(bytes32=>VALIDATOR_STATE) <<ValidatorRegistrator>> +   MAX_STAKE: uint256 <<ValidatorAccountant>> +   consensusRewards: uint256 <<ValidatorAccountant>> +   fuseIntervalStart: uint256 <<ValidatorAccountant>> +   fuseIntervalEnd: uint256 <<ValidatorAccountant>> +   accountingGovernor: address <<ValidatorAccountant>> +   platformAddress: address <<InitializableAbstractStrategy>> +   vaultAddress: address <<InitializableAbstractStrategy>> +   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> +   harvesterAddress: address <<InitializableAbstractStrategy>> +   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> +   SSV_TOKEN_ADDRESS: address <<NativeStakingSSVStrategy>> +   FEE_ACCUMULATOR_ADDRESS: address <<NativeStakingSSVStrategy>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _msgSender(): address <<Context>> +    _msgData(): bytes <<Context>> +    _pause() <<whenNotPaused>> <<Pausable>> +    _unpause() <<whenPaused>> <<Pausable>> +    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> +    _collectRewardTokens() <<whenNotPaused>> <<NativeStakingSSVStrategy>> +    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    _abstractSetPToken(_asset: address, address) <<NativeStakingSSVStrategy>> +    _deposit(_asset: address, _amount: uint256) <<NativeStakingSSVStrategy>> +    _withdraw(_recipient: address, _asset: address, _amount: uint256) <<NativeStakingSSVStrategy>> +External: +    <<payable>> null() <<NativeStakingSSVStrategy>> +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    setRegistrator(_address: address) <<onlyGovernor>> <<ValidatorRegistrator>> +    stakeEth(validators: ValidatorStakeData[]) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> +    registerSsvValidator(publicKey: bytes, operatorIds: uint64[], sharesData: bytes, amount: uint256, cluster: Cluster) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> +    exitSsvValidator(publicKey: bytes, operatorIds: uint64[]) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> +    removeSsvValidator(publicKey: bytes, operatorIds: uint64[], cluster: Cluster) <<onlyRegistrator, whenNotPaused>> <<ValidatorRegistrator>> +    depositSSV(operatorIds: uint64[], amount: uint256, cluster: Cluster) <<onlyStrategist>> <<ValidatorRegistrator>> +    setAccountingGovernor(_address: address) <<onlyGovernor>> <<ValidatorAccountant>> +    setFuseInterval(_fuseIntervalStart: uint256, _fuseIntervalEnd: uint256) <<onlyGovernor>> <<ValidatorAccountant>> +    doAccounting(): (accountingValid: bool) <<onlyRegistrator, whenNotPaused>> <<ValidatorAccountant>> +    manuallyFixAccounting(_activeDepositedValidators: uint256, _ethToWeth: uint256, _wethToBeSentToVault: uint256, _consensusRewards: uint256, _ethThresholdCheck: uint256, _wethThresholdCheck: uint256) <<onlyAccountingGovernor, whenPaused>> <<ValidatorAccountant>> +    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<InitializableAbstractStrategy>> +    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> +    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    safeApproveAllTokens() <<NativeStakingSSVStrategy>> +    deposit(_asset: address, _amount: uint256) <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> +    depositAll() <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> +    withdraw(_recipient: address, _asset: address, _amount: uint256) <<onlyVault, nonReentrant>> <<NativeStakingSSVStrategy>> +    withdrawAll() <<onlyVaultOrGovernor, nonReentrant>> <<NativeStakingSSVStrategy>> +    checkBalance(_asset: address): (balance: uint256) <<NativeStakingSSVStrategy>> +    initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<onlyGovernor, initializer>> <<NativeStakingSSVStrategy>> +    pause() <<onlyStrategist>> <<NativeStakingSSVStrategy>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> Paused(account: address) <<Pausable>> +    <<event>> Unpaused(account: address) <<Pausable>> +    <<event>> RegistratorChanged(newAddress: address) <<ValidatorRegistrator>> +    <<event>> ETHStaked(pubkey: bytes, amount: uint256, withdrawal_credentials: bytes) <<ValidatorRegistrator>> +    <<event>> SSVValidatorRegistered(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> +    <<event>> SSVValidatorExitInitiated(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> +    <<event>> SSVValidatorExitCompleted(pubkey: bytes, operatorIds: uint64[]) <<ValidatorRegistrator>> +    <<event>> FuseIntervalUpdated(start: uint256, end: uint256) <<ValidatorAccountant>> +    <<event>> AccountingFullyWithdrawnValidator(noOfValidators: uint256, remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>> +    <<event>> AccountingValidatorSlashed(remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>> +    <<event>> AccountingGovernorChanged(newAddress: address) <<ValidatorAccountant>> +    <<event>> AccountingConsensusRewards(amount: uint256) <<ValidatorAccountant>> +    <<event>> AccountingManuallyFixed(oldActiveDepositedValidators: uint256, activeDepositedValidators: uint256, oldBeaconChainRewards: uint256, beaconChainRewards: uint256, ethToWeth: uint256, wethToBeSentToVault: uint256) <<ValidatorAccountant>> +    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> +    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> whenNotPaused() <<Pausable>> +    <<modifier>> whenPaused() <<Pausable>> +    <<modifier>> onlyRegistrator() <<ValidatorRegistrator>> +    <<modifier>> onlyStrategist() <<ValidatorRegistrator>> +    <<modifier>> onlyAccountingGovernor() <<ValidatorAccountant>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> +    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> +    constructor() <<Pausable>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    paused(): bool <<Pausable>>    constructor(_wethAddress: address, _vaultAddress: address, _beaconChainDepositContract: address, _ssvNetwork: address) <<ValidatorAccountant>>    constructor(_config: BaseStrategyConfig) <<InitializableAbstractStrategy>>    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index af4f8e3efc..91714bc6f2 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1430,10 +1430,7 @@ async function nativeStakingSSVStrategyFixture() { .connect(sGovernor) .approveStrategy(nativeStakingSSVStrategy.address); - console.log( - "nativeStakingSSVStrategy.address", - nativeStakingSSVStrategy.address - ); + log("nativeStakingSSVStrategy.address", nativeStakingSSVStrategy.address); const fuseStartBn = ethers.utils.parseEther("21.6"); const fuseEndBn = ethers.utils.parseEther("25.6"); diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index 19ecd9cdb8..c176d3fea3 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -180,326 +180,366 @@ describe("Unit test: Native SSV Staking Strategy", function () { }); describe("Accounting", function () { - // fuseStart 21.6 - // fuseEnd 25.6 + describe("Should account for beacon chain ETH", function () { + // fuseStart 21.6 + // fuseEnd 25.6 + + const testCases = [ + // no new rewards + { + ethBalance: 0, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // no new rewards on previous rewards + { + ethBalance: 0.001, + previousConsensusRewards: 0.001, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // invalid eth balance + { + ethBalance: 1.9, + previousConsensusRewards: 2, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, + // tiny consensus rewards + { + ethBalance: 0.001, + previousConsensusRewards: 0, + expectedConsensusRewards: 0.001, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards on small previous rewards + { + ethBalance: 0.03, + previousConsensusRewards: 0.02, + expectedConsensusRewards: 0.01, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards on large previous rewards + { + ethBalance: 5.04, + previousConsensusRewards: 5, + expectedConsensusRewards: 0.04, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // large consensus rewards + { + ethBalance: 14, + previousConsensusRewards: 0, + expectedConsensusRewards: 14, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // just under fuse start + { + ethBalance: 21.5, + previousConsensusRewards: 0, + expectedConsensusRewards: 21.5, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // exactly fuse start + { + ethBalance: 21.6, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, + // fuse blown + { + ethBalance: 22, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, + // just under fuse end + { + ethBalance: 25.5, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, + // exactly fuse end + { + ethBalance: 25.6, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, + // just over fuse end + { + ethBalance: 25.7, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: true, + fuseBlown: false, + }, + // 1 validator slashed + { + ethBalance: 26.6, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: true, + fuseBlown: false, + }, + // no consensus rewards, 1 slashed validator + { + ethBalance: 31.9, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: true, + fuseBlown: false, + }, + // no consensus rewards, 1 validator fully withdrawn + { + ethBalance: 32, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards + 1 withdrawn validator + { + ethBalance: 32.01, + previousConsensusRewards: 0, + expectedConsensusRewards: 0.01, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // consensus rewards on previous rewards > 32 + { + ethBalance: 33, + previousConsensusRewards: 32.3, + expectedConsensusRewards: 0.7, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // large consensus rewards + 1 withdrawn validator + { + ethBalance: 34, + previousConsensusRewards: 0, + expectedConsensusRewards: 2, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // large consensus rewards on large previous rewards + { + ethBalance: 44, + previousConsensusRewards: 24, + expectedConsensusRewards: 20, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // fuse blown + 1 withdrawn validator + { + ethBalance: 54, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: true, + }, + // fuse blown + 1 withdrawn validator with previous rewards + { + ethBalance: 55, + previousConsensusRewards: 1, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: true, + }, + // 1 validator fully withdrawn + 1 slashed + { + ethBalance: 58.6, // 26.6 + 32 + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: true, + fuseBlown: false, + }, + // 2 full withdraws + { + ethBalance: 64, + previousConsensusRewards: 0, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 2, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards + 2 withdrawn validators + { + ethBalance: 64.1, + previousConsensusRewards: 0, + expectedConsensusRewards: 0.1, + expectedValidatorsFullWithdrawals: 2, + slashDetected: false, + fuseBlown: false, + }, + // 2 full withdraws on previous rewards + { + ethBalance: 66, + previousConsensusRewards: 2, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 2, + slashDetected: false, + fuseBlown: false, + }, + // consensus rewards on large previous rewards + { + ethBalance: 66, + previousConsensusRewards: 65, + expectedConsensusRewards: 1, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // consensus rewards on large previous rewards with withdraw + { + ethBalance: 100, + previousConsensusRewards: 65, + expectedConsensusRewards: 3, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // 8 withdrawn validators + consensus rewards + { + ethBalance: 276, + previousConsensusRewards: 0, + expectedConsensusRewards: 20, + expectedValidatorsFullWithdrawals: 8, + slashDetected: false, + fuseBlown: false, + }, + ]; + + for (const testCase of testCases) { + const { expectedValidatorsFullWithdrawals, slashDetected, fuseBlown } = + testCase; + const ethBalance = parseEther(testCase.ethBalance.toString()); + const previousConsensusRewards = parseEther( + testCase.previousConsensusRewards.toString() + ); + const expectedConsensusRewards = parseEther( + testCase.expectedConsensusRewards.toString() + ); - const testCases = [ - // no new rewards - { - ethBalance: 0, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // no new rewards on previous rewards - { - ethBalance: 0.001, - previousConsensusRewards: 0.001, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // invalid eth balance - { - ethBalance: 1.9, - previousConsensusRewards: 2, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: true, - }, - // tiny consensus rewards - { - ethBalance: 0.001, - previousConsensusRewards: 0, - expectedConsensusRewards: 0.001, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // tiny consensus rewards on small previous rewards - { - ethBalance: 0.03, - previousConsensusRewards: 0.02, - expectedConsensusRewards: 0.01, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // tiny consensus rewards on large previous rewards - { - ethBalance: 5.04, - previousConsensusRewards: 5, - expectedConsensusRewards: 0.04, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // large consensus rewards - { - ethBalance: 14, - previousConsensusRewards: 0, - expectedConsensusRewards: 14, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // just under fuse start - { - ethBalance: 21.5, - previousConsensusRewards: 0, - expectedConsensusRewards: 21.5, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // exactly fuse start - { - ethBalance: 21.6, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: true, - }, - // fuse blown - { - ethBalance: 22, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: true, - }, - // just under fuse end - { - ethBalance: 25.5, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: true, - }, - // exactly fuse end - { - ethBalance: 25.6, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: true, - fuseBlown: false, - }, - // just over fuse end - { - ethBalance: 25.7, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: true, - fuseBlown: false, - }, - // 1 validator slashed - { - ethBalance: 26.6, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: true, - fuseBlown: false, - }, - // no consensus rewards, 1 slashed validator - { - ethBalance: 31.9, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 0, - slashDetected: true, - fuseBlown: false, - }, - // no consensus rewards, 1 validator fully withdrawn - { - ethBalance: 32, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 1, - slashDetected: false, - fuseBlown: false, - }, - // tiny consensus rewards + 1 withdrawn validator - { - ethBalance: 32.01, - previousConsensusRewards: 0, - expectedConsensusRewards: 0.01, - expectedValidatorsFullWithdrawals: 1, - slashDetected: false, - fuseBlown: false, - }, - // consensus rewards on previous rewards > 32 - { - ethBalance: 33, - previousConsensusRewards: 32.3, - expectedConsensusRewards: 0.7, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // large consensus rewards + 1 withdrawn validator - { - ethBalance: 34, - previousConsensusRewards: 0, - expectedConsensusRewards: 2, - expectedValidatorsFullWithdrawals: 1, - slashDetected: false, - fuseBlown: false, - }, - // large consensus rewards on large previous rewards - { - ethBalance: 44, - previousConsensusRewards: 24, - expectedConsensusRewards: 20, - expectedValidatorsFullWithdrawals: 0, - slashDetected: false, - fuseBlown: false, - }, - // fuse blown + 1 withdrawn validator - { - ethBalance: 54, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 1, - slashDetected: false, - fuseBlown: true, - }, - // 1 validator fully withdrawn + 1 slashed - { - ethBalance: 58.6, // 26.6 + 32 - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 1, - slashDetected: true, - fuseBlown: false, - }, - // 2 full withdraws - { - ethBalance: 64, - previousConsensusRewards: 0, - expectedConsensusRewards: 0, - expectedValidatorsFullWithdrawals: 2, - slashDetected: false, - fuseBlown: false, - }, - // tiny consensus rewards + 2 withdrawn validators - { - ethBalance: 64.1, - previousConsensusRewards: 0, - expectedConsensusRewards: 0.1, - expectedValidatorsFullWithdrawals: 2, - slashDetected: false, - fuseBlown: false, - }, - // 8 withdrawn validators + consensus rewards - { - ethBalance: 276, - previousConsensusRewards: 0, - expectedConsensusRewards: 20, - expectedValidatorsFullWithdrawals: 8, - slashDetected: false, - fuseBlown: false, - }, - ]; + it(`given ${testCase.ethBalance} ETH balance and ${ + testCase.previousConsensusRewards + } previous consensus rewards, then ${ + testCase.expectedConsensusRewards + } consensus rewards, ${expectedValidatorsFullWithdrawals} withdraws${ + fuseBlown ? ", fuse blown" : "" + }${slashDetected ? ", slash detected" : ""}.`, async () => { + const { nativeStakingSSVStrategy, governor, strategist } = fixture; - for (const testCase of testCases) { - const { expectedValidatorsFullWithdrawals, slashDetected, fuseBlown } = - testCase; - const ethBalance = parseEther(testCase.ethBalance.toString()); - const previousConsensusRewards = parseEther( - testCase.previousConsensusRewards.toString() - ); - const expectedConsensusRewards = parseEther( - testCase.expectedConsensusRewards.toString() - ); + // setup state + if (ethBalance.gt(0)) { + await setBalance(nativeStakingSSVStrategy.address, ethBalance); + } + // pause, so manuallyFixAccounting can be called + await nativeStakingSSVStrategy.connect(strategist).pause(); + await nativeStakingSSVStrategy + .connect(governor) + .manuallyFixAccounting( + 30, // activeDepositedValidators + 0, //_ethToWeth + 0, //_wethToBeSentToVault + previousConsensusRewards, //_consensusRewards + parseEther("3000"), //_ethThresholdCheck + parseEther("3000") //_wethThresholdCheck + ); - it(`Expect ${testCase.ethBalance} ETH balance and ${ - testCase.previousConsensusRewards - } previous consensus rewards will result in ${ - testCase.expectedConsensusRewards - } consensus rewards, ${expectedValidatorsFullWithdrawals} withdraws${ - fuseBlown ? ", fuse blown" : "" - }${slashDetected ? ", slash detected" : ""}.`, async () => { - const { nativeStakingSSVStrategy, governor, strategist } = fixture; - - // setup state - if (ethBalance.gt(0)) { - await setBalance(nativeStakingSSVStrategy.address, ethBalance); - } - // pause, so manuallyFixAccounting can be called - await nativeStakingSSVStrategy.connect(strategist).pause(); - await nativeStakingSSVStrategy.connect(governor).manuallyFixAccounting( - 30, // activeDepositedValidators - 0, //_ethToWeth - 0, //_wethToBeSentToVault - previousConsensusRewards, //_consensusRewards - parseEther("3000"), //_ethThresholdCheck - parseEther("3000") //_wethThresholdCheck - ); + // check accounting values + const tx = await nativeStakingSSVStrategy + .connect(governor) + .doAccounting(); - // check accounting values - const tx = await nativeStakingSSVStrategy - .connect(governor) - .doAccounting(); - - if (expectedConsensusRewards.gt(BigNumber.from("0"))) { - await expect(tx) - .to.emit(nativeStakingSSVStrategy, "AccountingConsensusRewards") - .withArgs(expectedConsensusRewards); - } else { - await expect(tx).to.not.emit( - nativeStakingSSVStrategy, - "AccountingConsensusRewards" - ); - } + if (expectedConsensusRewards.gt(BigNumber.from("0"))) { + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingConsensusRewards") + .withArgs(expectedConsensusRewards); + } else { + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, + "AccountingConsensusRewards" + ); + } - if (expectedValidatorsFullWithdrawals > 0) { - await expect(tx) - .to.emit( + if (expectedValidatorsFullWithdrawals > 0) { + await expect(tx) + .to.emit( + nativeStakingSSVStrategy, + "AccountingFullyWithdrawnValidator" + ) + .withArgs( + expectedValidatorsFullWithdrawals, + 30 - expectedValidatorsFullWithdrawals, + parseEther("32").mul(expectedValidatorsFullWithdrawals) + ); + } else { + await expect(tx).to.not.emit( nativeStakingSSVStrategy, "AccountingFullyWithdrawnValidator" - ) - .withArgs( - expectedValidatorsFullWithdrawals, - 30 - expectedValidatorsFullWithdrawals, - parseEther("32").mul(expectedValidatorsFullWithdrawals) ); - } else { - await expect(tx).to.not.emit( - nativeStakingSSVStrategy, - "AccountingFullyWithdrawnValidator" - ); - } - - if (fuseBlown) { - await expect(tx).to.emit(nativeStakingSSVStrategy, "Paused"); - } else { - await expect(tx).to.not.emit(nativeStakingSSVStrategy, "Paused"); - } - - if (slashDetected) { - await expect(tx) - .to.emit(nativeStakingSSVStrategy, "AccountingValidatorSlashed") - .withNamedArgs({ - remainingValidators: 30 - expectedValidatorsFullWithdrawals - 1, - }); - } else { - await expect(tx).to.not.emit( - nativeStakingSSVStrategy, - "AccountingValidatorSlashed" - ); - } - }); - } + } + + if (fuseBlown) { + await expect(tx).to.emit(nativeStakingSSVStrategy, "Paused"); + } else { + await expect(tx).to.not.emit(nativeStakingSSVStrategy, "Paused"); + } + + if (slashDetected) { + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingValidatorSlashed") + .withNamedArgs({ + remainingValidators: 30 - expectedValidatorsFullWithdrawals - 1, + }); + } else { + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, + "AccountingValidatorSlashed" + ); + } + }); + } + }); it("Only accounting governor is allowed to manually fix accounting", async () => { const { nativeStakingSSVStrategy, strategist } = fixture; @@ -531,7 +571,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { parseEther("1", "ether"), //_ethThresholdCheck parseEther("0", "ether") //_wethThresholdCheck ) - ).to.be.revertedWith("not paused"); + ).to.be.revertedWith("Pausable: not paused"); }); it("Should not execute manual recovery if eth threshold reached", async () => { @@ -622,8 +662,49 @@ describe("Unit test: Native SSV Staking Strategy", function () { }); }); - describe("General functionality", function () { + describe("Harvest and strategy balance", function () { + // fuseStart 21.6 + // fuseEnd 25.6 + // expectedHarvester = feeAccumulatorEth + consensusRewards + // expectedBalance = deposits + nrOfActiveDepositedValidators * 32 const rewardTestCases = [ + // no rewards to harvest + { + feeAccumulatorEth: 0, + consensusRewards: 0, + deposits: 0, + nrOfActiveDepositedValidators: 0, + expectedHarvester: 0, + expectedBalance: 0, + }, + // a little execution rewards + { + feeAccumulatorEth: 0.1, + consensusRewards: 0, + deposits: 0, + nrOfActiveDepositedValidators: 0, + expectedHarvester: 0.1, + expectedBalance: 0, + }, + // a little consensus rewards + { + feeAccumulatorEth: 0, + consensusRewards: 0.2, + deposits: 0, + nrOfActiveDepositedValidators: 0, + expectedHarvester: 0.2, + expectedBalance: 0, + }, + // a little consensus and execution rewards + { + feeAccumulatorEth: 0.1, + consensusRewards: 0.2, + deposits: 0, + nrOfActiveDepositedValidators: 0, + expectedHarvester: 0.3, + expectedBalance: 0, + }, + // a lot of consensus rewards { feeAccumulatorEth: 2.2, consensusRewards: 16.3, @@ -632,56 +713,44 @@ describe("Unit test: Native SSV Staking Strategy", function () { expectedHarvester: 18.5, expectedBalance: 100 + 7 * 32, }, + // consensus rewards just below fuse start { feeAccumulatorEth: 10.2, - consensusRewards: 21.6, + consensusRewards: 21.5, deposits: 0, nrOfActiveDepositedValidators: 5, - expectedHarvester: 31.8, + expectedHarvester: 31.7, expectedBalance: 0 + 5 * 32, }, + // consensus rewards just below fuse start { feeAccumulatorEth: 10.2, - consensusRewards: 21.6, + consensusRewards: 21.5, deposits: 1, nrOfActiveDepositedValidators: 0, - expectedHarvester: 31.8, + expectedHarvester: 31.7, expectedBalance: 1 + 0 * 32, }, - { - feeAccumulatorEth: 0, - consensusRewards: 0, - deposits: 0, - nrOfActiveDepositedValidators: 0, - expectedHarvester: 0, - expectedBalance: 0 + 0 * 32, - }, ]; - describe("Collecting rewards and should correctly account for WETH", async () => { - for (const testCase of rewardTestCases) { - const feeAccumulatorEth = parseEther( - testCase.feeAccumulatorEth.toString() - ); - const consensusRewards = parseEther( - testCase.consensusRewards.toString() - ); - const deposits = parseEther(testCase.deposits.toString()); - const expectedHarvester = parseEther( - testCase.expectedHarvester.toString() - ); + for (const testCase of rewardTestCases) { + const feeAccumulatorEth = parseEther( + testCase.feeAccumulatorEth.toString() + ); + const consensusRewards = parseEther(testCase.consensusRewards.toString()); + const deposits = parseEther(testCase.deposits.toString()); + const expectedHarvester = parseEther( + testCase.expectedHarvester.toString() + ); + const expectedBalance = parseEther(testCase.expectedBalance.toString()); + const { nrOfActiveDepositedValidators } = testCase; - it(`with ${testCase.feeAccumulatorEth} execution rewards, ${testCase.consensusRewards} consensus rewards and ${testCase.deposits} deposits. expect harvest ${testCase.expectedHarvester}`, async () => { - const { - nativeStakingSSVStrategy, - governor, - oethHarvester, - weth, - josh, - } = fixture; + describe(`given ${testCase.feeAccumulatorEth} execution rewards, ${testCase.consensusRewards} consensus rewards, ${testCase.deposits} deposits and ${nrOfActiveDepositedValidators} validators`, () => { + beforeEach(async () => { + const { nativeStakingSSVStrategy, governor, strategist, weth, josh } = + fixture; const feeAccumulatorAddress = await nativeStakingSSVStrategy.FEE_ACCUMULATOR_ADDRESS(); - const sHarvester = await impersonateAndFund(oethHarvester.address); // setup state if (consensusRewards.gt(BigNumber.from("0"))) { @@ -702,10 +771,28 @@ describe("Unit test: Native SSV Staking Strategy", function () { .transfer(nativeStakingSSVStrategy.address, deposits); } + // set the correct amount of staked validators + await nativeStakingSSVStrategy.connect(strategist).pause(); + await nativeStakingSSVStrategy + .connect(governor) + .manuallyFixAccounting( + nrOfActiveDepositedValidators, // activeDepositedValidators + parseEther("0"), //_ethToWeth + parseEther("0"), //_wethToBeSentToVault + consensusRewards, //_consensusRewards + parseEther("3000"), //_ethThresholdCheck + parseEther("3000") //_wethThresholdCheck + ); + // run the accounting await nativeStakingSSVStrategy.connect(governor).doAccounting(); + }); - const harvesterWethBalance = await weth.balanceOf( + it(`then should harvest ${testCase.expectedHarvester} WETH`, async () => { + const { nativeStakingSSVStrategy, oethHarvester, weth } = fixture; + const sHarvester = await impersonateAndFund(oethHarvester.address); + + const harvesterWethBalanceBefore = await weth.balanceOf( oethHarvester.address ); const tx = await nativeStakingSSVStrategy @@ -725,77 +812,18 @@ describe("Unit test: Native SSV Staking Strategy", function () { const harvesterBalanceDiff = ( await weth.balanceOf(oethHarvester.address) - ).sub(harvesterWethBalance); + ).sub(harvesterWethBalanceBefore); expect(harvesterBalanceDiff).to.equal(expectedHarvester); }); - } - }); - - describe("Checking balance should return the correct values", async () => { - for (const testCase of rewardTestCases) { - const feeAccumulatorEth = parseEther( - testCase.feeAccumulatorEth.toString() - ); - const consensusRewards = parseEther( - testCase.consensusRewards.toString() - ); - const deposits = parseEther(testCase.deposits.toString()); - const expectedBalance = parseEther(testCase.expectedBalance.toString()); - const { nrOfActiveDepositedValidators } = testCase; - it(`with ${testCase.feeAccumulatorEth} execution rewards, ${testCase.consensusRewards} consensus rewards, ${testCase.deposits} deposits and ${nrOfActiveDepositedValidators} validators. expected balance ${testCase.expectedBalance}`, async () => { - const { - nativeStakingSSVStrategy, - governor, - strategist, - // oethHarvester, - weth, - josh, - } = fixture; - const feeAccumulatorAddress = - await nativeStakingSSVStrategy.FEE_ACCUMULATOR_ADDRESS(); - - // setup state - if (consensusRewards.gt(BigNumber.from("0"))) { - // set the reward eth on the strategy - await setBalance( - nativeStakingSSVStrategy.address, - consensusRewards - ); - } - if (feeAccumulatorEth.gt(BigNumber.from("0"))) { - // set execution layer rewards on the fee accumulator - await setBalance(feeAccumulatorAddress, feeAccumulatorEth); - } - if (deposits.gt(BigNumber.from("0"))) { - // send eth to the strategy as if Vault would send it via a Deposit function - await weth - .connect(josh) - .transfer(nativeStakingSSVStrategy.address, deposits); - } - // set the correct amount of staked validators - await nativeStakingSSVStrategy.connect(strategist).pause(); - await nativeStakingSSVStrategy - .connect(governor) - .manuallyFixAccounting( - nrOfActiveDepositedValidators, // activeDepositedValidators - parseEther("0", "ether"), //_ethToWeth - parseEther("0", "ether"), //_wethToBeSentToVault - parseEther("0", "ether"), //_beaconChainRewardWETH - parseEther("3000", "ether"), //_ethThresholdCheck - parseEther("3000", "ether") //_wethThresholdCheck - ); - - // run the accounting - await nativeStakingSSVStrategy.connect(governor).doAccounting(); + it(`then the strategy should have a ${testCase.expectedBalance} balance`, async () => { + const { nativeStakingSSVStrategy, weth } = fixture; expect( await nativeStakingSSVStrategy.checkBalance(weth.address) ).to.equal(expectedBalance); }); - } - }); - - it("Should be able to collect the SSV reward token", async () => {}); + }); + } }); });