MEV-Boost relays deliver blocks from block builders to proposing validators, each block having an ETH value which is delivered to the proposer. Some relays have proposed forming a "guild" of sorts, charging builders a small fee, formulated as a small % (e.g. 0.5% of the block value). While naively this could be accomplished by requiring each builder to create one additional transaction every block to pay the guild, blockchain transaction fees make this infeasible.
Instead, the contract designed here is intended to handle upfront deposits from builders, with a requirement to provide at least X ETH before relays will accept their blocks. A separate, off-chain accounting service monitors the block-to-block changes in current value (deposited collateral minus accumulated fees). On a regular schedule (weekly), the guild reconciles the on-chain balances, moving the correct amount of accumulated fees from each builders' deposits to the guild's multisig.
# This is not intended to be exhaustive but define the interfaces that we expect builders and the guild to need.
# Listed emit(Event) are those expected to be used by supporting tools.
# Listed require() are just suggestions, are not all necessary, and are non-exhaustive.
# Constructor, data structures, naming, require(), etc, are flexible
# Where functionality is ambiguous refer to prototype contract
# Registers a BLS pubkey with the sender's Ethereum address
# Optional: Require sender to deposit (or already have) some ETH deposited to prevent griefing
# Optional: Accept msg.value to simultaneously allow deposits
def builderRegistration(builderPubkey) -> None:
require(len(builderPubkey) == 48)
require("builderPubkey not already registered")
emit(BuilderRegistered(sender, builderPubkey))
# Manually modifies a builder pubkey registration, in case of error or griefing
# Trusting admin to handle this power responsibly
def adminBuilderRegistration(builderPubkey, builderAddress) -> None:
require(len(builderPubkey) == 48)
require(sender == owner)
emit(BuilderRegistered(builderAddress, builderPubkey))
# Deposits addional ETH collateral, backing all of their registered pubkeys
def builderDeposit() -> None:
emit(BuilderDeposit(sender, value))
# Marks a builder's account for withdrawal after the next reconciliation
def builderWithdraw() -> None:
# No specific requirements
# Gets the balance for the builder corresponding to the given pubkey
def getBalance(builderPubkey) -> uint:
# Returns builder balance
# Reconciles off-chain fee acounting to on-chain balances
# Practically, moves feeToTake from builderAddress's deposit to the guild multisig
# reconcileSlot is the block at which total fees for the last period were calculated,
# not necessarily the slot this tx appears in.
def reconcile(builderAddress, feeToTake, reconcileSlot) -> None:
require(sender == owner)
# If feeToTake > builderBalance, set to 0; debt resolution will be handled off-chain
# If builder has a queued withdrawal, handle it after taking fee
emit(BalanceReconciled(builderAddress, feeToTake, newBalance, reconcileSlot))
# Publicly announces any changes to the relay service fee
# There must always be some advance notice for builders to prepare
# So this says that the fee will be `current` before `fromBlock` and `new` after that
def setServiceFee(current, new, fromBlock) -> None:
require(sender == owner)
require("fee numbers match underlying format")
emit(ServiceFeeSet(current, new, fromBlock))
# Returns the current service fee, likely some fixed-point representation
def getServiceFee() -> uint:
# Returns current service fee
# Use fromBlock and current block number to get correct value
# Returns the latest slot reconcile was calculated
# Doesn't need to be stored per-builder, can remember one number globally
# and assume it applies to all builders
def getLatestReconcileSlot() -> uint:
# Returns latest slot when reconciliation was performed
We need to be able to retrieve/construct this kind of data by either querying the smart contract directly, or emitted events over a block range.
We should be able to construct a dictionary of builders containing pubkeys, address, withdrawal queued, status and contract balance. The builder names can come from a map stored off chain.
builders = ["beaverbuild.org": #name from a map stored elsewhere
{"builder_pubkeys": ["0x96a59d355b1f65e270b29981dd113625732539e955a1beeecbc471dd0196c4804574ff871d47ed34ff6d921061e9fc27",
"0xb5d883565500910f3f10f0a2e3a031139d972117a3b67da191ff93ba00ba26502d9b65385b5bca5e7c587273e40f2319",
"0xaec4ec48c2ec03c418c599622980184e926f0de3c9ceab15fc059d617fa0eafe7a0c62126a4657faf596a1b211eec347",
"0xacb407cfb554255db2fbbb320f79bb7f1cc1e8d2dc43324e8e31baafd0836340d49c43eebc51828f53bf6d364f9ac207",
"0xa21a2f4807a2bcb6b07c10cea241322e0910c30869c1e4eda686b0d69bdcb74d2a140ef994afcf0bb38e0b960df4d2ee",
"0x8dde59a0d40b9a77b901fc40bee1116acf643b2b60656ace951a5073fe317f57a086acf1eac7502ea32edcca1a900521",
"0xa474cd266e61c2a7e2ac434a75d0302f5ff0c307395ec18dbd434f7b256927a75a4ed972c6107a6c6c3fe6696dd034d1"],
"builder_address": "0x100000000000000000000000000000000000dEaD",
"withdrawal_queued": False,
"active": True,
"current_balance": 0
}, ...]}
Deposits should be retrieveable by getting events on the smart contract. Builder names can be looked up from a map (offchain)
deposits = [{'slot': 7591736, 'builder_name': 'rsync-builder.xyz', 'tx_id': '0x6fe6f1b3d3579680d2c9139444686d838ad2743a42ece2959d6dd8016f3ffcfb', 'deposit': 3000000000000000000}, ...}
Previous reconcilliation events should be retrievable by getting events on the smart contract.
reconciliation_events = [{'reconciliation_round': 0, 'reconcile_at_slot': 7450000, 'status': 'confirmed'}, ...]
It's probably helpful to be able to emit and listen for builderRegistration()
and setServiceFee()
too