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 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);
-
-
Mainnet: TBD
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: TBD
- Goerli: 0xb00C3a8D951b0C4bac8Db2f006c614F05D29E666
Native ETH2 (Beacon) deposit contract, 1 for all.
- Mainnet: 0x00000000219ab540356cBB839Cbe05303d7705Fa
- Goerli: 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b
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
- Goerli: 0xe17cA83F84295aA66EC1d199bc569B0dbCddFb05
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:
- ElOnlyFeeDistributor 0x6091767Be457a5A7f7d368dD68Ebf2f416728d97
- OracleFeeDistributor 0x7109DeEb07aa9Eed1e2613F88b2f3E1e6C05163f
- ContractWcFeeDistributor 0xf6aa125a49c7B371f27dEA01E7407DAa85AB91ed
- Goerli:
- ElOnlyFeeDistributor 0x6cC87e83c677F32C45e70Cf64B1fbCa7560d217f
- OracleFeeDistributor 0x98C275395677cAbc5EC21f4D669eB66a3A25fdA4
- ContractWcFeeDistributor 0x1A18808B53492dB3C6De7fAB83c728b430f5A105
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
- Goerli: 0x3a9f01091C446bdE031E39ea8354647AFef091E7
1 for all
- Mainnet: 0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1
- Goerli: 0xC3CD9A0aE89Fff83b71b58b6512D43F8a41f363D
1 for all