cd eth-staking-fee-distributor-contracts
yarn
cp .env.example .env
# edit .env with the actual values
yarn typechain
curl -L https://foundry.paradigm.xyz | bash
source /Users/$USER/.bashrc
foundryup
yarn testThe basic use case is reflected in ./test/foundry/Integration.t.sol's test_Main_Use_Case function.
-
Anyone (deployer, does not matter who) deploys
FeeDistributorFactoryproviding_defaultClientBasisPointsargument. This default value of client basis points will be used in case later on, during a client instance creation, increateFeeDistributorfunction,_clientConfig.basisPoints == 0. Initially, the plan is to use9000as the default value. -
Anyone (deployer, does not matter who) deploys
Oracle. -
Anyone (deployer, does not matter who) deploys
P2pOrgUnlimitedEthDepositorproviding arguments:_mainnet: true (means "mainnet")_feeDistributorFactory: address ofFeeDistributorFactory
-
The deployer calls
setP2pEth2DepositoronFeeDistributorFactorywith the address of theP2pOrgUnlimitedEthDepositorcontract from Step 3. -
Anyone (deployer, does not matter who) deploys reference implementations of FeeDistributor contracts. Currently, there are 3 of them:
ElOnlyFeeDistributorarguments:- address of
FeeDistributorFactoryfrom Step 1. - address of the service (P2P) fee recipient
- address of
OracleFeeDistributorarguments:- address of
Oraclefrom Step 2. - address of
FeeDistributorFactoryfrom Step 1. - address of the service (P2P) fee recipient
- address of
ContractWcFeeDistributorarguments:- address of
FeeDistributorFactoryfrom Step 1. - address of the service (P2P) fee recipient
- address of
-
The deployer calls
transferOwnershiponFeeDistributorFactorywith the secure P2P address as an argument. -
The owner calls
changeOperatoronFeeDistributorFactorywith the address of the operator. The operator is an Ethereum account who can callcreateFeeDistributorfor each new client/FeeDistributor type/FeeDistributor percentages. The operator can be a hot wallet, less secure than the owner. Since the primary way to createFeeDistributorinstances isP2pOrgUnlimitedEthDepositor'saddEth, the operator is needed only for creating alternativeFeeDistributorinstances when either client or referrer configs need to be updated. -
The deployer does the same steps (6 and 7) for
Oracle. -
A client calls
addEthonP2pOrgUnlimitedEthDepositorsending (32 * validator count) ETH and providing the arguments:_referenceFeeDistributor: address of FeeDistributor template that determines the terms of staking service_clientConfigaddress and basis points (percent * 100) of the client_referrerConfigaddress and basis points (percent * 100) of the referrer.
The client's ETH is now held in the P2pOrgUnlimitedEthDepositor contract. A new FeeDistributor instance has been deployed if it didn't exist for the same values of
_referenceFeeDistributor, _clientConfig, and _referrerConfig. If it did exist, no additional contract would be deployed.
Instead, the newly deposited ETH value would be added to the stored depositAmount value corresponding to the FeeDistributor instance address.
- P2P service listens to the
P2pOrgUnlimitedEthDepositor__ClientEthAddedevents emitted onaddEthexecution. If it determines that a certainFeeDistributorinstance has at least 32 ETH, it callsmakeBeaconDepositonP2pOrgUnlimitedEthDepositorproviding the arguments:_feeDistributorInstance: user FeeDistributor instance that determines the terms of staking service (from Step 9)_pubkeys: BLS12-381 public keys_signatures: BLS12-381 signatures_depositDataRoots: SHA-256 hashes of the SSZ-encoded DepositData objects
As a result, multiple ETH2 deposits are made.
-
P2P service sets the
_newFeeDistributorAddressfrom Step 9 as the EL rewards recipient in validators' settings. Now the per client/terms of service copy ofFeeDistributorcontract will be receiving EL rewards (MEV, priority fees). -
(Periodically, e.g. daily) P2P oracle service fetches the latest CL rewards sums for each
OracleFeeDistributorinstance's validators. This data is used as theoracleDataargument for./scripts/buildMerkleTreeForFeeDistributorAddress.tsfunction. The result is a Merkle Tree. -
The operator calls
reportonOracleproviding the Merkle Root as the argument. -
Anyone (client, P2P withdrawer service, does not matter who) calls
./scripts/obtainProof.tsfunction providing the address of theOracleFeeDistributorinstance as the argument. It returns:
proof- Merkle proof (the leaf's sibling, and each non-leaf hash that could not otherwise be calculated without additional leaf nodes)_amountInGwei- sum of CL rewards for all validators corresponding to theOracleFeeDistributorinstance.
- Anyone at any time can call
withdrawon a per client/terms of service copy ofFeeDistributor. ForOracleFeeDistributorthe required arguments are:_proofand_amountInGweifrom Step 14. ForElOnlyFeeDistributorandContractWcFeeDistributorno arguments are needed.
The result will be sending the whole current contract's balance to - address of the service (P2P) - address of the client - address of the referrer (optional)
proportionally to the pre-defined - % of EL rewards that should go to the service (P2P) - % of EL rewards that should go to the client - % of EL rewards that should go to the referrer (optional)
FeeDistributorFactory deploys new FeeDistributor instances based on provided FeeDistributor template (reference instance) that determines the type and values:
_clientConfig: address and basis points (percent * 100) of the client_referrerConfig: address and basis points (percent * 100) of the referrer that determine percentages of rewards that will go to the client, the service, and the referrer (terms of service).
Its createFeeDistributor function can be called by either P2pOrgUnlimitedEthDepositor contract during addEth transaction
or the P2P operator if the validators already exist.
FeeDistributor stores
- address of
FeeDistributorFactory - address of the service (P2P) fee recipient
- address and basis points of EL rewards that should go to the client
- address and basis points of EL rewards that should go to the referrer (optional)
FeeDistributor contract instance is unique and has its own address if all of these are the same:
_referenceFeeDistributor: address of the reference implementation of FeeDistributor used as the basis for clones_clientConfig: address and basis points (percent * 100) of the client_referrerConfig: address and basis points (percent * 100) of the referrer
FeeDistributor contract's address is assigned in a validator's setting as EL rewards recipient. Thus, its balance increases over time with each EL reward.
Anyone at any time can call withdraw on the user's own copy of FeeDistributor (See Step 15 above).
Also, it's possible for the P2P operator and the client to call withdraw as an ERC-4337 UserOperation.
An example of how to do it
- for
ElOnlyFeeDistributorandContractWcFeeDistributorcan be found at./scripts/sendErc4337UserOperation.ts - for
OracleFeeDistributorcan be found at./scripts/sendErc4337UserOperationForOracleFeeDistributor.tsUserOperation will allow the P2P operator and the client not to pay for the gas. Gas fee will be deducted from theFeeDistributorinstance itself.FeeDistributoris an ERC-4337Senderbut it is restricted towithdrawfunction only.
Currently, there are 3 types of FeeDistributor:
ElOnlyFeeDistributoraccepting and splitting EL rewards onlyOracleFeeDistributoraccepting EL rewards only but splitting them with consideration of CL rewardsContractWcFeeDistributoraccepting and splitting both CL and EL rewards
New types can be added in the future.
Clients can implement and deploy their own type of FeeDistributor and then use it as a _referenceFeeDistributor for addEth function in P2pOrgUnlimitedEthDepositor.
P2P will then be able to evaluate the proposed terms of service.
If they are OK, P2P will proceed with makeBeaconDeposit.
If they are not, the client will be able to return their ETH from P2pOrgUnlimitedEthDepositor via the refund function.
voluntaryExit function of FeeDistributor allows the client to signal P2P about their intention to exit 1 or more validators that correspond to the provided _pubkeys.
In ContractWcFeeDistributor it has additional functionality for accounting 32 ETH per validator (collateral) that should not be split and should go to the client in full.
Oracle stores Merkle Root, which is used to verify data (FeeDistributor instance address, _amountInGwei) using Merkle Proof.
P2pOrgUnlimitedEthDepositor is a batch deposit contract.
Its makeBeaconDeposit function passes client's ether to the official ETH2 DepositContract and calls FeeDistributorFactory to create an instance of FeeDistributor.
Its refund function allows the client to get their ETH back if P2P did not use it for staking during the pre-defined TIMEOUT.
Its rejectService function allows P2P to indicate that there will be no Beacon deposit made so the client can get a refund immediately without waiting for expiration.
Can be helpful if the client made a mistake and asked P2P to release their funds sooner than expiration. Or if P2P does not agree with the terms of service suggested by the client.