Thanks @Valdorff for the great RPIP. I compiled the below notes while going over the proposal.
RPL stake required is based on Protocol ETH
In order to require a NO to have 10% of matched Protocol ETH in staked RPL value, we need to now store total protocol ETH used on a per node operator basis. This is easy to calculate going forward, but we currently donāt track that for past minipools so we will need some migration path.
Simplest way to handle this migration is to make the following changes:
-
rocketNodeStaking.getNodeMinimumRPLStake
checks the new āNode Operatorās matched Protocol ETH balanceā (matchedETH
) value. If it is equal to 0, it then queries rocketMinipoolManager.getNodeStakingMinipoolCount
for the node and multiplies the result by 16 ETH. That value is then used in place of matchedETH
for the purposes of calculating the minimum RPL stake.
-
rocketMinipoolManager.incrementNodeStakingMinipoolCount
should do a similar thing as above. If matchedETH
is 0, calculate the correct value by multiplying active minipools by 16 ETH. It should then store that value into the matchedETH
storage. It can then also increase matchedETH
by minipool.getUserDepositBalance()
.
-
rocketMinipoolManager.decrementNodeStakingMinipoolCount
should to the same as above, but it should decrease matchedETH
by the user deposit balance instead.
-
getNodeEffectiveRPLStake
, getNodeMinimumRPLStake
, and getNodeMaximumRPLStake
on rocketNodeStaking
should all be updated to feature similar backwards compatibility logic.
This works because all previous minipools have (half) or will (full) take 16 ETH from the protocol. And in the case this is the NOs first minipool, this calculation just results in 0 which is the correct value anyway.
Reward tree generation should be unchanged as calls to rocketNodeStaking.getNodeEffectiveRPLStake
will return the correct value for pre- and post- migrated Node Operators.
Snapshot voting does not take into account effective RPL stake below the minimum requirement. This is because it uses RocketNodeStaking.getNodeEffectiveRPLStake()
which does not return 0 if the NOs RPL stake is below the minimum threshold.
Breaking delegate rollbacks
The upgrade to LEB8 supporting delegate has to be a one way ticket. Otherwise there are attack vectors in upgrading, migrating to an LEB, then downgrading. For this reason, we will have the migration to an LEB ābreakā the state of older delegates such that downgrading results in a non-functioning delegate.
We create a new rocketMinipoolDelegate
which contains some migrateToLEB
method. Calling this method sets the existing userDepositBalance
storage slot to (2**256)-1. It stores the correct value in a new storage slot which takes the name userDepositBalance
. The new distribute method reads the new correct value. Whereas rolling back to the old delegate would now result in the NO not receiving any of their ETH back if they call distribute.
Handling existing rewards
This problem is decribed in more depth in my post here: Design decisions for LEBs aside from collateral requirements
In short, we need a way to handle rewards accrued up to the point of migrating to an LEB. As there will be undistributed beaconchain rewards that are owed to the NO at the existing collateral ratio. Seems like most people agree that a merkle tree solution is the way to go. The decision remains on whether we do a single āline in the sandā tree or something more like Kenās idea of including the migration data in each reward period merkle tree for some period of time (12 months).
Credit system
Calling migrateToLEB
would perform a further call to a new method on rocketNodeManager
called increaseNodeETHCredit
. The method increases the amount of credit the NO can draw from when deploying a minipool.
During a call into rocketNodeDeposit.deposit
, this credit balance can be decreased to top up any shortfall in ETH supplied via msg.value. The ETH must be available in the deposit pool at the time of deposit.
If a NOās credit falls below the minimum required to perform a deposit, I think allowing them to refund that amount is desirable (thanks Knoshua for this idea). This involves a new method called withdrawDustCredit
on rocketNodeManager
.
New commission rate
The upgrade process can set ānetwork.node.fee.minimumā, ānetwork.node.fee.targetā, and ānetwork.node.fee.maximumā to 14%.
Should a minipool that migrates to an LEB have its node fee reset to 14%? This would require updating ānode.average.fee.numeratorā for the given NO.
Distributor upgrade
Currently, the priority fee/mev distributor contract assumes a 50/50 split of user and node funds. We will need to upgrade this to support LEBs that no longer meet that assumption.
We run into the same issue we did with varying node fees per minipool. ETH rewards simply appear in the distributor contract so we do not know from which minipool they were contributed.
There are 2 ways I can think to handle this:
1. Averaging supplied ETH collateral ratio
Similarly to how we average the node fee across minipools, we average the collateral ratio. This would mean keeping track of the sum of all matched Protocol ETH and NO supplied ETH on a per NO basis. Then during distribution, we use this ratio when splitting rewards.
rocketNodeDistributor.distribute
would change from something like this:
halfBalance = balance / 2
nodeShare = halfBalance + (halfBalance * averageNodeFee)
userShare = balance - nodeShare
to something like this:
collateralRatio = protocolSupplied / (nodeSupplied + protocolSupplied)
userBalance = balance * collateralRatio
nodeShare = (balance - userBalance) + (userBalance * averageNodeFee)
userShare = balance - nodeShare
2. Separate distributor
With a separate distributor for LEBs, we could keep precise track of which minipools contributed rewards. Regular minipools would set their fee_recipient to the current distributor which does a 50/50 split and LEB minipools would be set to send rewards to a different distributor which does the 25/75 split.
This is more accurate but requires an additional contract deployment per node operator and requires the smartnode to be able to set a fee_recipient per validator which is not as robust and brings about more complexity with some smartnode setups.
Minipool queue overhaul
The current minipool queue is designed around having 3 distinct deposit types (half, full, empty) and items are dequeued in series. E.g. all half minipools are assigned first, then only after all half pools are assigned, the full queue starts to get assigned.
This queue system no longer works as we will want to have both 16 ETH and 8 ETH pools in the same queue. Since deploying we have also removed both full and empty minipool types so it makes sense to overhaul the queue system to support the new design and remove the unused stuff.
For this, we can create an entirely new queue which can take both 16 ETH and 8 ETH deposits (as well as any other future amount). During _assignDeposits
on rocketDepositPool
, we now simply dequeue a minipool from the global queue and call userDeposit
on the minipool with the value calculated as 32 ETH minus the value returned from rocketMinipool.getNodeDepositBalance
. This now supports any node deposit amount we may decide to add in the future including variable amounts.
Of course, we need to service the existing queues too as they will likely have minipools waiting to be assigned at the time of the upgrade. So _assignDeposits
would check if the old queues have any items and assign them before moving to the new global queue. In some future update we can remove the old queue code entirely for some marginal gas savings.
Misc notes
- RPIP-8 mentions āMigration from higher commission minipools SHOULD be prioritized if there is any kind of queue for migrationā. Migrating an existing minipool happens immediately by way of refunding the difference to the NOs credit balance. There is no queue to migrate. However, if there isnāt enough ETH in the deposit pool, the NO may not be able to make use of the credit balance until further ETH deposits are made.
- Minipools with < 32 ETH balance should be prevented from migrating to LEBs.