diff --git a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol index 76737fc398..35ea29debf 100644 --- a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { IWETH9 } from "../../interfaces/IWETH9.sol"; @@ -18,6 +19,26 @@ struct ValidatorStakeData { /// @title Native Staking SSV Strategy /// @notice Strategy to deploy funds into DVT validators powered by the SSV Network /// @author Origin Protocol Inc +/// @dev This contract handles WETH and ETH and in some operations interchanges between the two. Any WETH that +/// is on the contract across multiple blocks (and not just transitory within a transaction) is considered an +/// asset. Meaning deposits increase the balance of the asset and withdrawal decrease it. As opposed to all +/// our other strategies the WETH doesn't immediately get deposited into an underlying strategy and can be present +/// across multiple blocks waiting to be unwrapped to ETH and staked to validators. This separation of WETH and ETH is +/// required since the rewards (reward token) is also in ETH. +/// +/// To simplify the accounting of WETH there is another difference in behavior compared to the other strategies. +/// To withdraw WETH asset - exit message is posted to validators and the ETH hits this contract with multiple days +/// delay. In order to simplify the WETH accounting upon detection of such an event the ValidatorAccountant +/// immediately wraps ETH to WETH and sends it to the Vault. +/// +/// On the other hand any ETH on the contract (across multiple blocks) is there either: +/// - as a result of already accounted for consensus rewards +/// - as a result of not yet accounted for consensus rewards +/// - as a results of not yet accounted for full validator withdrawals (or validator slashes) +/// +/// Even though the strategy assets and rewards are a very similar asset the consensus layer rewards and the +/// execution layer rewards are considered rewards and those are dripped to the Vault over a configurable time +/// interval and not immediately. contract NativeStakingSSVStrategy is ValidatorAccountant, InitializableAbstractStrategy @@ -33,8 +54,20 @@ contract NativeStakingSSVStrategy is /// (rewards for arranging transactions in a way that benefits the validator). address payable public immutable FEE_ACCUMULATOR_ADDRESS; + /// @dev This contract receives WETH as the deposit asset, but unlike other strategies doesn't immediately + /// deposit it to an underlying platform. Rather a special privilege account stakes it to the validators. + /// For that reason calling WETH.balanceOf(this) in a deposit function can contain WETH that has just been + /// deposited and also WETH that has previously been deposited. To keep a correct count we need to keep track + /// of WETH that has already been accounted for. + /// This value represents the amount of WETH balance of this contract that has already been accounted for by the + /// deposit events. + /// It is important to note that this variable is not concerned with WETH that is a result of full/partial + /// withdrawal of the validators. It is strictly concerned with WETH that has been deposited and is waiting to + /// be staked. + uint256 depositedWethAccountedFor; + // For future use - uint256[50] private __gap; + uint256[49] private __gap; /// @param _baseConfig Base strategy config with platformAddress (ERC-4626 Vault contract), eg sfrxETH or sDAI, /// and vaultAddress (OToken Vault contract), eg VaultProxy or OETHVaultProxy @@ -126,6 +159,7 @@ contract NativeStakingSSVStrategy is nonReentrant { require(_asset == WETH_TOKEN_ADDRESS, "Unsupported asset"); + depositedWethAccountedFor += _amount; _deposit(_asset, _amount); } @@ -154,8 +188,12 @@ contract NativeStakingSSVStrategy is uint256 wethBalance = IERC20(WETH_TOKEN_ADDRESS).balanceOf( address(this) ); - if (wethBalance > 0) { - _deposit(WETH_TOKEN_ADDRESS, wethBalance); + uint256 newWeth = wethBalance - depositedWethAccountedFor; + + if (newWeth > 0) { + depositedWethAccountedFor = wethBalance; + + _deposit(WETH_TOKEN_ADDRESS, newWeth); } } @@ -262,7 +300,20 @@ contract NativeStakingSSVStrategy is ); } - function wethWithdrawnToVault(uint256 _amount) internal override { + function _wethWithdrawnToVault(uint256 _amount) internal override { emit Withdrawal(WETH_TOKEN_ADDRESS, address(0), _amount); } + + function _wethWithdrawnAndStaked(uint256 _amount) internal override { + /* In an ideal world we wouldn't need to reduce the deduction amount when the + * depositedWethAccountedFor is smaller than the _amount. + * + * The reason this is required is that a malicious actor could sent WETH direclty + * to this contract and that would circumvent the increase of depositedWethAccountedFor + * property. When the ETH would be staked the depositedWethAccountedFor amount could + * be deducted so much that it would be negative. + */ + uint256 deductAmount = Math.min(_amount, depositedWethAccountedFor); + depositedWethAccountedFor -= deductAmount; + } } diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol index 996a28bfa7..02d5b42500 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorAccountant.sol @@ -42,7 +42,8 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { event AccountingManuallyFixed( int256 validatorsDelta, - int256 consensusRewardsDelta + int256 consensusRewardsDelta, + uint256 wethToVault ); /// @param _wethAddress Address of the Erc20 WETH Token contract @@ -132,8 +133,8 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: wethToVault }(); // slither-disable-next-line unchecked-transfer IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, wethToVault); - wethWithdrawnToVault(wethToVault); - + _wethWithdrawnToVault(wethToVault); + emit AccountingFullyWithdrawnValidator( fullyWithdrawnValidators, activeDepositedValidators, @@ -163,7 +164,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, ethRemaining); activeDepositedValidators -= 1; - wethWithdrawnToVault(ethRemaining); + _wethWithdrawnToVault(ethRemaining); emit AccountingValidatorSlashed( activeDepositedValidators, @@ -194,9 +195,16 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { /// @notice Allow the Strategist to fix the accounting of this strategy and unpause. /// @param _validatorsDelta adjust the active validators by up to plus three or minus three /// @param _consensusRewardsDelta adjust the accounted for consensus rewards up or down + /// @param _ethToVaultAmount the amount of ETH that gets wrapped into WETH and sent to the Vault + /// @dev There is a case when a validator(s) gets slashed so much that the eth swept from + /// the beacon chain enters the fuse area and there are no consensus rewards on the contract + /// to "dip into"/use. To increase the amount of unaccounted ETH over the fuse end interval + /// we need to reduce the amount of active deposited validators and immediately send WETH + /// to the vault, so it doesn't interfere with further accounting. function manuallyFixAccounting( int256 _validatorsDelta, - int256 _consensusRewardsDelta + int256 _consensusRewardsDelta, + uint256 _ethToVaultAmount ) external onlyStrategist whenPaused { require( lastFixAccountingBlockNumber + MIN_FIX_ACCOUNTING_CADENCE < @@ -217,8 +225,13 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { int256(consensusRewards) + _consensusRewardsDelta >= 0, "invalid consensusRewardsDelta" ); + require(_ethToVaultAmount <= 32 ether * 3, "invalid wethToVaultAmount"); - emit AccountingManuallyFixed(_validatorsDelta, _consensusRewardsDelta); + emit AccountingManuallyFixed( + _validatorsDelta, + _consensusRewardsDelta, + _ethToVaultAmount + ); activeDepositedValidators = uint256( int256(activeDepositedValidators) + _validatorsDelta @@ -226,8 +239,16 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { consensusRewards = uint256( int256(consensusRewards) + _consensusRewardsDelta ); - lastFixAccountingBlockNumber = block.number; + if (_ethToVaultAmount > 0) { + IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: _ethToVaultAmount }(); + // slither-disable-next-line unchecked-transfer + IWETH9(WETH_TOKEN_ADDRESS).transfer( + VAULT_ADDRESS, + _ethToVaultAmount + ); + _wethWithdrawnToVault(_ethToVaultAmount); + } // rerun the accounting to see if it has now been fixed. // Do not pause the accounting on failure as it is already paused @@ -242,5 +263,5 @@ abstract contract ValidatorAccountant is ValidatorRegistrator { ****************************************/ /// @dev allows for NativeStakingSSVStrategy contract to emit Withdrawal event - function wethWithdrawnToVault(uint256 _amount) internal virtual; + function _wethWithdrawnToVault(uint256 _amount) internal virtual; } diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol index c8818530c0..23d07e2e4e 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol @@ -114,6 +114,7 @@ abstract contract ValidatorRegistrator is Governable, Pausable { // Convert required ETH from WETH IWETH9(WETH_TOKEN_ADDRESS).withdraw(requiredETH); + _wethWithdrawnAndStaked(requiredETH); uint256 validatorsLength = validators.length; // For each validator @@ -247,4 +248,11 @@ abstract contract ValidatorRegistrator is Governable, Pausable { cluster ); } + + /*************************************** + Abstract + ****************************************/ + + /// @dev allows for NativeStakingSSVStrategy contract know how much WETH had been staked + function _wethWithdrawnAndStaked(uint256 _amount) internal virtual; } diff --git a/contracts/test/behaviour/ssvStrategy.js b/contracts/test/behaviour/ssvStrategy.js index 5bf9f5fc3e..44b4c7949b 100644 --- a/contracts/test/behaviour/ssvStrategy.js +++ b/contracts/test/behaviour/ssvStrategy.js @@ -127,16 +127,23 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { }); describe("Validator operations", function () { - beforeEach(async () => { - const { weth, domen, nativeStakingSSVStrategy } = await context(); + const depositToStrategy = async (amount) => { + const { weth, domen, nativeStakingSSVStrategy, oethVault, strategist } = + await context(); - // Add 32 WETH to the strategy so it can be staked - await weth - .connect(domen) - .transfer(nativeStakingSSVStrategy.address, oethUnits("32")); - }); + // Add 32 WETH to the strategy via a Vualt deposit + await weth.connect(domen).transfer(oethVault.address, amount); + + return await oethVault + .connect(strategist) + .depositToStrategy( + nativeStakingSSVStrategy.address, + [weth.address], + [amount] + ); + }; - it("Should register and staked 32 ETH by validator registrator", async () => { + const registerAndStakeEth = async () => { const { addresses, weth, @@ -157,8 +164,6 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { ssvNetwork: addresses.SSVNetwork, }); - const stakeAmount = oethUnits("32"); - await setERC20TokenBalance( nativeStakingSSVStrategy.address, ssv, @@ -166,6 +171,8 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { hre ); + const stakeAmount = oethUnits("32"); + // Register a new validator with the SSV Network const regTx = await nativeStakingSSVStrategy .connect(validatorRegistrator) @@ -180,7 +187,7 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { .to.emit(nativeStakingSSVStrategy, "SSVValidatorRegistered") .withArgs(testValidator.publicKey, testValidator.operatorIds); - // Stake 32 ETH to the new validator + // Stake stakeAmount ETH to the new validator const stakeTx = await nativeStakingSSVStrategy .connect(validatorRegistrator) .stakeEth([ @@ -195,7 +202,7 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { .to.emit(nativeStakingSSVStrategy, "ETHStaked") .withNamedArgs({ pubkey: testValidator.publicKey, - amount: stakeAmount, + amount: oethUnits("32"), }); expect(await weth.balanceOf(nativeStakingSSVStrategy.address)).to.equal( @@ -204,6 +211,43 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { "strategy WETH not decreased" ) ); + }; + + it("Should register and stake 32 ETH by validator registrator", async () => { + await depositToStrategy(oethUnits("32")); + await registerAndStakeEth(); + }); + + it("Should emit correct values in deposit event", async () => { + const { weth, nativeStakingSSVStrategy } = await context(); + + await depositToStrategy(oethUnits("40")); + // at least 8 WETH has remained on the contract and a deposit all + // event should emit a correct amount + await registerAndStakeEth(); + + /* deposit to strategy calls depositAll on the strategy contract after sending the WETH + * to it. The event should contain only the amount of newly deposited WETH and not include + * the pre-exiting WETH. + */ + const tx = await depositToStrategy(parseEther("10")); + + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "Deposit") + .withArgs(weth.address, AddressZero, parseEther("10")); + }); + + it("Should register and stake 32 ETH even if half supplied by a 3rd party", async () => { + const { weth, domen, nativeStakingSSVStrategy } = await context(); + + await depositToStrategy(oethUnits("16")); + // A malicious actor is sending WETH directly to the native staking contract hoping to + // mess up the accounting. + await weth + .connect(domen) + .transfer(nativeStakingSSVStrategy.address, oethUnits("16")); + + await registerAndStakeEth(); }); it("Should exit and remove validator by validator registrator", async () => { @@ -215,6 +259,7 @@ const shouldBehaveLikeAnSsvStrategy = (context) => { addresses, testValidator, } = await context(); + await depositToStrategy(oethUnits("32")); const { cluster } = await getClusterInfo({ ownerAddress: nativeStakingSSVStrategy.address, diff --git a/contracts/test/strategies/nativeSSVStaking.js b/contracts/test/strategies/nativeSSVStaking.js index d7419dd089..f3cff0d0aa 100644 --- a/contracts/test/strategies/nativeSSVStaking.js +++ b/contracts/test/strategies/nativeSSVStaking.js @@ -13,6 +13,7 @@ const { shouldBehaveLikeHarvestable } = require("../behaviour/harvestable"); const { shouldBehaveLikeStrategy } = require("../behaviour/strategy"); const { MAX_UINT256 } = require("../../utils/constants"); const { impersonateAndFund } = require("../../utils/signers"); +const { zero } = require("../../utils/addresses"); const minFixAccountingCadence = 7200 + 1; const { @@ -447,7 +448,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { } consensus rewards, ${expectedValidatorsFullWithdrawals} withdraws${ fuseBlown ? ", fuse blown" : "" }${slashDetected ? ", slash detected" : ""}.`, async () => { - const { nativeStakingSSVStrategy, governor } = fixture; + const { nativeStakingSSVStrategy, governor, weth } = fixture; // setup state if (ethBalance.gt(0)) { @@ -477,6 +478,9 @@ describe("Unit test: Native SSV Staking Strategy", function () { } if (expectedValidatorsFullWithdrawals > 0) { + const ethWithdrawnToVault = parseEther("32").mul( + expectedValidatorsFullWithdrawals + ); await expect(tx) .to.emit( nativeStakingSSVStrategy, @@ -485,8 +489,12 @@ describe("Unit test: Native SSV Staking Strategy", function () { .withArgs( expectedValidatorsFullWithdrawals, 30 - expectedValidatorsFullWithdrawals, - parseEther("32").mul(expectedValidatorsFullWithdrawals) + ethWithdrawnToVault ); + + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "Withdrawal") + .withArgs(weth.address, zero, ethWithdrawnToVault); } else { await expect(tx).to.not.emit( nativeStakingSSVStrategy, @@ -501,11 +509,22 @@ describe("Unit test: Native SSV Staking Strategy", function () { } if (slashDetected) { + const fullExitEthWithdrawnToVault = parseEther("32").mul( + expectedValidatorsFullWithdrawals + ); + const slashedEthRemaining = ethBalance.sub( + fullExitEthWithdrawnToVault + ); + await expect(tx) .to.emit(nativeStakingSSVStrategy, "AccountingValidatorSlashed") .withNamedArgs({ remainingValidators: 30 - expectedValidatorsFullWithdrawals - 1, }); + + await expect(tx) + .to.emit(nativeStakingSSVStrategy, "Withdrawal") + .withArgs(weth.address, zero, slashedEthRemaining); } else { await expect(tx).to.not.emit( nativeStakingSSVStrategy, @@ -524,7 +543,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy.connect(governor).manuallyFixAccounting( 1, //_validatorsDelta - parseEther("2") //_consensusRewardsDelta + parseEther("2"), //_consensusRewardsDelta, + parseEther("2") //_ethToVault ) ).to.be.revertedWith("Caller is not the Strategist"); }); @@ -536,7 +556,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( 1, //_validatorsDelta - parseEther("2") //_consensusRewardsDelta + parseEther("2"), //_consensusRewardsDelta + parseEther("2") //_ethToVault ) ).to.be.revertedWith("Pausable: not paused"); }); @@ -550,14 +571,16 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( -4, //_validatorsDelta - 0 //_consensusRewardsDelta + 0, //_consensusRewardsDelta, + 0 //_ethToVault ) ).to.be.revertedWith("invalid validatorsDelta"); await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( 4, //_validatorsDelta - 0 //_consensusRewardsDelta + 0, //_consensusRewardsDelta + 0 //_ethToVault ) ).to.be.revertedWith("invalid validatorsDelta"); }); @@ -571,18 +594,35 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( 0, //_validatorsDelta - parseEther("-333") //_consensusRewardsDelta + parseEther("-333"), //_consensusRewardsDelta + 0 //_ethToVault ) ).to.be.revertedWith("invalid consensusRewardsDelta"); await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( 0, //_validatorsDelta - parseEther("333") //_consensusRewardsDelta + parseEther("333"), //_consensusRewardsDelta + 0 //_ethToVault ) ).to.be.revertedWith("invalid consensusRewardsDelta"); }); + it("WETH to Vault amount should not be > 96 for fix accounting function", async () => { + const { nativeStakingSSVStrategy, strategist } = fixture; + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + + await expect( + nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( + 0, //_validatorsDelta + 0, //_consensusRewardsDelta + parseEther("97") //_ethToVault + ) + ).to.be.revertedWith("invalid wethToVaultAmount"); + }); + describe("Should allow strategist to recover paused contract", async () => { for (const validatorsDelta of [-3, -2, -1, 0, 1, 2, 3]) { it(`by changing validators by ${validatorsDelta}`, async () => { @@ -592,12 +632,13 @@ describe("Unit test: Native SSV Staking Strategy", function () { await nativeStakingSSVStrategy.connect(strategist).pause(); await mine(minFixAccountingCadence); + const activeDepositedValidatorsBefore = await nativeStakingSSVStrategy.activeDepositedValidators(); const tx = await nativeStakingSSVStrategy .connect(strategist) - .manuallyFixAccounting(validatorsDelta, 0); + .manuallyFixAccounting(validatorsDelta, 0, 0); expect(tx) .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") @@ -629,7 +670,7 @@ describe("Unit test: Native SSV Staking Strategy", function () { const tx = await nativeStakingSSVStrategy .connect(strategist) - .manuallyFixAccounting(0, consensusRewardsDelta); + .manuallyFixAccounting(0, consensusRewardsDelta, 0); expect(tx) .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") @@ -644,6 +685,99 @@ describe("Unit test: Native SSV Staking Strategy", function () { }); } + for (const eth of [0, 1, 26, 32, 63, 65, 95]) { + it(`by sending ${eth} ETH wrapped to WETH to the vault`, async () => { + const { nativeStakingSSVStrategy, strategist } = fixture; + + const wethToVaultBn = parseEther(`${eth}`); + + // add a bit more ETH so we don't completely empty the contract + await setBalance( + nativeStakingSSVStrategy.address, + wethToVaultBn.add(parseEther("2")) + ); + + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + const ethBefore = await nativeStakingSSVStrategy.provider.getBalance( + nativeStakingSSVStrategy.address + ); + + const tx = await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting(0, 0, wethToVaultBn); + + expect(tx) + .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") + .withArgs(0, 0, wethToVaultBn); + + expect( + await nativeStakingSSVStrategy.provider.getBalance( + nativeStakingSSVStrategy.address + ) + ).to.equal( + ethBefore.sub(wethToVaultBn), + "consensus rewards not updated" + ); + + expect(await nativeStakingSSVStrategy.consensusRewards()).to.equal( + await nativeStakingSSVStrategy.provider.getBalance( + nativeStakingSSVStrategy.address + ), + "consensus rewards matches eth balance" + ); + }); + } + + it("by marking a validator as withdrawn when severely slashed and sent its funds to the vault", async () => { + const { nativeStakingSSVStrategy, governor, strategist, weth } = + fixture; + + // setup initial state + await nativeStakingSSVStrategy.connect(strategist).pause(); + await mine(minFixAccountingCadence); + // setup 1 validator so one can be deducted later in the test + await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting( + 1, //_validatorsDelta + 0, //_consensusRewardsDeltaDelta + 0 //_ethToVault + ); + + // a validator has been slashed and penalized by 8 ETH + await setBalance(nativeStakingSSVStrategy.address, parseEther("24")); + + // run the accounting + const tx = await nativeStakingSSVStrategy + .connect(governor) + .doAccounting(); + // fuse blown contract paused + await expect(tx).to.emit(nativeStakingSSVStrategy, "Paused"); + await mine(minFixAccountingCadence); + + // unit test fixture sets OUSD governor as accounting governor + const tx2 = await nativeStakingSSVStrategy + .connect(strategist) + .manuallyFixAccounting( + -1, //_validatorsDelta + 0, //_consensusRewardsDeltaDelta + parseEther("24") //_ethToVault + ); + + expect(tx2) + .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") + .withArgs( + -1, // validatorsDelta + 0, // consensusRewards + parseEther("24") + ); + + expect(tx2) + .to.emit(nativeStakingSSVStrategy, "Withdrawal") + .withArgs(weth.address, zero, parseEther("24")); + }); + it("by changing all three manuallyFixAccounting delta values", async () => { const { nativeStakingSSVStrategy, strategist, josh, weth } = fixture; @@ -659,14 +793,16 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(strategist) .manuallyFixAccounting( 1, //_validatorsDelta - parseEther("2.3") //_consensusRewardsDeltaDelta + parseEther("2.3"), //_consensusRewardsDeltaDelta + parseEther("2.2") //_ethToVault ); expect(tx) .to.emit(nativeStakingSSVStrategy, "AccountingManuallyFixed") .withArgs( 1, // validatorsDelta - parseEther("2.3") // consensusRewards + parseEther("2.3"), // consensusRewards + parseEther("2.2") ); }); @@ -679,7 +815,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(strategist) .manuallyFixAccounting( 0, //_validatorsDelta - parseEther("0") //_consensusRewardsDelta + 0, //_consensusRewardsDelta + 0 //_ethToVault ); await nativeStakingSSVStrategy.connect(strategist).pause(); @@ -687,7 +824,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { await expect( nativeStakingSSVStrategy.connect(strategist).manuallyFixAccounting( 0, //_validatorsDelta - parseEther("0") //_consensusRewardsDelta + 0, //_consensusRewardsDelta + 0 //_ethToVault ) ).to.be.revertedWith("manuallyFixAccounting called too soon"); }); @@ -701,7 +839,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(strategist) .manuallyFixAccounting( 0, //_validatorsDelta - parseEther("0") //_consensusRewardsDelta + 0, //_consensusRewardsDelta + 0 //_ethToVault ); await nativeStakingSSVStrategy.connect(strategist).pause(); @@ -710,7 +849,8 @@ describe("Unit test: Native SSV Staking Strategy", function () { .connect(strategist) .manuallyFixAccounting( 0, //_validatorsDelta - parseEther("0") //_consensusRewardsDelta + 0, //_consensusRewardsDelta + 0 //_ethToVault ); }); });