diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index 31fd891951..1014d0a546 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -10,6 +10,8 @@ import { IWETH9 } from "../../interfaces/IWETH9.sol"; /// Full withdrawals are from exited validators. /// @author Origin Protocol Inc abstract contract ValidatorAccountant is ValidatorRegistrator { + /// @notice The minimum amount of blocks that need to pass between two calls to manuallyFixAccounting + uint256 public constant MIN_FIX_ACCOUNTING_CADENCE = 7200; // 1 day /// @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; @@ -21,8 +23,10 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { uint256 public fuseIntervalStart; /// @notice end of fuse interval uint256 public fuseIntervalEnd; + /// @notice last block number manuallyFixAccounting has been called + uint256 public lastFixAccountingBlockNumber; - uint256[50] private __gap; + uint256[49] private __gap; event FuseIntervalUpdated(uint256 start, uint256 end); event AccountingFullyWithdrawnValidator( @@ -186,6 +190,11 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { int256 _validatorsDelta, int256 _consensusRewardsDelta ) external onlyStrategist whenPaused { + require( + lastFixAccountingBlockNumber + MIN_FIX_ACCOUNTING_CADENCE < + block.number, + "manuallyFixAccounting called too soon" + ); require( _validatorsDelta >= -3 && _validatorsDelta <= 3 && @@ -210,6 +219,8 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { int256(consensusRewards) + _consensusRewardsDelta ); + lastFixAccountingBlockNumber = block.number; + // rerun the accounting to see if it has now been fixed. // Do not pause the accounting on failure as it is already paused require(_doAccounting(false), "fuse still blown"); diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index da1babcf5f..e96de92d3b 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -4,6 +4,7 @@ const { parseEther } = require("ethers").utils; const { setBalance, setStorageAt, + mine, } = require("@nomicfoundation/hardhat-network-helpers"); const { isCI } = require("../helpers"); @@ -12,6 +13,7 @@ const { shouldBehaveLikeHarvestable } = require("../behaviour/harvestable"); const { shouldBehaveLikeStrategy } = require("../behaviour/strategy"); const { MAX_UINT256 } = require("../../utils/constants"); const { impersonateAndFund } = require("../../utils/signers"); +const minFixAccountingCadence = 7200 + 1; const { createFixtureLoader, @@ -543,6 +545,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { const { nativeStakingSSVStrategy, strategist } = fixture; await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( @@ -563,6 +566,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { const { nativeStakingSSVStrategy, strategist } = fixture; await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( @@ -587,6 +591,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { await setActiveDepositedValidators(10, nativeStakingSSVStrategy); await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); const activeDepositedValidatorsBefore = await nativeStakingSSVStrategy.activeDepositedValidators(); @@ -619,6 +624,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { await setActiveDepositedValidators(10000, nativeStakingSSVStrategy); await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); const consensusRewardsDelta = parseEther(delta.toString()); const tx = await nativeStakingSSVStrategy @@ -647,6 +653,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { .transfer(nativeStakingSSVStrategy.address, parseEther("5")); await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); // unit test fixture sets OUSD governor as accounting governor const tx = await nativeStakingSSVStrategy .connect(strategist) @@ -662,6 +669,50 @@ describe("Unit test: Native SSV Staking Strategy", function () { parseEther("2.3") // consensusRewards ); }); + + it("Calling manually fix accounting too often should result in an error", async () => { + const { nativeStakingSSVStrategy, strategist, governor } = fixture; + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting( + 0, //_validatorsDelta + parseEther("0") //_consensusRewardsDelta + ); + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence - 4); + await expect( + nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( + 0, //_validatorsDelta + parseEther("0") //_consensusRewardsDelta + ) + ).to.be.revertedWith("manuallyFixAccounting called too soon"); + }); + + it("Calling manually fix accounting twice with enough blocks in between should pass", async () => { + const { nativeStakingSSVStrategy, strategist, governor } = fixture; + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting( + 0, //_validatorsDelta + parseEther("0") //_consensusRewardsDelta + ); + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting( + 0, //_validatorsDelta + parseEther("0") //_consensusRewardsDelta + ); + }); }); });