Skip to content

Commit

Permalink
Native staking updates (#2023)
Browse files Browse the repository at this point in the history
* Update Natspec

* Generated docs for native eth strategy

* Prettier and linter
Fixed spelling of ValidatorAccountant events
Implemented depositSSV

* Updated Natspec
Moved MAX_STAKE on ValidatorAccountant to a constant

* Removed strategist from strategy as its already maintained in the Vault

* Fix compilation error

* Fix unit tests

* fix linter
  • Loading branch information
naddison36 authored Apr 22, 2024
1 parent 5d5e9cb commit 121c7f7
Show file tree
Hide file tree
Showing 18 changed files with 741 additions and 156 deletions.
2 changes: 1 addition & 1 deletion contracts/contracts/interfaces/IDepositContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ interface IDepositContract {
/// @notice Query the current deposit count.
/// @return The deposit count encoded as a little endian 64-bit number.
function get_deposit_count() external view returns (bytes memory);
}
}
28 changes: 22 additions & 6 deletions contracts/contracts/mocks/BeaconChainDepositContractMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,29 @@ contract BeaconChainDepositContractMock {
) external payable {
// Extended ABI length checks since dynamic types are used.
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");
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");
uint deposit_amount = msg.value / 1 gwei;
require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");
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"
);
require(
deposit_data_root != 0,
"DepositContract: invalid deposit_data_root"
);
}
}
}
27 changes: 15 additions & 12 deletions contracts/contracts/strategies/NativeStaking/FeeAccumulator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { IWETH9 } from "../../interfaces/IWETH9.sol";
/**
* @title Fee Accumulator for Native Staking SSV Strategy
* @notice This contract is setup to receive fees from processing transactions on the beacon chain
* which includes priority fees and any MEV rewards
* which includes priority fees and any MEV rewards.
* It does NOT include swept ETH from consensus rewards or withdrawals.
* @author Origin Protocol Inc
*/
contract FeeAccumulator is Governable {
/// @dev ETH is sent to the collector address
/// @notice The address the WETH is sent to on `collect` which is the Native Staking Strategy
address public immutable COLLECTOR;
/// @notice WETH token address
/// @notice The address of the Wrapped ETH (WETH) token contract
address public immutable WETH_TOKEN_ADDRESS;

error CallerNotCollector(address caller, address expectedCaller);
Expand All @@ -23,30 +24,32 @@ contract FeeAccumulator is Governable {

/**
* @param _collector Address of the contract that collects the fees
* @param _weth Address of the Wrapped ETH (WETH) token contract
*/
constructor(address _collector, address _weth) {
COLLECTOR = _collector;
WETH_TOKEN_ADDRESS = _weth;
}

/*
* @notice Asserts that the caller is the collector
/**
* @dev Asserts that the caller is the collector
*/
function _assertIsCollector() internal view {
if (msg.sender != COLLECTOR) {
revert CallerNotCollector(msg.sender, COLLECTOR);
}
}

/*
* @notice Send all the ETH to the collector
/**
* @notice Converts ETH to WETH and sends the WETH to the collector
* @return weth The amount of WETH sent to the collector
*/
function collect() external returns (uint256 wethReturned) {
function collect() external returns (uint256 weth) {
_assertIsCollector();
wethReturned = address(this).balance;
if (wethReturned > 0) {
IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: wethReturned }();
IWETH9(WETH_TOKEN_ADDRESS).transfer(COLLECTOR, wethReturned);
weth = address(this).balance;
if (weth > 0) {
IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: weth }();
IWETH9(WETH_TOKEN_ADDRESS).transfer(COLLECTOR, weth);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol";
import { ISSVNetwork, Cluster } from "../../interfaces/ISSVNetwork.sol";
import { IWETH9 } from "../../interfaces/IWETH9.sol";
import { FeeAccumulator } from "./FeeAccumulator.sol";
import { ValidatorAccountant } from "./ValidatorAccountant.sol";
Expand Down Expand Up @@ -39,7 +40,7 @@ contract NativeStakingSSVStrategy is

error EmptyRecipient();
error NotWeth();
error InsuffiscientWethBalance(
error InsufficientWethBalance(
uint256 requiredBalance,
uint256 availableBalance
);
Expand All @@ -60,7 +61,12 @@ contract NativeStakingSSVStrategy is
address _beaconChainDepositContract
)
InitializableAbstractStrategy(_baseConfig)
ValidatorAccountant(_wethAddress, _baseConfig.vaultAddress, _beaconChainDepositContract, _ssvNetwork)
ValidatorAccountant(
_wethAddress,
_baseConfig.vaultAddress,
_beaconChainDepositContract,
_ssvNetwork
)
{
SSV_TOKEN_ADDRESS = _ssvToken;
FEE_ACCUMULATOR_ADDRESS = _feeAccumulator;
Expand Down Expand Up @@ -98,7 +104,8 @@ contract NativeStakingSSVStrategy is
beaconChainRewardWETH;
}

/// @notice Collect accumulated WETH & SSV tokens and send to the Harvester.
/// @notice Convert accumulated ETH to WETH and send to the Harvester.
/// Only callable by the Harvester.
function collectRewardTokens()
external
virtual
Expand Down Expand Up @@ -129,7 +136,7 @@ contract NativeStakingSSVStrategy is
if (balance > 0) {
if (address(rewardToken) == WETH_TOKEN_ADDRESS) {
if (beaconChainRewardWETH > balance) {
revert InsuffiscientWethBalance(
revert InsufficientWethBalance(
beaconChainRewardWETH,
balance
);
Expand Down Expand Up @@ -232,9 +239,9 @@ contract NativeStakingSSVStrategy is
function _abstractSetPToken(address _asset, address) internal override {}

/// @notice Returns the total value of (W)ETH that is staked to the validators
/// and also present on the native staking and fee accumulator contracts
/// and also present on the native staking and fee accumulator contracts.
/// @param _asset Address of weth asset
/// @return balance Total value of (W)ETH
/// @return balance Total value of (W)ETH
function checkBalance(address _asset)
external
view
Expand All @@ -254,14 +261,13 @@ contract NativeStakingSSVStrategy is
_pause();
}

/// @dev Retuns bool indicating whether asset is supported by strategy
/// @param _asset Address of the asset
/// @notice Returns bool indicating whether asset is supported by strategy.
/// @param _asset The address of the asset token.
function supportsAsset(address _asset) public view override returns (bool) {
return _asset == WETH_TOKEN_ADDRESS;
}

/// @notice Approve the spending of all assets
/// @dev Approves the SSV Network contract to transfer SSV tokens for deposits
/// @notice Approves the SSV Network contract to transfer SSV tokens for deposits
function safeApproveAllTokens() external override {
/// @dev Approves the SSV Network contract to transfer SSV tokens for deposits
IERC20(SSV_TOKEN_ADDRESS).approve(
Expand All @@ -271,17 +277,20 @@ 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 fron-running can't DOS our maintenance service
/// that tries to top us SSV tokens.
/// @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
)
onlyStrategist
external {
// address SSV_NETWORK_ADDRESS = lrtConfig.getContract(LRTConstants.SSV_NETWORK);
// ISSVNetwork(SSV_NETWORK_ADDRESS).deposit(address(this), operatorIds, amount, cluster);
) external onlyStrategist {
ISSVNetwork(SSV_NETWORK_ADDRESS).deposit(
address(this),
operatorIds,
amount,
cluster
);
}
}
21 changes: 21 additions & 0 deletions contracts/contracts/strategies/NativeStaking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Diagrams

## Native Staking SSV Strategy

### Hierarchy

![Native Staking SSV Strategy Hierarchy](../../../docs/NativeStakingSSVStrategyHierarchy.svg)

### Squashed

![Native Staking SSV Strategy Squashed](../../../docs/NativeStakingSSVStrategySquashed.svg)

### Storage

![Native Staking SSV Strategy Storage](../../../docs/NativeStakingSSVStrategyStorage.svg)

## Fee Accumulator

### Squashed

![Fee Accumulator Squashed](../../../docs/FeeAccumulatorSquashed.svg)
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ 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 Accountant of the rewards Beacon Chain ETH
/// @notice This contract contains the logic to attribute the Beacon Chain swept ETH either to full
/// or partial withdrawals
/// @author Origin Protocol Inc
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;

/// @dev The WETH present on this contract will come from 2 sources:
Expand All @@ -23,14 +28,12 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
/// present as a result of a deposit.
uint256 public beaconChainRewardWETH = 0;

/// @dev start of fuse interval
/// @notice start of fuse interval
uint256 public fuseIntervalStart = 0;
/// @dev end of fuse interval
/// @notice end of fuse interval
uint256 public fuseIntervalEnd = 0;
/// @dev Governor that can manually correct the accounting
/// @notice Governor that can manually correct the accounting
address public accountingGovernor;
/// @dev Strategist that can pause the accounting
address public strategist;

uint256[50] private __gap;

Expand All @@ -40,12 +43,12 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
uint256 start,
uint256 end
);
event AccuntingFullyWithdrawnValidator(
event AccountingFullyWithdrawnValidator(
uint256 noOfValidators,
uint256 remainingValidators,
uint256 wethSentToVault
);
event AccuntingValidatorSlashed(
event AccountingValidatorSlashed(
uint256 remainingValidators,
uint256 wethSentToVault
);
Expand All @@ -54,10 +57,6 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
address newAddress
);
event AccountingBeaconChainRewards(uint256 amount);
event StrategistAddressChanged(
address oldStrategist,
address newStrategist
);

event AccountingManuallyFixed(
uint256 oldActiveDepositedValidators,
Expand Down Expand Up @@ -85,16 +84,29 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {

/// @dev Throws if called by any account other than the Strategist
modifier onlyStrategist() {
require(msg.sender == strategist, "Caller is not the Strategist");
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)
ValidatorRegistrator(_wethAddress, _beaconChainDepositContract, _ssvNetwork) {
constructor(
address _wethAddress,
address _vaultAddress,
address _beaconChainDepositContract,
address _ssvNetwork
)
ValidatorRegistrator(
_wethAddress,
_beaconChainDepositContract,
_ssvNetwork
)
{
VAULT_ADDRESS = _vaultAddress;
}

Expand All @@ -103,11 +115,6 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
accountingGovernor = _address;
}

function setStrategist(address _address) external onlyGovernor {
emit StrategistAddressChanged(strategist, _address);
strategist = _address;
}

/// @notice set fuse interval values
function setFuseInterval(
uint256 _fuseIntervalStart,
Expand All @@ -133,19 +140,24 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
fuseIntervalEnd = _fuseIntervalEnd;
}

/* 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
/// 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.
/// @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
/// 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
/// for now
function doAccounting() external onlyRegistrator returns (bool accountingValid) {
/// for now.
/* solhint-enable max-line-length */
function doAccounting()
external
onlyRegistrator
returns (bool accountingValid)
{
uint256 ethBalance = address(this).balance;
uint256 MAX_STAKE = 32 ether;
accountingValid = true;

// send the WETH that is from fully withdrawn validators to the Vault
Expand All @@ -157,7 +169,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: wethToVault }();
IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, wethToVault);

emit AccuntingFullyWithdrawnValidator(
emit AccountingFullyWithdrawnValidator(
fullyWithdrawnValidators,
activeDepositedValidators,
wethToVault
Expand All @@ -173,6 +185,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
// Beacon chain rewards swept (partial validator withdrawals)
if (ethRemaining <= fuseIntervalStart) {
IWETH9(WETH_TOKEN_ADDRESS).deposit{ value: ethRemaining }();
// solhint-disable-next-line reentrancy
beaconChainRewardWETH += ethRemaining;
emit AccountingBeaconChainRewards(ethRemaining);
}
Expand All @@ -182,7 +195,7 @@ abstract contract ValidatorAccountant is ValidatorRegistrator {
IWETH9(WETH_TOKEN_ADDRESS).transfer(VAULT_ADDRESS, ethRemaining);
activeDepositedValidators -= 1;

emit AccuntingValidatorSlashed(
emit AccountingValidatorSlashed(
activeDepositedValidators,
ethRemaining
);
Expand Down
Loading

0 comments on commit 121c7f7

Please sign in to comment.