diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index 632070bfb3..3ee62da8af 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -52,16 +52,19 @@ contract CompoundingStakingSSVStrategy is /// @param _rewardTokenAddresses Not used so empty array /// @param _assets Not used so empty array /// @param _pTokens Not used so empty array + /// @param _initialDepositAmountWei The amount of ETH required for the first deposit to a new validator. function initialize( address[] memory _rewardTokenAddresses, address[] memory _assets, - address[] memory _pTokens + address[] memory _pTokens, + uint256 _initialDepositAmountWei ) external onlyGovernor initializer { InitializableAbstractStrategy._initialize( _rewardTokenAddresses, _assets, _pTokens ); + _setInitialDepositAmountWei(_initialDepositAmountWei); } /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol b/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol index 639fcd89cc..3ed478d156 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol @@ -23,8 +23,6 @@ import { IBeaconProofs } from "../../interfaces/IBeaconProofs.sol"; abstract contract CompoundingValidatorManager is Governable, Pausable { using SafeERC20 for IERC20; - /// @dev The amount of ETH in wei that is required for a deposit to a new validator. - uint256 internal constant DEPOSIT_AMOUNT_WEI = 1 ether; /// @dev Validator balances over this amount will eventually become active on the beacon chain. /// Due to hysteresis, if the effective balance is 31 ETH, the actual balance /// must rise to 32.25 ETH to trigger an effective balance update to 32 ETH. @@ -124,7 +122,7 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { REGISTERED, // validator is registered on the SSV network STAKED, // validator has funds staked VERIFIED, // validator has been verified to exist on the beacon chain - ACTIVE, // The validator balance is at least 32 ETH. The validator may not yet be active on the beacon chain. + ACTIVE, // The validator balance is at least 32.25 ETH. The validator may not yet be active on the beacon chain. EXITING, // The validator has been requested to exit EXITED, // The validator has been verified to have a zero balance REMOVED, // validator has funds withdrawn to this strategy contract and is removed from the SSV @@ -168,11 +166,14 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { /// withdrawal of the validators. It is strictly concerned with WETH that has been deposited and is waiting to /// be staked. uint256 public depositedWethAccountedFor; + /// @notice The amount of ETH in wei that is required for the first deposit to a new validator. + uint256 public initialDepositAmountWei; // For future use - uint256[41] private __gap; + uint256[40] private __gap; event RegistratorChanged(address indexed newAddress); + event InitialDepositAmountChanged(uint256 amountWei); event FirstDepositReset(); event SSVValidatorRegistered( bytes32 indexed pubKeyHash, @@ -262,6 +263,14 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { emit RegistratorChanged(_address); } + /// @notice Set the amount of ETH required for the first deposit to a new validator. + function setInitialDepositAmount(uint256 _initialDepositAmountWei) + external + onlyGovernor + { + _setInitialDepositAmountWei(_initialDepositAmountWei); + } + /// @notice Reset the `firstDeposit` flag to false so deposits to unverified validators can be made again. function resetFirstDeposit() external onlyGovernor { require(firstDeposit, "No first deposit"); @@ -328,9 +337,9 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { } /// @notice Stakes WETH in this strategy to a compounding validator. - /// The first deposit to a new validator, the amount must be 1 ETH. - /// Another deposit of at least 31 ETH is required for the validator to be activated. - /// This second deposit has to be done after the validator has been verified. + /// The first deposit to a new validator must be exactly `initialDepositAmountWei`. + /// Once verified on the beacon chain, rewards can push the validator's balance above + /// the activation threshold so it can become active without requiring a second deposit. /// Does not convert any ETH sitting in this strategy to WETH. /// There can not be two deposits to the same validator in the same block for the same amount. /// Function is pausable so in case a run-away Registrator can be prevented from continuing @@ -378,7 +387,7 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { require(!firstDeposit, "Existing first deposit"); // Limits the amount of ETH that can be at risk from a front-running deposit attack. require( - depositAmountWei == DEPOSIT_AMOUNT_WEI, + depositAmountWei == initialDepositAmountWei, "Invalid first deposit amount" ); // Limits the number of validator balance proofs to verifyBalances @@ -1202,6 +1211,19 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { } } + function _setInitialDepositAmountWei(uint256 _initialDepositAmountWei) + internal + { + require(_initialDepositAmountWei >= 1 ether, "Deposit too small"); + require( + _initialDepositAmountWei <= MIN_ACTIVATION_BALANCE_GWEI * 1e9, + "Deposit too large" + ); + + initialDepositAmountWei = _initialDepositAmountWei; + emit InitialDepositAmountChanged(_initialDepositAmountWei); + } + /// @notice Hash a validator public key using the Beacon Chain's format function _hashPubKey(bytes memory pubKey) internal pure returns (bytes32) { require(pubKey.length == 48, "Invalid public key"); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 87bb6950ac..b4c5e52c50 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -458,7 +458,7 @@ const deployCompoundingStakingSSVStrategy = async () => { log("Deploy encode initialize function of the strategy contract"); const initData = cStrategyImpl.interface.encodeFunctionData( - "initialize(address[],address[],address[])", + "initialize(address[],address[],address[],uint256)", [ [], // reward token addresses /* no need to specify WETH as an asset, since we have that overridden in the "supportsAsset" @@ -466,6 +466,7 @@ const deployCompoundingStakingSSVStrategy = async () => { */ [], // asset token addresses [], // platform tokens addresses + ethers.utils.parseEther("1"), // initial validator deposit amount ] ); diff --git a/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js b/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js new file mode 100644 index 0000000000..86d9a60bd8 --- /dev/null +++ b/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js @@ -0,0 +1,52 @@ +const addresses = require("../../utils/addresses"); +const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "196_upgrade_compounding_staking_initial_deposit", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + }, + async ({ deployWithConfirmation, ethers }) => { + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cCompoundingStakingStrategyProxy = await ethers.getContract( + "CompoundingStakingSSVStrategyProxy" + ); + const cCompoundingStakingSSVStrategy = await ethers.getContractAt( + "CompoundingStakingSSVStrategy", + cCompoundingStakingStrategyProxy.address + ); + const cBeaconProofs = await ethers.getContract("BeaconProofs"); + + console.log("Deploy CompoundingStakingSSVStrategy"); + const dCompoundingStakingStrategy = await deployWithConfirmation( + "CompoundingStakingSSVStrategy", + [ + [addresses.zero, cOETHVaultProxy.address], //_baseConfig + addresses.mainnet.WETH, // wethAddress + addresses.mainnet.SSVNetwork, // ssvNetwork + addresses.mainnet.beaconChainDepositContract, // beaconChainDepositContract + cBeaconProofs.address, // beaconProofs + beaconChainGenesisTimeMainnet, + ] + ); + + return { + name: "Upgrade the compounding staking strategy initial deposit to 32.25 ETH", + actions: [ + { + contract: cCompoundingStakingStrategyProxy, + signature: "upgradeTo(address)", + args: [dCompoundingStakingStrategy.address], + }, + { + contract: cCompoundingStakingSSVStrategy, + signature: "setInitialDepositAmount(uint256)", + args: [ethers.utils.parseEther("32.25")], + }, + ], + }; + } +); diff --git a/contracts/docs/plantuml/oethProcesses.png b/contracts/docs/plantuml/oethProcesses.png index 882c88080b..f2d05257c0 100644 Binary files a/contracts/docs/plantuml/oethProcesses.png and b/contracts/docs/plantuml/oethProcesses.png differ diff --git a/contracts/docs/plantuml/oethProcesses.puml b/contracts/docs/plantuml/oethProcesses.puml index ab6f8f134f..067795d95c 100644 --- a/contracts/docs/plantuml/oethProcesses.puml +++ b/contracts/docs/plantuml/oethProcesses.puml @@ -98,7 +98,7 @@ return ETH compStrat -> dep : deposit(\npubkey,\nwithdrawal_credentials,\nsignature,\ndeposit root) activate dep note left -32 ETH from Staking Strategy is sent to Beacon Deposit. +1 ETH from Staking Strategy is sent to Beacon Deposit. Withdrawal credential is the Staking Strategy. end note return diff --git a/contracts/tasks/validatorCompound.js b/contracts/tasks/validatorCompound.js index 70cad05061..7af8b4f2aa 100644 --- a/contracts/tasks/validatorCompound.js +++ b/contracts/tasks/validatorCompound.js @@ -172,10 +172,16 @@ async function stakeValidator({ withdrawalCredentials = calcWithdrawalCredential("0x02", strategy.address); } - if (amount == 1) { + const amountWei = parseUnits(amount.toString(), 18); + const initialDepositAmountWei = await strategy.initialDepositAmountWei(); + + if (amountWei.eq(initialDepositAmountWei)) { if (!sig) { throw new Error( - "The signature is required for the first deposit of 1 ETH" + `The signature is required for the first deposit of ${formatUnits( + initialDepositAmountWei, + 18 + )} ETH` ); } await verifyDepositSignatureAndMessageRoot({ diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index 4b1ae961ac..cd5a94eb4e 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -87,6 +87,8 @@ const getWithdrawalCredentials = (type, address) => { const ETHInGwei = BigNumber.from("1000000000"); // 1 ETH in Gwei const GweiInWei = BigNumber.from("1000000000"); // 1 Gwei in Wei +const INITIAL_DEPOSIT_AMOUNT = "1"; +const INITIAL_DEPOSIT_AMOUNT_GWEI = parseUnits(INITIAL_DEPOSIT_AMOUNT, 9); describe("Unit test: Compounding SSV Staking Strategy", function () { this.timeout(0); @@ -173,6 +175,55 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ); expect(assets).to.equal(true); }); + it("Should initialize the first deposit amount to 1 ETH", async () => { + const { compoundingStakingSSVStrategy } = fixture; + + expect( + await compoundingStakingSSVStrategy.initialDepositAmountWei() + ).to.equal(parseEther(INITIAL_DEPOSIT_AMOUNT)); + }); + it("Governor should be able to change the first deposit amount", async () => { + const { compoundingStakingSSVStrategy } = fixture; + + const updatedAmount = parseEther("32.25"); + const tx = await compoundingStakingSSVStrategy + .connect(sGov) + .setInitialDepositAmount(updatedAmount); + + await expect(tx) + .to.emit(compoundingStakingSSVStrategy, "InitialDepositAmountChanged") + .withArgs(updatedAmount); + expect( + await compoundingStakingSSVStrategy.initialDepositAmountWei() + ).to.equal(updatedAmount); + }); + it("Non governor should not be able to change the first deposit amount", async () => { + const { compoundingStakingSSVStrategy, strategist } = fixture; + + await expect( + compoundingStakingSSVStrategy + .connect(strategist) + .setInitialDepositAmount(parseEther("33")) + ).to.be.revertedWith("Caller is not the Governor"); + }); + it("Should revert when setting the first deposit amount below 1 ETH", async () => { + const { compoundingStakingSSVStrategy } = fixture; + + await expect( + compoundingStakingSSVStrategy + .connect(sGov) + .setInitialDepositAmount(parseUnits("0.5", 18)) + ).to.be.revertedWith("Deposit too small"); + }); + it("Should revert when setting the first deposit amount above 32.25 ETH", async () => { + const { compoundingStakingSSVStrategy } = fixture; + + await expect( + compoundingStakingSSVStrategy + .connect(sGov) + .setInitialDepositAmount(parseEther("32.25").add(1)) + ).to.be.revertedWith("Deposit too large"); + }); it("Should not collect rewards", async () => { const { compoundingStakingSSVStrategy, governor } = fixture; @@ -246,7 +297,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { const { beaconRoots, compoundingStakingSSVStrategy, validatorRegistrator } = fixture; - const depositAmount = 1; + const depositAmount = INITIAL_DEPOSIT_AMOUNT; // Register a new validator with the SSV Network const regTx = await compoundingStakingSSVStrategy @@ -272,7 +323,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { depositAmount ); - const depositGwei = BigNumber.from(depositAmount).mul(ETHInGwei); // Convert ETH to Gwei + const depositGwei = parseUnits(depositAmount.toString(), 9); const stakeTx = await compoundingStakingSSVStrategy .connect(validatorRegistrator) @@ -687,10 +738,13 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { .transfer(compoundingStakingSSVStrategy.address, ethUnits("5000")); }); - const stakeValidators = async (testValidatorIndex, amount = 1) => { + const stakeValidators = async ( + testValidatorIndex, + amount = INITIAL_DEPOSIT_AMOUNT + ) => { const { compoundingStakingSSVStrategy, validatorRegistrator } = fixture; - const amountGwei = BigNumber.from(amount.toString()).mul(ETHInGwei); + const amountGwei = parseUnits(amount.toString(), 9); // there is a limitation to this function as it will only check for // a failure transaction with the last stake call @@ -768,11 +822,11 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ).to.equal(2, "Validator state not 2 (STAKED)"); }; - it("Should stake to a validator: 1 ETH", async () => { - await stakeValidators(0, 1); + it("Should stake the initial deposit amount to a validator", async () => { + await stakeValidators(0, INITIAL_DEPOSIT_AMOUNT); }); - it("Should stake 1 ETH then 2047 ETH to a validator", async () => { + it("Should stake the initial deposit amount then 2047 ETH to a validator", async () => { const { compoundingStakingSSVStrategy, validatorRegistrator, @@ -801,10 +855,10 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { "0x02", testValidator.publicKey, testValidator.signature, - 1 + INITIAL_DEPOSIT_AMOUNT ); - // Stake 1 ETH to the new validator + // Stake the initial deposit amount to the new validator let stakeTx = await compoundingStakingSSVStrategy .connect(validatorRegistrator) .stakeEth( @@ -813,7 +867,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { signature: testValidator.signature, depositDataRoot, }, - ETHInGwei.mul(1) // 1 ETH + INITIAL_DEPOSIT_AMOUNT_GWEI ); const { pendingDepositRoot, depositSlot } = await getLastDeposit( compoundingStakingSSVStrategy @@ -909,7 +963,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ).to.equal(stratBalanceBefore); }); - it("Should revert when first stake amount is not exactly 1 ETH", async () => { + it("Should revert when first stake amount is not exactly the initial deposit amount", async () => { const { compoundingStakingSSVStrategy, validatorRegistrator } = fixture; const testValidator = testValidators[0]; @@ -925,7 +979,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { { value: ethUnits("2") } ); - // Try to stake 2 ETH to the new validator + // Try to stake 32 ETH to the new validator const stakeTx = compoundingStakingSSVStrategy .connect(validatorRegistrator) .stakeEth( @@ -934,7 +988,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { signature: testValidator.signature, depositDataRoot: testValidator.depositProof.depositDataRoot, }, - BigNumber.from("2").mul(GweiInWei) + parseUnits("32", 9) ); await expect(stakeTx).to.be.revertedWith("Invalid first deposit amount"); @@ -1178,7 +1232,6 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { // Third validator is later withdrawn later await processValidator(testValidators[3], "VERIFIED_DEPOSIT"); - await topUpValidator(testValidators[3], 32, "VERIFIED_DEPOSIT"); // verifyBalances has not been called so the validator is still VERIFIED even though the // validator has more then 32.25 ETH staked @@ -1538,7 +1591,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { }); it("Should revert when removing a validator that has been found", async () => { - await stakeValidators(0, 1); + await stakeValidators(0, INITIAL_DEPOSIT_AMOUNT); const testValidator = testValidators[0]; @@ -1556,7 +1609,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { it("Should fail removing a strategy with funds", async () => { const { compoundingStakingSSVStrategy, oethVault, governor } = fixture; - await stakeValidators(0, 1); + await stakeValidators(0, INITIAL_DEPOSIT_AMOUNT); if ( (await oethVault.defaultStrategy()) === compoundingStakingSSVStrategy.address @@ -1676,7 +1729,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { await expect(tx) .to.emit(compoundingStakingSSVStrategy, "DepositVerified") - .withArgs(pendingDepositRoot, parseEther("1")); + .withArgs(pendingDepositRoot, parseEther(INITIAL_DEPOSIT_AMOUNT)); }); it("Should verify deposit with processed slot 1 before the snapped balances slot", async () => { const { @@ -1710,7 +1763,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { await expect(tx) .to.emit(compoundingStakingSSVStrategy, "DepositVerified") - .withArgs(pendingDepositRoot, parseEther("1")); + .withArgs(pendingDepositRoot, parseEther(INITIAL_DEPOSIT_AMOUNT)); }); it("Should verify deposit with processed slot well before the snapped balances slot", async () => { const { @@ -1741,7 +1794,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { await expect(tx) .to.emit(compoundingStakingSSVStrategy, "DepositVerified") - .withArgs(pendingDepositRoot, parseEther("1")); + .withArgs(pendingDepositRoot, parseEther(INITIAL_DEPOSIT_AMOUNT)); }); }); @@ -2223,14 +2276,14 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { await processValidator(testValidators[0], "STAKED"); const balancesAfter = await assertBalances({ - pendingDepositAmount: 1, + pendingDepositAmount: Number(INITIAL_DEPOSIT_AMOUNT), wethAmount: 0, ethAmount: 0, balancesProof: testBalancesProofs[2], activeValidators: [], // no active validators }); - const depositAmountWei = parseEther("1"); + const depositAmountWei = parseEther(INITIAL_DEPOSIT_AMOUNT); expect(balancesAfter.totalDepositsWei).to.equal(depositAmountWei); expect(balancesAfter.verifiedEthBalance).to.equal(depositAmountWei); expect(balancesAfter.stratBalance).to.equal(depositAmountWei); @@ -2244,7 +2297,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ); await assertBalances({ - pendingDepositAmount: 1, + pendingDepositAmount: Number(INITIAL_DEPOSIT_AMOUNT), wethAmount: 0, ethAmount: 0, balancesProof: testBalancesProofs[5], @@ -2872,10 +2925,12 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ); expect(depositData.status).to.equal(2); // VERIFIED - // The last verified ETH balance is reduced by the 1 ETH deposit + // The last verified ETH balance is reduced by the initial deposit amount expect( await compoundingStakingSSVStrategy.lastVerifiedEthBalance() - ).to.equal(lastVerifiedEthBalanceBefore.sub(parseEther("1"))); + ).to.equal( + lastVerifiedEthBalanceBefore.sub(parseEther(INITIAL_DEPOSIT_AMOUNT)) + ); // The first deposit flag is still set expect(await compoundingStakingSSVStrategy.firstDeposit()).to.equal(true); @@ -3050,7 +3105,6 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { const testValidator = testValidators[3]; await processValidator(testValidator, "VERIFIED_DEPOSIT"); - await topUpValidator(testValidator, 31, "VERIFIED_DEPOSIT"); const validatorBefore = await compoundingStakingSSVStrategy.validator( testValidator.publicKeyHash @@ -3061,7 +3115,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { .connect(validatorRegistrator) .snapBalances(); - // Set validator balance to 32.25 Gwei + // Set validator balance to 32.25 ETH in gwei await mockBeaconProof.setValidatorBalance( testValidator.index, parseUnits("32.25", 9) @@ -3095,7 +3149,6 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { const testValidator = testValidators[3]; await processValidator(testValidator, "VERIFIED_DEPOSIT"); - await topUpValidator(testValidator, 31, "VERIFIED_DEPOSIT"); const validatorBefore = await compoundingStakingSSVStrategy.validator( testValidator.publicKeyHash