From 6dcc982e497d9ff7553a776367b57a415225cc44 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 24 Apr 2024 10:12:19 +1000 Subject: [PATCH 1/5] Fixed native staking deployment since the strategist is got from the vault --- contracts/deploy/091_native_ssv_staking.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/deploy/091_native_ssv_staking.js b/contracts/deploy/091_native_ssv_staking.js index 36e0c3393a..37d46cc697 100644 --- a/contracts/deploy/091_native_ssv_staking.js +++ b/contracts/deploy/091_native_ssv_staking.js @@ -159,12 +159,6 @@ module.exports = deploymentWithGovernanceProposal( signature: "setAccountingGovernor(address)", args: [deployerAddr], // TODO: change this to the defender action }, - // 6. configure strategist address - { - contract: cStrategy, - signature: "setStrategist(address)", - args: [strategistAddr], - }, ], }; } From 78082bef5675435aa1cc085d7c4ca9c2b0c2a548 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 24 Apr 2024 17:15:01 +1000 Subject: [PATCH 2/5] Refactor of some Native Staking events Refactor of Native Staking unit tests --- .../NativeStaking/ValidatorAccountant.sol | 21 +- .../NativeStaking/ValidatorRegistrator.sol | 8 +- .../docs/NativeStakingSSVStrategySquashed.svg | 8 +- contracts/test/_fixture.js | 2 +- contracts/test/strategies/nativeSSVStaking.js | 183 ++++++------------ 5 files changed, 77 insertions(+), 145 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index f6d919b4b7..7c6e1b9ce3 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -30,12 +30,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { uint256[50] private __gap; - event FuseIntervalUpdated( - uint256 oldStart, - uint256 oldEnd, - uint256 start, - uint256 end - ); + event FuseIntervalUpdated(uint256 start, uint256 end); event AccountingFullyWithdrawnValidator( uint256 noOfValidators, uint256 remainingValidators, @@ -45,10 +40,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { uint256 remainingValidators, uint256 wethSentToVault ); - event AccountingGovernorAddressChanged( - address oldAddress, - address newAddress - ); + event AccountingGovernorChanged(address newAddress); event AccountingBeaconChainRewards(uint256 amount); event AccountingManuallyFixed( @@ -98,7 +90,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { } function setAccountingGovernor(address _address) external onlyGovernor { - emit AccountingGovernorAddressChanged(accountingGovernor, _address); + emit AccountingGovernorChanged(_address); accountingGovernor = _address; } @@ -115,12 +107,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { "incorrect fuse interval" ); - emit FuseIntervalUpdated( - fuseIntervalStart, - fuseIntervalEnd, - _fuseIntervalStart, - _fuseIntervalEnd - ); + emit FuseIntervalUpdated(_fuseIntervalStart, _fuseIntervalEnd); fuseIntervalStart = _fuseIntervalStart; fuseIntervalEnd = _fuseIntervalEnd; diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol index 27aaaf247c..0f3cc8a46a 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol @@ -45,7 +45,7 @@ abstract contract ValidatorRegistrator is Governable, Pausable { EXIT_COMPLETE // validator has funds withdrawn to the EigenPod and is removed from the SSV } - event RegistratorAddressChanged(address oldAddress, address newAddress); + event RegistratorChanged(address newAddress); event ETHStaked(bytes pubkey, uint256 amount, bytes withdrawal_credentials); event SSVValidatorRegistered(bytes pubkey, uint64[] operatorIds); event SSVValidatorExitInitiated(bytes pubkey, uint64[] operatorIds); @@ -73,9 +73,9 @@ abstract contract ValidatorRegistrator is Governable, Pausable { SSV_NETWORK_ADDRESS = _ssvNetwork; } - /// @notice Set the address of the registrator - function setRegistratorAddress(address _address) external onlyGovernor { - emit RegistratorAddressChanged(validatorRegistrator, _address); + /// @notice Set the address of the registrator which can register, exit and remove validators + function setRegistrator(address _address) external onlyGovernor { + emit RegistratorChanged(_address); validatorRegistrator = _address; } diff --git a/contracts/docs/NativeStakingSSVStrategySquashed.svg b/contracts/docs/NativeStakingSSVStrategySquashed.svg index a12c2db559..c6eab8e365 100644 --- a/contracts/docs/NativeStakingSSVStrategySquashed.svg +++ b/contracts/docs/NativeStakingSSVStrategySquashed.svg @@ -77,7 +77,7 @@    <<payable>> null() <<NativeStakingSSVStrategy>>    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>>    claimGovernance() <<Governable>> -    setRegistratorAddress(_address: address) <<onlyGovernor>> <<ValidatorRegistrator>> +    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>> @@ -106,15 +106,15 @@    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>>    <<event>> Paused(account: address) <<Pausable>>    <<event>> Unpaused(account: address) <<Pausable>> -    <<event>> RegistratorAddressChanged(oldAddress: address, newAddress: address) <<ValidatorRegistrator>> +    <<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(oldStart: uint256, oldEnd: uint256, start: uint256, end: uint256) <<ValidatorAccountant>> +    <<event>> FuseIntervalUpdated(start: uint256, end: uint256) <<ValidatorAccountant>>    <<event>> AccountingFullyWithdrawnValidator(noOfValidators: uint256, remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>>    <<event>> AccountingValidatorSlashed(remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>> -    <<event>> AccountingGovernorAddressChanged(oldAddress: address, newAddress: address) <<ValidatorAccountant>> +    <<event>> AccountingGovernorChanged(newAddress: address) <<ValidatorAccountant>>    <<event>> AccountingBeaconChainRewards(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>> diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index f969368877..af4f8e3efc 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1449,7 +1449,7 @@ async function nativeStakingSSVStrategyFixture() { await nativeStakingSSVStrategy .connect(sGovernor) - .setRegistratorAddress(governorAddr); + .setRegistrator(governorAddr); await nativeStakingSSVStrategy .connect(sGovernor) diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index dafaed2aff..859a295c13 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -49,7 +49,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { })); describe("Initial setup", function () { - it("Should not allow sending of ETH to the strategy via a transaction", async () => { + it("Should not allow ETH to be sent to the strategy if not Fee Accumulator", async () => { const { nativeStakingSSVStrategy, strategist } = fixture; const signer = nativeStakingSSVStrategy.provider.getSigner( @@ -82,21 +82,11 @@ describe("Unit test: Native SSV Staking Strategy", function () { const tx = await nativeStakingSSVStrategy .connect(governor) - .setRegistratorAddress(strategist.address); + .setRegistrator(strategist.address); - const events = (await tx.wait()).events || []; - const RegistratorAddressChangedEvent = events.find( - (e) => e.event === "RegistratorAddressChanged" - ); - - expect(RegistratorAddressChangedEvent).to.not.be.undefined; - expect(RegistratorAddressChangedEvent.event).to.equal( - "RegistratorAddressChanged" - ); - expect(RegistratorAddressChangedEvent.args[0]).to.equal(governor.address); - expect(RegistratorAddressChangedEvent.args[1]).to.equal( - strategist.address - ); + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "RegistratorChanged") + .withArgs(strategist.address); }); it("Non governor should not be able to change the registrator address", async () => { @@ -105,7 +95,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy .connect(strategist) - .setRegistratorAddress(strategist.address) + .setRegistrator(strategist.address) ).to.be.revertedWith("Caller is not the Governor"); }); @@ -152,8 +142,6 @@ describe("Unit test: Native SSV Staking Strategy", function () { it("Governor should be able to change fuse interval", async () => { const { nativeStakingSSVStrategy, governor } = fixture; - const oldFuseStartBn = parseEther("21.6"); - const oldFuseEndBn = parseEther("25.6"); const fuseStartBn = parseEther("22.6"); const fuseEndBn = parseEther("26.6"); @@ -161,17 +149,9 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(governor) .setFuseInterval(fuseStartBn, fuseEndBn); - const events = (await tx.wait()).events || []; - const FuseIntervalUpdated = events.find( - (e) => e.event === "FuseIntervalUpdated" - ); - - expect(FuseIntervalUpdated).to.not.be.undefined; - expect(FuseIntervalUpdated.event).to.equal("FuseIntervalUpdated"); - expect(FuseIntervalUpdated.args[0]).to.equal(oldFuseStartBn); // prev fuse start - expect(FuseIntervalUpdated.args[1]).to.equal(oldFuseEndBn); // prev fuse end - expect(FuseIntervalUpdated.args[2]).to.equal(fuseStartBn); // fuse start - expect(FuseIntervalUpdated.args[3]).to.equal(fuseEndBn); // fuse end + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "FuseIntervalUpdated") + .withArgs(fuseStartBn, fuseEndBn); }); it("Only accounting governor can call accounting", async () => {}); @@ -193,19 +173,9 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(governor) .setAccountingGovernor(strategist.address); - const events = (await tx.wait()).events || []; - const AccountingGovernorChangedEvent = events.find( - (e) => e.event === "AccountingGovernorAddressChanged" - ); - - expect(AccountingGovernorChangedEvent).to.not.be.undefined; - expect(AccountingGovernorChangedEvent.event).to.equal( - "AccountingGovernorAddressChanged" - ); - expect(AccountingGovernorChangedEvent.args[0]).to.equal(governor.address); - expect(AccountingGovernorChangedEvent.args[1]).to.equal( - strategist.address - ); + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingGovernorChanged") + .withArgs(strategist.address); }); }); @@ -302,57 +272,52 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(governor) .doAccounting(); - const events = (await tx.wait()).events || []; - - const BeaconRewardsEvent = events.find( - (e) => e.event === "AccountingBeaconChainRewards" - ); if (expectedRewards.gt(BigNumber.from("0"))) { - expect(BeaconRewardsEvent).to.not.be.undefined; - expect(BeaconRewardsEvent.args[0]).to.equal(expectedRewards); + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingBeaconChainRewards") + .withArgs(expectedRewards); } else { - expect(BeaconRewardsEvent).to.be.undefined; + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, + "AccountingBeaconChainRewards" + ); } - const WithdrawnEvent = events.find( - (e) => e.event === "AccountingFullyWithdrawnValidator" - ); if (expectedValidatorsFullWithdrawals > 0) { - expect(WithdrawnEvent).to.not.be.undefined; - expect(WithdrawnEvent.args[0]).to.equal( - BigNumber.from(`${expectedValidatorsFullWithdrawals}`) - ); - // still active validators - expect(WithdrawnEvent.args[1]).to.equal( - BigNumber.from(`${30 - expectedValidatorsFullWithdrawals}`) - ); - // weth sent to vault - expect(WithdrawnEvent.args[2]).to.equal( - parseEther("32").mul( - BigNumber.from(`${expectedValidatorsFullWithdrawals}`) + await expect(tx) + .to.emit( + nativeStakingSSVStrategy, + "AccountingFullyWithdrawnValidator" ) - ); + .withArgs( + expectedValidatorsFullWithdrawals, + 30 - expectedValidatorsFullWithdrawals, + parseEther("32").mul(expectedValidatorsFullWithdrawals) + ); } else { - expect(WithdrawnEvent).to.be.undefined; + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, + "AccountingFullyWithdrawnValidator" + ); } - const PausedEvent = events.find((e) => e.event === "Paused"); if (fuseBlown) { - expect(PausedEvent).to.not.be.undefined; + await expect(tx).to.emit(nativeStakingSSVStrategy, "Paused"); } else { - expect(PausedEvent).to.be.undefined; + await expect(tx).to.not.emit(nativeStakingSSVStrategy, "Paused"); } - const SlashEvent = events.find( - (e) => e.event === "AccountingValidatorSlashed" - ); if (slashDetected) { - expect(SlashEvent).to.not.be.undefined; - expect(SlashEvent.args[0]).to.equal( - BigNumber.from(`${30 - expectedValidatorsFullWithdrawals - 1}`) - ); + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingValidatorSlashed") + .withNamedArgs({ + remainingValidators: 30 - expectedValidatorsFullWithdrawals - 1, + }); } else { - expect(SlashEvent).to.be.undefined; + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, + "AccountingValidatorSlashed" + ); } }); } @@ -465,29 +430,16 @@ describe("Unit test: Native SSV Staking Strategy", function () { parseEther("5", "ether") //_wethThresholdCheck ); - const events = (await tx.wait()).events || []; - const AccountingManuallyFixedEvent = events.find( - (e) => e.event === "AccountingManuallyFixed" - ); - - expect(AccountingManuallyFixedEvent).to.not.be.undefined; - expect(AccountingManuallyFixedEvent.event).to.equal( - "AccountingManuallyFixed" - ); - expect(AccountingManuallyFixedEvent.args[0]).to.equal(0); // oldActiveDepositedValidators - expect(AccountingManuallyFixedEvent.args[1]).to.equal(3); // activeDepositedValidators - expect(AccountingManuallyFixedEvent.args[2]).to.equal( - parseEther("0", "ether") - ); // oldBeaconChainRewardWETH - expect(AccountingManuallyFixedEvent.args[3]).to.equal( - parseEther("2.3", "ether") - ); // beaconChainRewardWETH - expect(AccountingManuallyFixedEvent.args[4]).to.equal( - parseEther("2.1", "ether") - ); // ethToWeth - expect(AccountingManuallyFixedEvent.args[5]).to.equal( - parseEther("2.2", "ether") - ); // wethToBeSentToVault + expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") + .withArgs( + 0, // oldActiveDepositedValidators + 3, // activeDepositedValidators + 0, // oldBeaconChainRewardWETH + parseEther("2.3"), // beaconChainRewardWETH + parseEther("2.1"), // ethToWeth + parseEther("2.2") // wethToBeSentToVault + ); }); }); @@ -580,29 +532,22 @@ describe("Unit test: Native SSV Staking Strategy", function () { const tx = await nativeStakingSSVStrategy .connect(sHarvester) .collectRewardTokens(); - const events = (await tx.wait()).events || []; - - const harvesterBalanceDiff = ( - await weth.balanceOf(oethHarvester.address) - ).sub(harvesterWethBalance); - expect(harvesterBalanceDiff).to.equal(expectedHarvester); - - const rewardTokenCollectedEvent = events.find( - (e) => e.event === "RewardTokenCollected" - ); if (expectedHarvester.gt(BigNumber.from("0"))) { - expect(rewardTokenCollectedEvent).to.not.be.undefined; - expect(rewardTokenCollectedEvent.event).to.equal( + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "RewardTokenCollected") + .withArgs(oethHarvester.address, weth.address, expectedHarvester); + } else { + await expect(tx).to.not.emit( + nativeStakingSSVStrategy, "RewardTokenCollected" ); - expect(rewardTokenCollectedEvent.args[1]).to.equal(weth.address); - expect(rewardTokenCollectedEvent.args[2]).to.equal( - expectedHarvester - ); - } else { - expect(rewardTokenCollectedEvent).to.be.undefined; } + + const harvesterBalanceDiff = ( + await weth.balanceOf(oethHarvester.address) + ).sub(harvesterWethBalance); + expect(harvesterBalanceDiff).to.equal(expectedHarvester); }); } }); From 5689686a5317514c7df03e12225f90868cba5953 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 24 Apr 2024 18:54:58 +1000 Subject: [PATCH 3/5] Renamed AccountingBeaconChainRewards to AccountingConsensusRewards Accounting updated to handle zero ETH from the beacon chain --- .../NativeStaking/ValidatorAccountant.sol | 23 ++- .../docs/NativeStakingSSVStrategySquashed.svg | 2 +- contracts/test/strategies/nativeSSVStaking.js | 183 +++++++++++++----- 3 files changed, 153 insertions(+), 55 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index 7c6e1b9ce3..8c3d356532 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -41,7 +41,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { uint256 wethSentToVault ); event AccountingGovernorChanged(address newAddress); - event AccountingBeaconChainRewards(uint256 amount); + event AccountingConsensusRewards(uint256 amount); event AccountingManuallyFixed( uint256 oldActiveDepositedValidators, @@ -116,10 +116,10 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { /* solhint-disable max-line-length */ /// This notion page offers a good explanation of how the accounting functions /// https://www.notion.so/originprotocol/Limited-simplified-native-staking-accounting-67a217c8420d40678eb943b9da0ee77d - /// In short, after dividing by 32 if the ETH remaining on the contract falls between 0 and fuseIntervalStart the accounting - /// function will treat that ETH as a Beacon Chain Reward ETH. - /// On the contrary if after dividing by 32 the ETH remaining on the contract falls between fuseIntervalEnd and 32 the - /// accounting function will treat that as a validator slashing. + /// In short, after dividing by 32, if the ETH remaining on the contract falls between 0 and fuseIntervalStart, + /// the accounting function will treat that ETH as Beacon chain consensus rewards. + /// On the contrary, if after dividing by 32, the ETH remaining on the contract falls between fuseIntervalEnd and 32, + /// the accounting function will treat that as a validator slashing. /// @notice Perform the accounting attributing beacon chain ETH to either full or partial withdrawals. Returns true when /// accounting is valid and fuse isn't "blown". Returns false when fuse is blown. /// @dev This function could in theory be permission-less but lets allow only the Registrator (Defender Action) to call it @@ -154,13 +154,18 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { // should be less than a whole validator stake require(ethRemaining < 32 ether, "unexpected accounting"); - // Beacon chain rewards swept (partial validator withdrawals) - if (ethRemaining <= fuseIntervalStart) { + // If no Beacon chain consensus rewards swept + if (ethRemaining == 0) { + // do nothing + return accountingValid; + } + // Beacon chain consensus rewards swept (partial validator withdrawals) + else if (ethRemaining < fuseIntervalStart) { // solhint-disable-next-line reentrancy consensusRewards += ethRemaining; - emit AccountingBeaconChainRewards(ethRemaining); + emit AccountingConsensusRewards(ethRemaining); } - // Beacon chain rewards swept but also a slashed validator fully exited + // Beacon chain consensus rewards swept but also a slashed validator fully exited else if (ethRemaining >= fuseIntervalEnd) { IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: ethRemaining }(); IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, ethRemaining); diff --git a/contracts/docs/NativeStakingSSVStrategySquashed.svg b/contracts/docs/NativeStakingSSVStrategySquashed.svg index c6eab8e365..53d7e4d623 100644 --- a/contracts/docs/NativeStakingSSVStrategySquashed.svg +++ b/contracts/docs/NativeStakingSSVStrategySquashed.svg @@ -115,7 +115,7 @@    <<event>> AccountingFullyWithdrawnValidator(noOfValidators: uint256, remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>>    <<event>> AccountingValidatorSlashed(remainingValidators: uint256, wethSentToVault: uint256) <<ValidatorAccountant>>    <<event>> AccountingGovernorChanged(newAddress: address) <<ValidatorAccountant>> -    <<event>> AccountingBeaconChainRewards(amount: uint256) <<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>> diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index 859a295c13..68e3aa322b 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); const { BigNumber } = require("ethers"); -const { formatUnits, parseEther } = require("ethers").utils; +const { parseEther } = require("ethers").utils; const { setBalance } = require("@nomicfoundation/hardhat-network-helpers"); const { isCI } = require("../helpers"); @@ -180,91 +180,184 @@ describe("Unit test: Native SSV Staking Strategy", function () { }); describe("Accounting", function () { + // fuseStart 21.6 + // fuseEnd 25.6 + const testCases = [ - // normal beacon chain rewards + // no new rewards { - ethBalance: parseEther("14"), - expectedRewards: parseEther("14"), + ethBalance: 0, + expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: false, fuseBlown: false, }, - // normal beacon chain rewards + 1 withdrawn validator + // tiny consensus rewards { - ethBalance: parseEther("34"), - expectedRewards: parseEther("2"), - expectedValidatorsFullWithdrawals: 1, + ethBalance: 0.001, + expectedConsensusRewards: 0.001, + expectedValidatorsFullWithdrawals: 0, slashDetected: false, fuseBlown: false, }, - // 8 withdrawn validators + beacon chain rewards + // large consensus rewards { - ethBalance: parseEther("276"), - expectedRewards: parseEther("20"), - expectedValidatorsFullWithdrawals: 8, + ethBalance: 14, + expectedConsensusRewards: 14, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: false, + }, + // just under fuse start + { + ethBalance: 21.5, + expectedConsensusRewards: 21.5, + expectedValidatorsFullWithdrawals: 0, slashDetected: false, fuseBlown: false, }, + // exactly fuse start + { + ethBalance: 21.6, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: false, + fuseBlown: true, + }, // fuse blown { - ethBalance: parseEther("22"), - expectedRewards: parseEther("0"), + ethBalance: 22, + expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: false, fuseBlown: true, }, - // fuse blown + 1 full withdrawal + // just under fuse end { - ethBalance: parseEther("54"), - expectedRewards: parseEther("0"), - expectedValidatorsFullWithdrawals: 1, + ethBalance: 25.5, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, slashDetected: false, fuseBlown: true, }, + // exactly fuse end + { + ethBalance: 25.6, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: true, + fuseBlown: false, + }, + // just over fuse end + { + ethBalance: 25.7, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 0, + slashDetected: true, + fuseBlown: false, + }, // 1 validator slashed { - ethBalance: parseEther("26.6"), - expectedRewards: parseEther("0"), + ethBalance: 26.6, + expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: true, fuseBlown: false, }, + // no consensus rewards, 1 validator fully withdrawn + { + ethBalance: 32, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards + 1 withdrawn validator + { + ethBalance: 32.01, + expectedConsensusRewards: 0.01, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // large consensus rewards + 1 withdrawn validator + { + ethBalance: 34, + expectedConsensusRewards: 2, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: false, + }, + // fuse blown + 2 withdrawn validator + { + ethBalance: 54, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 1, + slashDetected: false, + fuseBlown: true, + }, // 1 validator fully withdrawn + 1 slashed { - ethBalance: parseEther("58.6"), // 26.6 + 32 - expectedRewards: parseEther("0"), + ethBalance: 58.6, // 26.6 + 32 + expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 1, slashDetected: true, fuseBlown: false, }, + // 2 full withdraws + { + ethBalance: 64, + expectedConsensusRewards: 0, + expectedValidatorsFullWithdrawals: 2, + slashDetected: false, + fuseBlown: false, + }, + // tiny consensus rewards + 2 withdrawn validators + { + ethBalance: 64.1, + expectedConsensusRewards: 0.1, + expectedValidatorsFullWithdrawals: 2, + slashDetected: false, + fuseBlown: false, + }, + // 8 withdrawn validators + consensus rewards + { + ethBalance: 276, + expectedConsensusRewards: 20, + expectedValidatorsFullWithdrawals: 8, + slashDetected: false, + fuseBlown: false, + }, ]; for (const testCase of testCases) { - const { - ethBalance, - expectedRewards, - expectedValidatorsFullWithdrawals, - slashDetected, - fuseBlown, - } = testCase; - it(`Expect that ${formatUnits( - ethBalance - )} ETH will result in ${formatUnits( - expectedRewards - )} ETH rewards and ${expectedValidatorsFullWithdrawals} validators withdrawn.`, async () => { + const { expectedValidatorsFullWithdrawals, slashDetected, fuseBlown } = + testCase; + const ethBalance = parseEther(testCase.ethBalance.toString()); + const expectedConsensusRewards = parseEther( + testCase.expectedConsensusRewards.toString() + ); + + it.only(`Expect ${testCase.ethBalance} ETH balance will result in ${ + testCase.expectedConsensusRewards + } consensus rewards, ${expectedValidatorsFullWithdrawals} withdraws${ + fuseBlown ? ", fuse blown" : "" + }${slashDetected ? ", slash detected" : ""}.`, async () => { const { nativeStakingSSVStrategy, governor, strategist } = fixture; // setup state - await setBalance(nativeStakingSSVStrategy.address, ethBalance); + 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 - parseEther("0", "ether"), //_ethToWeth - parseEther("0", "ether"), //_wethToBeSentToVault - parseEther("0", "ether"), //_beaconChainRewardWETH - parseEther("3000", "ether"), //_ethThresholdCheck - parseEther("3000", "ether") //_wethThresholdCheck + 0, //_ethToWeth + 0, //_wethToBeSentToVault + 0, //_consensusRewards + parseEther("3000"), //_ethThresholdCheck + parseEther("3000") //_wethThresholdCheck ); // check accounting values @@ -272,14 +365,14 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(governor) .doAccounting(); - if (expectedRewards.gt(BigNumber.from("0"))) { + if (expectedConsensusRewards.gt(BigNumber.from("0"))) { await expect(tx) - .to.emit(nativeStakingSSVStrategy, "AccountingBeaconChainRewards") - .withArgs(expectedRewards); + .to.emit(nativeStakingSSVStrategy, "AccountingConsensusRewards") + .withArgs(expectedConsensusRewards); } else { await expect(tx).to.not.emit( nativeStakingSSVStrategy, - "AccountingBeaconChainRewards" + "AccountingConsensusRewards" ); } @@ -332,7 +425,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { 10, //_activeDepositedValidators parseEther("2", "ether"), //_ethToWeth parseEther("2", "ether"), //_wethToBeSentToVault - parseEther("2", "ether"), //_beaconChainRewardWETH + parseEther("2", "ether"), //_consensusRewards parseEther("0", "ether"), //_ethThresholdCheck parseEther("0", "ether") //_wethThresholdCheck ) From 66d556488cdeb7d6c4ec52dc40c2458ad9d137c4 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 24 Apr 2024 20:00:49 +1000 Subject: [PATCH 4/5] fixed bug not accounting for previous consensus rewards Blow fuse if ETH balance < previous consensus rewards --- .../NativeStaking/ValidatorAccountant.sol | 8 +- contracts/test/strategies/nativeSSVStaking.js | 92 ++++++++++++++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index 8c3d356532..91182386c8 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -130,6 +130,12 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { onlyRegistrator returns (bool accountingValid) { + if (address(this).balance < consensusRewards) { + // pause and fail the accounting + _pause(); + return false; + } + // Calculate all the new ETH that has been swept to the contract since the last accounting uint256 newSweptETH = address(this).balance - consensusRewards; accountingValid = true; @@ -150,7 +156,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { ); } - uint256 ethRemaining = address(this).balance; + uint256 ethRemaining = address(this).balance - consensusRewards; // should be less than a whole validator stake require(ethRemaining < 32 ether, "unexpected accounting"); diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index 68e3aa322b..19ecd9cdb8 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -187,22 +187,61 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 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, @@ -211,6 +250,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // just under fuse start { ethBalance: 21.5, + previousConsensusRewards: 0, expectedConsensusRewards: 21.5, expectedValidatorsFullWithdrawals: 0, slashDetected: false, @@ -219,6 +259,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // exactly fuse start { ethBalance: 21.6, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: false, @@ -227,6 +268,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // fuse blown { ethBalance: 22, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: false, @@ -235,6 +277,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // just under fuse end { ethBalance: 25.5, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: false, @@ -243,6 +286,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // exactly fuse end { ethBalance: 25.6, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: true, @@ -251,6 +295,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // just over fuse end { ethBalance: 25.7, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 0, slashDetected: true, @@ -259,6 +304,16 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 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, @@ -267,6 +322,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // no consensus rewards, 1 validator fully withdrawn { ethBalance: 32, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 1, slashDetected: false, @@ -275,22 +331,43 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 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, }, - // fuse blown + 2 withdrawn validator + // 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, @@ -299,6 +376,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 1 validator fully withdrawn + 1 slashed { ethBalance: 58.6, // 26.6 + 32 + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 1, slashDetected: true, @@ -307,6 +385,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 2 full withdraws { ethBalance: 64, + previousConsensusRewards: 0, expectedConsensusRewards: 0, expectedValidatorsFullWithdrawals: 2, slashDetected: false, @@ -315,6 +394,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // tiny consensus rewards + 2 withdrawn validators { ethBalance: 64.1, + previousConsensusRewards: 0, expectedConsensusRewards: 0.1, expectedValidatorsFullWithdrawals: 2, slashDetected: false, @@ -323,6 +403,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { // 8 withdrawn validators + consensus rewards { ethBalance: 276, + previousConsensusRewards: 0, expectedConsensusRewards: 20, expectedValidatorsFullWithdrawals: 8, slashDetected: false, @@ -334,11 +415,16 @@ describe("Unit test: Native SSV Staking Strategy", function () { const { expectedValidatorsFullWithdrawals, slashDetected, fuseBlown } = testCase; const ethBalance = parseEther(testCase.ethBalance.toString()); + const previousConsensusRewards = parseEther( + testCase.previousConsensusRewards.toString() + ); const expectedConsensusRewards = parseEther( testCase.expectedConsensusRewards.toString() ); - it.only(`Expect ${testCase.ethBalance} ETH balance will result in ${ + it(`Expect ${testCase.ethBalance} ETH balance and ${ + testCase.previousConsensusRewards + } previous consensus rewards will result in ${ testCase.expectedConsensusRewards } consensus rewards, ${expectedValidatorsFullWithdrawals} withdraws${ fuseBlown ? ", fuse blown" : "" @@ -355,7 +441,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { 30, // activeDepositedValidators 0, //_ethToWeth 0, //_wethToBeSentToVault - 0, //_consensusRewards + previousConsensusRewards, //_consensusRewards parseEther("3000"), //_ethThresholdCheck parseEther("3000") //_wethThresholdCheck ); From 7af69e4fd5ad5d601b85cedba425bd6e2a0ff540 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 24 Apr 2024 23:27:14 +1000 Subject: [PATCH 5/5] Pause collectRewardTokens and doAccounting on accounting failure. Validated asset on deposit to Native Staking Strategy. Moved depositSSV from NativeStakingSSVStrategy to ValidatorRegistrator moved onlyStrategist modified and VAULT_ADDRESS immutable from ValidatorAccountant to ValidatorRegistrator manuallyFixAccounting changed to use whenPaused modifier made fuseIntervalEnd inclusive Natspec updates refactoring of native staking unit tests --- .../NativeStakingSSVStrategy.sol | 48 +- .../NativeStaking/ValidatorAccountant.sol | 24 +- .../NativeStaking/ValidatorRegistrator.sol | 33 + contracts/deploy/091_native_ssv_staking.js | 2 +- .../docs/NativeStakingSSVStrategySquashed.svg | 265 +++--- contracts/test/_fixture.js | 5 +- contracts/test/strategies/nativeSSVStaking.js | 852 +++++++++--------- 7 files changed, 635 insertions(+), 594 deletions(-) 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 () => {}); + }); + } }); });