Their primary purpose of these contracts is to batch and proxy SSV validator registrations so that SSV tokens are abstracted away from the clients.
cd p2p-ssv-proxy
cp .env_example .env
# edit .env with the actual values
curl -L https://foundry.paradigm.xyz | bash
source /Users/$USER/.bashrc
foundryup
forge test
Client has 100500 ETH and wants to stake it with DVT (SSV) in 1 transaction, preserving custody over withdrawal credentials but delegating validator key management and all the low-level data generation to P2P.
-
Client calls
P2pSsvProxyFactory
'saddEth
function once. The ETH value should be 100500 ETH.The arguments are:
- _eth2WithdrawalCredentials - client's withdrawal credentials
- _ethAmountPerValidatorInWei - amount of ETH per validator (exactly 32000000000000000000 (32 ETH in wei) before Pectra)
- _clientConfig - should be in the format {recipient:"0x6Bb8b45a1C6eA816B70d76f83f7dC4f0f87365Ff",basisPoints:9000} for Etherscan if client address is 0x6Bb8b45a1C6eA816B70d76f83f7dC4f0f87365Ff and client should get 90% of total ETH rewards
- _referrerConfig - should be in the format {recipient:"0x81CE71EEB7531AA26073eD0d1110F6F0563C6C7c",basisPoints:600} for Etherscan if referrer address is 0x81CE71EEB7531AA26073eD0d1110F6F0563C6C7c and referrer should get 6% of total ETH rewards. P2P will receive 4% in this case.
- _extraData - any data. Can be empty (""). Intended to be used for MEV relay information, client-specific encrypted data, etc.
Note: addresses and percentages (basis points) here are for example only. Please double check the actual values before sending any mainnet ETH!!!
Client does not need to do anything else for staking. All the following steps are done by P2P:
-
P2P generates ETH2 deposit data and corresponding SSV shares data for each validator.
-
P2P calls
P2pSsvProxyFactory
'smakeBeaconDepositsAndRegisterValidators
function for batches up to 50 validators using- the client data from Step 1 (retrived from
P2pSsvProxyFactory
'sP2pSsvProxyFactory__EthForSsvStakingDeposited
events andP2pOrgUnlimitedEthDepositor
'sP2pOrgUnlimitedEthDepositor__ClientEthAdded
events). - the ETH2 deposit data and corresponding SSV shares data generated in Step 2.
- the client data from Step 1 (retrived from
-
If P2P did not do the actual staking deposits within 1 day, client can call
P2pOrgUnlimitedEthDepositor
'srefund
function to get their ETH back.
Client has 3200 ETH and wants to stake it with DVT (SSV), preserving custody over withdrawal credentials and private keys of the validators.
All the steps below can happen client-side without any interaction with P2P servers. SSV API can be used for convenience, although, all the data is also available on-chain.
- Client generates 100 ETH2 validator private keys on their side. (For example, can be generated from 1 mnemonic).
- Client generates 100 keystore JSON files on their side (encrypted private keys).
- Client generates 100 ETH2 deposit data JSON files on their side.
Steps 1 - 3 can be done using the native tools (like staking-deposit-cli or wagyu-key-gen).
-
Client reads a list of addresses of allowed SSV operator owners from P2pSsvProxyFactory contract’s
getAllowedSsvOperatorOwners
function. (For example, it can return 4 addresses). -
Client reads allowed SSV operator IDs from P2pSsvProxyFactory contract’s
getAllowedSsvOperatorIds
function providing it with the address of the SSV operator owner from the previous step each time it’s called. (For example, for 4 addresses,getAllowedSsvOperatorIds
function should be called 4 times. As a result, the client gets 4 SSV operator IDs). -
Client predicts
FeeDistributor
instance address by readingFeeDistributorFactory
’spredictFeeDistributorAddress
function.Need to provide it with:
-
_referenceFeeDistributor
- address of the templateFeeDistributor
(can be of any type likeElOnlyFeeDistributor
,OracleFeeDistributor
, orContractWcFeeDistributor
) -
_clientConfig
(basis points, client fee recipient address) -
_referrerConfig
(basis points, referrer fee recipient address)
-
-
Client predicts
P2pSsvProxy
instance address by readingP2pSsvProxyFactory
'spredictP2pSsvProxyAddress
function, providing it with theFeeDistributor
instance address from the previous step. -
Client generates 100 SSV keyshares JSON files choosing operator IDs from Step 5 and cluster owner from Step 7.
(
P2pSsvProxy
instance address is the cluster owner).ssv-keys tool can be used for generation.
-
Client reads operator snapshots from SSVNetwork contract’s storage slots. Each operator has its own snapshot.
💡 This can be done using any library with RPC access to Ethereum execution layer blockchain (`eth_getStorageAt` RPC Method, e.g. [ethers.js](https://docs.ethers.org/v5/api/providers/provider/#Provider-getStorageAt), [web3.py](https://web3py.readthedocs.io/en/v5/web3.eth.html#web3.eth.Eth.get_storage_at), etc.).An example of how it’s done using Foundry’s forge:
function getSnapshot(uint64 operatorId) private view returns(bytes32 snapshot) { uint256 p = uint256(keccak256("ssv.network.storage.main")) + 5; bytes32 slot1 = bytes32(uint256(keccak256(abi.encode(uint256(operatorId), p))) + 2); snapshot = vm.load(ssvNetworkAddress, slot1); }
-
Client reads slot #
💡 This can be done using any library with RPC access to Ethereum execution layer blockchain (`eth_getStorageAt` ****RPC Method, e.g. [ethers.js](https://docs.ethers.org/v5/api/providers/provider/#Provider-getStorageAt), [web3.py](https://web3py.readthedocs.io/en/v5/web3.eth.html#web3.eth.Eth.get_storage_at), etc.).6836850959782774711213773224022472945316713988199727877409042202683022748181
(DEC)0x0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15
(HEX) from SSVNetwork contract’s storage.An example of how it’s done using Foundry’s forge:
function getSsvSlot0() private view returns(bytes32 ssvSlot0) { bytes32 slot = bytes32(uint256(keccak256("ssv.network.storage.protocol")) - 1); ssvSlot0 = vm.load(ssvNetworkAddress, slot); }
slot
here equals to0x0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15
An example of how it’s done using Foundry’s cast on Mainnet:
cast storage 0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1 6836850959782774711213773224022472945316713988199727877409042202683022748181 --rpc-url https://rpc.ankr.com/eth
-
Client gets the latest SSV cluster state either from SSV API or from SSV Scanner CLI. (
P2pSsvProxy
****instance address predicted in Step 7 is the cluster owner). -
Client gets all the operator fees (per block) either from SSV API or from SSVNetworkViews’s
getOperatorFee
function. -
Client gets all the network fee (per block) either from SSV API or from SSVNetworkViews’s
getNetworkFee
function. -
Client gets all the liquidation threshold period (in blocks) either from SSV API or from SSVNetworkViews’s
getLiquidationThresholdPeriod
function. -
Client calculates the SSV token amount required to serve all 100 validators for the desired period of time (in blocks). We recommend the desired period at least 250000 blocks (about 35 days).
$tokenAmount = (sum(Operator Fees) + Network Fee) * (Liquidation Threshold Period + Desired Period) * 100$ -
Client calls
P2pSsvProxyFactory
'sdepositEthAndRegisterValidators
function with the data prepared above in batches of 50 validators. (For 100 validators, it’s going to be 2 transactions). The ETH value should be 1600 ETH (32 ETH * 50 validators) in each transaction.-
depositEthAndRegisterValidators
interfacefunction depositEthAndRegisterValidators( DepositData calldata _depositData, address _withdrawalCredentialsAddress, SsvPayload calldata _ssvPayload, FeeRecipient calldata _clientConfig, FeeRecipient calldata _referrerConfig ) external payable returns (address p2pSsvProxy); struct DepositData { bytes[] signatures; bytes32[] depositDataRoots; } struct SsvPayload { SsvOperator[] ssvOperators; SsvValidator[] ssvValidators; Cluster cluster; uint256 tokenAmount; bytes32 ssvSlot0; } struct SsvOperator { address owner; uint64 id; bytes32 snapshot; uint256 fee; } struct SsvValidator { bytes pubkey; bytes sharesData; } struct Cluster { uint32 validatorCount; uint64 networkFeeIndex; uint64 index; bool active; uint256 balance; } struct FeeRecipient { uint96 basisPoints; address payable recipient; }
-
Client has 100 already deposited validators and wants to distribute the keys with DVT (SSV), preserving custody over withdrawal credentials and private keys of the validators.
All the steps below can happen client-side without any interaction with P2P servers. SSV API can be used for convenience, although, all the data is also available on-chain.
-
Client reads a list of addresses of allowed SSV operator owners from P2pSsvProxyFactory contract’s
getAllowedSsvOperatorOwners
function. (For example, it can return 4 addresses). -
Client reads allowed SSV operator IDs from P2pSsvProxyFactory contract’s
getAllowedSsvOperatorIds
function providing it with the address of the SSV operator owner from the previous step each time it’s called. (For example, for 4 addresses,getAllowedSsvOperatorIds
function should be called 4 times. As a result, the client gets 4 SSV operator IDs). -
Client predicts
**FeeDistributor**
instance address by readingFeeDistributorFactory
’spredictFeeDistributorAddress
function.Need to provide it with:
-
_referenceFeeDistributor
- address of the templateFeeDistributor
(can be of any type likeElOnlyFeeDistributor
,OracleFeeDistributor
, orContractWcFeeDistributor
) -
_clientConfig
(basis points, client fee recipient address) -
_referrerConfig
(basis points, referrer fee recipient address)
-
-
Client predicts
**P2pSsvProxy
** instance address by reading**P2pSsvProxyFactory**
'spredictP2pSsvProxyAddress
function, providing it with theFeeDistributor
instance address from the previous step. -
Client generates 100 SSV keyshares JSON files choosing operator IDs from Step 2 and cluster owner from Step 4.
(
P2pSsvProxy
****instance address is the cluster owner).ssv-keys tool can be used for generation.
-
Client reads operator snapshots from SSVNetwork contract’s storage slots. Each operator has its own snapshot.
💡 This can be done using any library with RPC access to Ethereum execution layer blockchain (`eth_getStorageAt` ****RPC Method, e.g. [ethers.js](https://docs.ethers.org/v5/api/providers/provider/#Provider-getStorageAt), [web3.py](https://web3py.readthedocs.io/en/v5/web3.eth.html#web3.eth.Eth.get_storage_at), etc.).An example of how it’s done using Foundry’s forge:
function getSnapshot(uint64 operatorId) private view returns(bytes32 snapshot) { uint256 p = uint256(keccak256("ssv.network.storage.main")) + 5; bytes32 slot1 = bytes32(uint256(keccak256(abi.encode(uint256(operatorId), p))) + 2); snapshot = vm.load(ssvNetworkAddress, slot1); }
-
Client reads slot #
💡 This can be done using any library with RPC access to Ethereum execution layer blockchain (`eth_getStorageAt` ****RPC Method, e.g. [ethers.js](https://docs.ethers.org/v5/api/providers/provider/#Provider-getStorageAt), [web3.py](https://web3py.readthedocs.io/en/v5/web3.eth.html#web3.eth.Eth.get_storage_at), etc.).6836850959782774711213773224022472945316713988199727877409042202683022748181
(DEC)0x0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15
(HEX) from SSVNetwork contract’s storage.An example of how it’s done using Foundry’s forge:
function getSsvSlot0() private view returns(bytes32 ssvSlot0) { bytes32 slot = bytes32(uint256(keccak256("ssv.network.storage.protocol")) - 1); ssvSlot0 = vm.load(ssvNetworkAddress, slot); }
slot
here equals to0x0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15
An example of how it’s done using Foundry’s cast on Mainnet:
cast storage 0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1 6836850959782774711213773224022472945316713988199727877409042202683022748181 --rpc-url https://rpc.ankr.com/eth
-
Client gets the latest SSV cluster state either from SSV API or from SSV Scanner CLI. (
P2pSsvProxy
****instance address predicted in Step 4 is the cluster owner). -
Client gets all the operator fees (per block) either from SSV API or from SSVNetworkViews’s
getOperatorFee
function. -
Client gets all the network fee (per block) either from SSV API or from SSVNetworkViews’s
getNetworkFee
function. -
Client gets all the liquidation threshold period (in blocks) either from SSV API or from SSVNetworkViews’s
getLiquidationThresholdPeriod
function. -
Client calculates the SSV token amount required to serve all 100 validators for the desired period of time (in blocks). We recommend the desired period at least 250000 blocks (about 35 days).
$tokenAmount = (sum(Operator Fees) + Network Fee) * (Liquidation Threshold Period + Desired Period) * 100$ -
Client reads the ETH amount required to cover SSV token costs from
P2pSsvProxyFactory
'sgetNeededAmountOfEtherToCoverSsvFees
function providing it with thetokenAmount
from the previous step. -
Client calls
P2pSsvProxyFactory
'sregisterValidators
function with the data prepared above. The ETH value should be equal to the ETH amount required to cover SSV token costs (from the previous step).-
registerValidators
interfacefunction registerValidators( SsvPayload calldata _ssvPayload, FeeRecipient calldata _clientConfig, FeeRecipient calldata _referrerConfig ) external payable returns (address); struct SsvPayload { SsvOperator[] ssvOperators; SsvValidator[] ssvValidators; Cluster cluster; uint256 tokenAmount; bytes32 ssvSlot0; } struct SsvOperator { address owner; uint64 id; bytes32 snapshot; uint256 fee; } struct SsvValidator { bytes pubkey; bytes sharesData; } struct Cluster { uint32 validatorCount; uint64 networkFeeIndex; uint64 index; bool active; uint256 balance; } struct FeeRecipient { uint96 basisPoints; address payable recipient; }
-
Both P2pSsvProxyFactory
and P2pSsvProxy
contracts have built-in functions to recover (send to any chosen address) ETH and any ERC-20, ERC-721, and ERC-1155 tokens by the owner (P2P).
function transferEther(address _recipient, uint256 _amount) external;
function transferERC20(address _token, address _recipient, uint256 _amount) external;
function transferERC721(address _token, address _recipient, uint256 _tokenId) external;
function transferERC1155(address _token, address _recipient, uint256 _tokenId, uint256 _amount, bytes calldata _data) external;
P2pSsvProxyFactory exists as a single instance for everyone. It is the entry point for validator registration.
It stores:
referenceFeeDistributor
- a template set by P2P to be used for newFeeDistributor
instances. Can be changed by P2P at any time. It will only affect the new clusters. Existing clusters will keep their existingFeeDistributor
instance.referenceP2pSsvProxy
- a template set by P2P to be used for newP2pSsvProxy
instances. Can be changed by P2P at any time. It will only affect the new clusters. Existing clusters will keep their existingP2pSsvProxy
instance.allowedSsvOperatorOwners
- a set of addresses of SSV operator owners (both P2P and partners). Only P2P can add or remove addresses from the set.allowedSsvOperatorIds
- a mapping of (operator owner address → SSV operator IDs list). The list of allowed SSV operator IDs for each address is limited to 24 IDs. The operator owner can update only their list. P2P can update lists of any owners.allClientP2pSsvProxies
- a mapping of (client address → a list of addresses of the deployed clientP2pSsvProxy
instances). Updated automatically duringP2pSsvProxy
instance deployment.allP2pSsvProxies
- a list of all ever deployed clientP2pSsvProxy
instances. Updated automatically duringP2pSsvProxy
instance deployment.clientSelectors
- a mapping to check if a certain selector (function signature) is allowed for clients to call onSSVNetwork
viaP2pSsvProxy
.operatorSelectors
- a mapping to check if a certain selector (function signature) is allowed for a P2P operator to call onSSVNetwork
viaP2pSsvProxy
.ssvPerEthExchangeRateDividedByWei
- Exchange rate between SSV and ETH set by P2P. (If 1 SSV = 0.007539 ETH, it should be 0.007539 * 10^18 = 7539000000000000). Only used during validator registration without ETH deposits to cover SSV token costs with client ETH.
P2pSsvProxyFactory’s functions:
-
depositEthAndRegisterValidators
- batch validator registration with ETH deposit. Callable by anyone.-
interface
function depositEthAndRegisterValidators( DepositData calldata _depositData, address _withdrawalCredentialsAddress, SsvPayload calldata _ssvPayload, FeeRecipient calldata _clientConfig, FeeRecipient calldata _referrerConfig ) external payable returns (address p2pSsvProxy);
-
-
registerValidators
- batch validator registration without ETH deposit. Callable by anyone.-
interface
function registerValidators( SsvPayload calldata _ssvPayload, FeeRecipient calldata _clientConfig, FeeRecipient calldata _referrerConfig ) external payable returns (address);
-
-
predictP2pSsvProxyAddress
- getP2pSsvProxy
instance address for a givenFeeDistributor
instance address.-
interface
function predictP2pSsvProxyAddress( address _feeDistributorInstance ) external view returns (address);
-
P2pSsvProxy has identity tied to FeeDistributor
. A new instance of P2pSsvProxy
is created each time when the first SSV validator registration happens for a set of:
-
_referenceFeeDistributor
- address of the templateFeeDistributor
-
_clientConfig
(basis points, client fee recipient address) -
_referrerConfig
(basis points, referrer fee recipient address) -
UML Class Diagram
-
Call Graph
It stores:
feeDistributor
- FeeDistributor
instance address
P2pSsvProxy allows to call all SSVNetwork
functions having P2pSsvProxy instance as msg.sender for those calls.
For the client, only removeValidator
function is available out of the box. It’s still possible for P2P to allow any other functions for clients to call. It’s done via P2pSsvProxyFactory’s setAllowedSelectorsForClient
function.
- Mainnet: 0xec17A02B2A8b0C291C0DddE2a00Ca24477c17ED5
- Holesky: 0x1e534b1B344fDA49F2778b9b956f7EeB2280e258
Native ETH2 (Beacon) deposit contract, 1 for all.
- Mainnet: 0x00000000219ab540356cBB839Cbe05303d7705Fa
- Holesky: 0x4242424242424242424242424242424242424242
FeeDistributorFactory
1 for all. Predicts the address and creates FeeDistributor instances.
function predictFeeDistributorAddress(
address _referenceFeeDistributor,
FeeRecipient calldata _clientConfig,
FeeRecipient calldata _referrerConfig
) external view returns (address);
function createFeeDistributor(
address _referenceFeeDistributor,
FeeRecipient calldata _clientConfig,
FeeRecipient calldata _referrerConfig
) external returns (address newFeeDistributorAddress);
- Mainnet: 0x86a9f3e908b4658A1327952Eb1eC297a4212E1bb
- Holesky: 0x5cdF046Bd49629E5130a4A82400733523Ba5820C
FeeDistributor is a family of contracts with the same interface. Currently, there are 3 types of FeeDistributor:
ElOnlyFeeDistributor
accepting and splitting EL rewards only, WC == client rewards recipient addressOracleFeeDistributor
accepting EL rewards only but splitting them with consideration of CL rewards, WC == client rewards recipient addressContractWcFeeDistributor
accepting and splitting both CL and EL rewards, WC == address of a client instance ofContractWcFeeDistributor
contract
You can read more about them here.
Also, for each type of FeeDistributor
contract, there is a reference instance that doesn’t belong to any client and only exists as a template. The address of such a template can be passed to FeeDistributorFactory
's predictFeeDistributorAddress
and createFeeDistributor
functions.
Reference (template) FeeDistributor instances:
- Mainnet:
- OracleFeeDistributor 0x7109DeEb07aa9Eed1e2613F88b2f3E1e6C05163f
- Holesky:
- OracleFeeDistributor 0x26766611723B654DeE5F041C8E20FDBC4EB37b31
For each set of
_referenceFeeDistributor
- address of the referenceFeeDistributor
_clientConfig
(basis points, client fee recipient address)_referrerConfig
(basis points, referrer fee recipient address)
there will be a separate instance of FeeDistributor
. Its address can be predicted even before it has been deployed using FeeDistributorFactory’s predictFeeDistributorAddress
function.
The actual deployment is done using FeeDistributorFactory’s createFeeDistributor
function.
1 for all
ERC-20 token used for paying fees in SSV.
- Mainnet: 0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54
- Holesky: 0xad45A78180961079BFaeEe349704F411dfF947C6
1 for all
- Mainnet: 0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1
- Holesky: 0x38A4794cCEd47d3baf7370CcC43B560D3a1beEFA
1 for all