Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stake funds with confirmations for front-running protection of Beacon Deposits #2074

Merged
merged 10 commits into from
May 28, 2024
53 changes: 0 additions & 53 deletions contracts/contracts/mocks/BeaconChainDepositContractMock.sol

This file was deleted.

80 changes: 80 additions & 0 deletions contracts/contracts/mocks/MockDepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { IDepositContract } from "./../interfaces/IDepositContract.sol";

contract MockDepositContract is IDepositContract {
uint256 deposit_count;

function deposit(
bytes calldata pubkey,
bytes calldata withdrawal_credentials,
bytes calldata signature,
bytes32 deposit_data_root
) external payable override {
require(pubkey.length == 48, "DepositContract: invalid pubkey length");
require(
withdrawal_credentials.length == 32,
"DepositContract: invalid withdrawal_credentials length"
);
require(
signature.length == 96,
"DepositContract: invalid signature length"
);

// Check deposit amount
require(msg.value >= 1 ether, "DepositContract: deposit value too low");
require(
msg.value % 1 gwei == 0,
"DepositContract: deposit value not multiple of gwei"
);
uint256 deposit_amount = msg.value / 1 gwei;
require(
deposit_amount <= type(uint64).max,
"DepositContract: deposit value too high"
);

// Emit `DepositEvent` log
bytes memory amount = to_little_endian_64(uint64(deposit_amount));
emit DepositEvent(
pubkey,
withdrawal_credentials,
amount,
signature,
to_little_endian_64(uint64(deposit_count))
);
require(
deposit_data_root != 0,
"DepositContract: invalid deposit_data_root"
);
}

function get_deposit_root() external view override returns (bytes32) {
// just return some bytes32
return sha256(abi.encodePacked(deposit_count, bytes16(0)));
}

/// @notice Query the current deposit count.
/// @return The deposit count encoded as a little endian 64-bit number.
function get_deposit_count() external view override returns (bytes memory) {
return to_little_endian_64(uint64(deposit_count));
}

function to_little_endian_64(uint64 value)
internal
pure
returns (bytes memory ret)
{
ret = new bytes(8);
bytes8 bytesValue = bytes8(value);
// Byteswapping during copying to bytes.
ret[0] = bytesValue[7];
ret[1] = bytesValue[6];
ret[2] = bytesValue[5];
ret[3] = bytesValue[4];
ret[4] = bytesValue[3];
ret[5] = bytesValue[2];
ret[6] = bytesValue[1];
ret[7] = bytesValue[0];
}
}
28 changes: 27 additions & 1 deletion contracts/contracts/mocks/MockSSVNetwork.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { Cluster } from "./../interfaces/ISSVNetwork.sol";

contract MockSSVNetwork {
constructor() {}
function registerValidator(
bytes calldata publicKey,
uint64[] calldata operatorIds,
bytes calldata sharesData,
uint256 amount,
Cluster memory cluster
) external {}

function exitValidator(
bytes calldata publicKey,
uint64[] calldata operatorIds
) external {}

function removeValidator(
bytes calldata publicKey,
uint64[] calldata operatorIds,
Cluster memory cluster
) external {}

function deposit(
address clusterOwner,
uint64[] calldata operatorIds,
uint256 amount,
Cluster memory cluster
) external {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,15 @@ abstract contract ValidatorRegistrator is Governable, Pausable {
uint256 public activeDepositedValidators;
/// @notice State of the validators keccak256(pubKey) => state
mapping(bytes32 => VALIDATOR_STATE) public validatorsStates;

/// @notice The account that is allowed to modify stakeETHThreshold and reset stakeETHTally
address public stakingMonitor;
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
/// @notice Amount of ETH that can be staked before staking on the contract is suspended
/// and the governor needs to approve further staking
uint256 public stakeETHThreshold;
/// @notice Amount of ETH that can has been staked since the last governor approval.
uint256 public stakeETHTally;
// For future use
uint256[50] private __gap;
uint256[47] private __gap;

enum VALIDATOR_STATE {
REGISTERED, // validator is registered on the SSV network
Expand All @@ -49,10 +55,13 @@ abstract contract ValidatorRegistrator is Governable, Pausable {
}

event RegistratorChanged(address newAddress);
event StakingMonitorChanged(address newAddress);
event ETHStaked(bytes pubkey, uint256 amount, bytes withdrawal_credentials);
event SSVValidatorRegistered(bytes pubkey, uint64[] operatorIds);
event SSVValidatorExitInitiated(bytes pubkey, uint64[] operatorIds);
event SSVValidatorExitCompleted(bytes pubkey, uint64[] operatorIds);
event StakeETHThresholdChanged(uint256 amount);
event StakeETHTallyReset();

/// @dev Throws if called by any account other than the Registrator
modifier onlyRegistrator() {
Expand All @@ -63,6 +72,12 @@ abstract contract ValidatorRegistrator is Governable, Pausable {
_;
}

/// @dev Throws if called by any account other than the Staking monitor
modifier onlyStakingMonitor() {
require(msg.sender == stakingMonitor, "Caller is not the Monitor");
_;
}

/// @dev Throws if called by any account other than the Strategist
modifier onlyStrategist() {
require(
Expand Down Expand Up @@ -94,6 +109,26 @@ abstract contract ValidatorRegistrator is Governable, Pausable {
validatorRegistrator = _address;
}

/// @notice Set the address of the staking monitor that is allowed to modify
/// stakeETHThreshold and reset stakeETHTally
function setStakingMonitor(address _address) external onlyGovernor {
emit StakingMonitorChanged(_address);
stakingMonitor = _address;
}

/// @notice Set the amount of ETH that can be staked before governor needs to a approve
/// further staking.
function setStakeETHThreshold(uint256 _amount) external onlyStakingMonitor {
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
emit StakeETHThresholdChanged(_amount);
stakeETHThreshold = _amount;
}

/// @notice Reset the stakeETHTally
function resetStakeETHTally() external onlyStakingMonitor {
emit StakeETHTallyReset();
stakeETHTally = 0;
}

/// @notice Stakes WETH to the node validators
/// @param validators A list of validator data needed to stake.
/// The `ValidatorStakeData` struct contains the pubkey, signature and depositDataRoot.
Expand All @@ -112,6 +147,12 @@ abstract contract ValidatorRegistrator is Governable, Pausable {
"insufficient WETH"
);

require(
stakeETHTally + requiredETH <= stakeETHThreshold,
"Staking ETH over approved threshold"
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
);
stakeETHTally += requiredETH;

// Convert required ETH from WETH
IWETH9(WETH_TOKEN_ADDRESS).withdraw(requiredETH);
_wethWithdrawnAndStaked(requiredETH);
Expand Down
9 changes: 5 additions & 4 deletions contracts/deploy/mainnet/000_mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => {
from: deployerAddr,
});

// Mock SSV Network
await deploy("MockDepositContract", {
from: deployerAddr,
});

const dai = await ethers.getContract("MockDAI");
const usdc = await ethers.getContract("MockUSDC");
const usdt = await ethers.getContract("MockUSDT");
Expand Down Expand Up @@ -425,10 +430,6 @@ const deployMocks = async ({ getNamedAccounts, deployments }) => {
from: deployerAddr,
});

await deploy("BeaconChainDepositContractMock", {
from: deployerAddr,
});

console.log("000_mock deploy done.");

return true;
Expand Down
15 changes: 14 additions & 1 deletion contracts/deploy/mainnet/097_native_ssv_staking.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,20 @@ module.exports = deploymentWithGovernanceProposal(
signature: "setRegistrator(address)",
args: [addresses.mainnet.validatorRegistrator],
},
// 6. Upgrade the OETH Harvester
// 6. set staking threshold
{
contract: cStrategy,
signature: "setStakeETHThreshold(uint256)",
// TODO: confirm this number makes sense
args: [ethers.utils.parseEther("32")], // 32ETH * 32
naddison36 marked this conversation as resolved.
Show resolved Hide resolved
},
// 7. set staking monitor
{
contract: cStrategy,
signature: "setStakingMonitor(address)",
args: [addresses.mainnet.Guardian], // 32ETH * 32
},
// 8. Upgrade the OETH Harvester
{
contract: cOETHHarvesterProxy,
signature: "upgradeTo(address)",
Expand Down
3 changes: 3 additions & 0 deletions contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ async function nativeStakingSSVStrategyFixture() {
addresses.mainnet.SSVNetwork
);
} else {
fixture.ssvNetwork = await ethers.getContract("MockSSVNetwork");
const { governorAddr } = await getNamedAccounts();
const { oethVault, weth, nativeStakingSSVStrategy } = fixture;
const sGovernor = await ethers.provider.getSigner(governorAddr);
Expand All @@ -1641,6 +1642,8 @@ async function nativeStakingSSVStrategyFixture() {
await nativeStakingSSVStrategy
.connect(sGovernor)
.setRegistrator(governorAddr);

fixture.validatorRegistrator = sGovernor;
}

return fixture;
Expand Down
4 changes: 4 additions & 0 deletions contracts/test/behaviour/ssvStrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const shouldBehaveLikeAnSsvStrategy = (context) => {
addresses.validatorRegistrator,
"Incorrect validator registrator"
);
await expect(await nativeStakingSSVStrategy.stakingMonitor()).to.equal(
addresses.Guardian,
"Incorrect validator registrator"
);
});
});

Expand Down
5 changes: 2 additions & 3 deletions contracts/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,8 @@ const getAssetAddresses = async (deployments) => {
BAL: (await deployments.get("MockBAL")).address,
SSV: (await deployments.get("MockSSV")).address,
SSVNetwork: (await deployments.get("MockSSVNetwork")).address,
beaconChainDepositContract: (
await deployments.get("BeaconChainDepositContractMock")
).address,
beaconChainDepositContract: (await deployments.get("MockDepositContract"))
.address,
};

try {
Expand Down
Loading
Loading