The Staking Contracts allows the registered node operators to deposit validator keys that are funded by external users. This allows a seemless staking experience for end users, enabling them to stake one or several validators with one transaction.
Install all required Node dependencies by running yarn
Foundry is used to manage the contracts source code and tests. Install it by following the instructions and make sure that forge
and cast
are available commands.
All sources can be built by running forge build
You can run all the test suite by running forge test
. Increase verbosity with forge test -vvv
to retrieve complete error stack traces.
To run only one test file, you can run forge test -vvv --match-contract StakingContractTest
which is the name of the contract you can find in the StakingContract.t.sol
test file.
You will find all the steps to properly deploy and populate the system. You will find several VARIABLES
that you will need to make sure are properly setup according to the instructions. They will be referenced by using the $VARIABLE
syntax in the required commands.
The system administrator address will be in charge of all the admin operations in the all the system, like changing the administrator or the operator address.
If you want to share the administration between several recipients, you can deploy a Gnosis Safe contract and configure its threshold as you please. The easiest way to deploy a Gnosis Safe is to use the official Gnosis Safe App and follow the instructions to setup the initial members and threshold.
The system is composed of several upgradeable contracts. These contracts work by using a proxy that is following the transparent proxy pattern. This simply means that the administrator of the proxies cannot call functions on the system like a regular wallet and is bound to the proxy methods. This is useful when you want to make sure there are no collisions between method names in the proxy and in the implementation. The Implementation Administrator is the account in charge to orchestrating implementation upgrades.
If you want to share the implementation administration between several recipients, you can deploy a Gnosis Safe contract and configure its threshold as you please. The easiest way to deploy a Gnosis Safe is to use the official Gnosis Safe App and follow the instructions to setup the initial members and threshold.
This is not the same gnosis safe account as the system administrator, it needs to have a different address.
This/these account(s) will be in charge of adding keys to the system. They have to be handled by the Node Operator that manages the validator infrastructure.
These environment variables are required for the deployment command.
You will need a private key pointing to a funded account on the deployment network. This account won't have any ownership or extra rights upon the system, losing this key will no represent a threat for the system (still, don't lose your keys)
You will need an RPC endpoint on the deployment network. You can use a service like Infura, Alchemy or your own node.
The name of the network you are deploying to. Can be one of:
goerli
You will need to have the address of the official Deposit Contract available on your deployment network.
These configuration variables are required to be properly set for the deployment. The variable naming is file:path = value
hardhat.config.ts
:namedAccounts.admin.$NETWORK
=$SYSTEM_ADMIN
hardhat.config.ts
:namedAccounts.proxyAdmin.$NETWORK
=$PROXY_ADMIN
hardhat.config.ts
:namedAccounts.operator.$NETWORK
=$OPERATOR
hardhat.config.ts
:namedAccounts.depositContract.$NETWORK
=$DEPOSIT_CONTRACT
To start the deployment process, run this command by replacing the variables with the values gathered in the steps above and making sure that configuration file values are set properly.
env PK=$PK RPC_URL=$RPC_URL yarn hh deploy --network $NETWORK
The method to call on the StakingContract
to add a node operator is function addOperator(address _operatorAddress)
from the system administrator account.
This command is intended for testnet only purposes. In production, it is expected from the system administrator to properly submit transactions from the multisig account.
You will need to prepare the following variable before sending the call
OPERATOR_ADDRESS
: Address of a node operatorSTAKING_CONTRACT_ADDRESS
: Address of the deployed staking contractRPC_URL
: Ethereum RPC endpoint on the deployment networkMNEMONIC_FILE
: A file containing the system administrator mnemonic wallet (testnet only !)
Run
cast send --mnemonic-path $MNEMONIC_FILE $STAKING_CONTRACT_ADDRESS "addOperator(address)" $OPERATOR_ADDRESS
The method to call on the StakingContract
to set a node operator staking limit is function setOperatorLimit(uint256 _operatorIndex, uint256 _limit)
from the system administrator account.
This command is intended for testnet only purposes. In production, it is expected from the system administrator to properly submit transactions from the multisig account.
You will need to prepare the following variable before sending the call
OPERATOR_INDEX
: Index of a registered node operatorSTAKING_LIMIT
: The maximum amount of funded validators the node operator can haveSTAKING_CONTRACT_ADDRESS
: Address of the deployed staking contractRPC_URL
: Ethereum RPC endpoint on the deployment networkMNEMONIC_FILE
: A file containing the system administrator mnemonic wallet (testnet only !)
Run
cast send --mnemonic-path $MNEMONIC_FILE $STAKING_CONTRACT_ADDRESS "setOperatorLimit(uint256,uint256)" $OPERATOR_INDEX
$STAKING_LIMIT
The method to call on the StakingContract
to add new validator keys is function addValidators(uint256 _operatorIndex, uint256 _keyCount, bytes calldata _publicKeys, bytes calldata _signatures)
from the node operator account registered at index _operatorIndex
.
This command is intended for testnet only purposes. In production, it is expected from the operator to properly submit keys from its infrastructure and making sure the operator wallet is stored in the adequate hardware or service. This solution is mainly meant to quickly populate the contract.
You will need to prepare the following variable before sending the call
PUBLIC_KEYS
: Concatenate your public keys.SIGNATURES
: Concatenate your signatures. Make sure the signature for a public key is at the same index in the concatenation as its associated public key.KEY_COUNT
: Total count of keysSTAKING_CONTRACT_ADDRESS
: Address of the deployed staking contractRPC_URL
: Ethereum RPC endpoint on the deployment networkMNEMONIC_FILE
: A file containing the operator mnemonic keyOPERATOR_INDEX
: The index of the operator
Run
cast send --mnemonic-path $MNEMONIC_FILE $STAKING_CONTRACT_ADDRESS "addValidators(uint256,uint256,bytes,bytes)" $OPERATOR_INDEX $KEY_COUNT $PUBLIC_KEYS $SIGNATURES
The upgrade process consists in deploying a new implementation contract and changing the implementation pointed by the corresponding Proxy. In production, this upgrade will be proposed as a multisig signature and all involved parties will be able to see what is the new implementation address for the target proxy.
To upgrade a proxy, you can call upgradeTo(address)
or upgradeToAndCall(address,bytes)
with the implementation admin multisig.
To make sure the upgrade is actually pointing to a contract implementation of a smart contract code that all parties have agreed upon, we can reproduce these steps locally.
To retrieve the implementation bytecode, you have to compile the contracts locally. In the following example, we assume that the upgrade is for the StakingContract
contract.
To compile everything, run forge build
To retrieve the bytecode, run cat out/StakingContract.sol/StakingContract.json | jq .deployedBytecode.object
To retrieve the implementation bytecode that is currently deployed on the network, you will need:
RPC_URL
: Ethereum RPC endpoint on the deployment networkIMPLEMENTATION_ADDRESS
: Address of the new implementation address
and you can run cast code --rpc-url $RPC_URL $IMPLEMENTATION_ADDRESS
You can then compare if the local bytecode and the deployed bytecode are matching.
Generate by running yarn docs
The Staking Contract is the main input of the system. Node Operator pre-register batchs of validator keys. End users can send multiples of 32 ETH directly to the contract, and if enough keys are available the validator deposit(s) will occur. Stakers are also able to define Withdrawer accounts, an account that is allow to withdraw the funds and the collected fees. This allows them to not only specify a different address than the one they use for deposits but also to change this account in the future.
There are two types of fee recipients that can be deployed by the StakingContract
:
- the Execution Layer fee recipient
- the Consensus Layer fee recipient
Each validator public key has two unique fee recipients. We are using the CREATE2
instruction in order to perform a deterministic and state agnostic deployment for both of these fee recipients. What this means is that the address of these recipients can be computed before they are deployed by the StakingContract
and they can also start receiving fees / withdrawals before they are deployed. Users can then ask for withdrawals and the fee recipients will be deployed only at this point, allowing the system to take a fee given to the node operator. Node operators can also trigger withdrawals in behalf of their users to actively collect fees when required.
This Contract is deployed as the implementation for minimal proxy clones used to gather the fees from the Execution Layer or the Consensus Layer. One clone will be deployed per public key at a deterministic address. It is required from node operators to compute this address and use it as the execution client feeRecipient
for the blocks proposed by the validator identified by the public key. This receiver will then forward its balancer to an upgradeable dispatcher, in charge of splitting the funds.
As the recipient address is deterministic, we can compute this address before publishing the key to the contract.
To compute this address, call function getELFeeRecipient(bytes calldata _publicKey) view
on the StakingContract
.
As the recipient address is deterministic, we can compute this address before publishing the key to the contract.
To compute this address, call function getCLFeeRecipient(bytes calldata _publicKey) view
on the StakingContract
.
flowchart TB
subgraph Actors
U[[User]]
O[[Operator]]
A[[Admin]]
end
subgraph Recipients
RA[Recipient Contract A]
RB[Recipient Contract B]
end
subgraph Consensus Layer
VA[Validator A]
VB[Validator B]
end
Dispatcher
StakingContract
RI[Recipient Implementation]
DepositContract
DepositContract--Fund-->VA & VB
U--Stake ETH-->StakingContract
O--Add/Manage keys-->StakingContract
A--Approve keys-->StakingContract
StakingContract--Deposit funded validators-->DepositContract
StakingContract-.- RI --Clone--> RA & RB
U--Exit-->StakingContract
U--Withdraw-->StakingContract
StakingContract--Withdraw--> RB
RB--Withdraw--> Dispatcher
Dispatcher--Take commission--> OT[Operator Treasury]
Dispatcher--Take commission--> IT[Integrator Treasury]
Dispatcher--Withdraw--> U
sequenceDiagram
actor U as User Wallet
U->>Staking Contract: Stake n * 32 ETH
Staking Contract-->>Staking Contract: Load n stored keys and signatures
Staking Contract->>ETH Deposit Contract : Deposit n validators
Staking Contract-->>Staking Contract: Set user as owner of the validator(s)
sequenceDiagram
actor U as User Wallet
participant S as Staking Contract
participant R as Recipient Contract
participant D as Dispatcher Contract
participant T as Integrator Treasury
participant T2 as Operator Treasury
R->>R: Rewards accumulate
U->>S: Withdraw validator
S-->>R: Deploy if needed
S->>R: Withdraw
R->>D: Split rewards
D->>T: Send commission
D->>T2: Send operator commission
D->>U: Send net rewards
sequenceDiagram
actor U as User Wallet
participant S as Staking Contract
participant R as Recipient Contract
participant D as Dispatcher Contract
participant T as Integrator Treasury
participant T2 as Operator Treasury
U->>S: Request exit
S->>S: Emit exit event
Note over S: Exit event triggers exit message broadcast on CL
Note over R: After the protocol withdrawal process concludes funds are here
U->>S: Withdraw validator
S-->>R: Deploy if needed
S->>R: Withdraw
R->>D: Split rewards
D->>T: Send commission
D->>T2: Send operator commission
D->>U: Send principial + net rewards