StakeTableV2
Inherits: StakeTable, PausableUpgradeable, AccessControlUpgradeable
Title: Ethereum L1 component of the Espresso Global Confirmation Layer (GCL) stake table.
This contract is an upgrade to the original StakeTable contract. On Espresso mainnet we will only use the V2 contract. On decaf the V2 is used to upgrade the V1 that was first deployed with the original proof of stake release.
The V2 contract contains the following changes:
- The functions to register validators and update consensus keys are updated to require both a
BLS signature and a Schnorr signature and emit the signatures via events so that the GCL can
verify them. The new functions and events have a V2 postfix. After the upgrade components that
support registration and key updates must use the V2 functions and listen to the V2 events. The
original functions revert with a
DeprecatedFunctionerror in V2. - The exit escrow period can be updated by the owner of the contract within valid bounds (15 blocks to 14 days).
- The following functions can be paused by the PAUSER_ROLE:
claimWithdrawal(...)claimValidatorExit(...)delegate(...)undelegate(...)deregisterValidator(...)registerValidatorV2(...)updateConsensusKeysV2(...)updateCommission(...)updateMetadataUri(...)When paused, these functions revert with a standard pausable error,EnforcedPause(). Only the PAUSER_ROLE can pause/unpause the contract. Note:updateExitEscrowPeriodis NOT pausable for emergency governance access.
- The
claimValidatorExitfunction is overridden to ensure that the validator’s delegatedAmount is updated during this method The update is deferred until the funds are actually withdrawn. - The
deregisterValidatorfunction is overridden to ensure that the validator’s delegatedAmount is not updated during this method as it was in v1. - The
updateExitEscrowPeriodfunction is added to allow governance to update the exit escrow period within valid bounds (15 blocks to 14 days). - The
pauseandunpausefunctions are added for emergency control. - The commission rate for validators can be updated with the
updateCommissionfunction. - The
activeStakevariable is added to allow governance to track the total stake in the contract. The activeStake is the total stake that is not awaiting exit or in exited state. - Unique undelegation IDs are assigned to each undelegation via an auto-incrementing counter
for better event tracking. The
undelegationIdspublic mapping stores IDs alongside the baseundelegationsmapping. EventsUndelegatedV2,WithdrawalClaimed, andValidatorExitClaimedinclude the undelegation ID as an indexed parameter for efficient querying and tracking. - Validators must provide a metadata URI during registration and can update it via
updateMetadataUri. The metadata URI is event-sourced only (not stored on-chain for gas efficiency). TheValidatorRegisteredV2event includes the metadata URI, and a newMetadataUriUpdatedevent is emitted when validators update their URI. Metadata URIs can be empty and cannot exceed 2048 bytes. - A minimum delegation amount (
minDelegateAmount) is enforced to prevent dust delegations and reduce state bloat. The minimum is initialized to 1 ESP token (1 ether) ininitializeV2and can be updated by governance via thesetMinDelegateAmountfunction. Thedelegatefunction reverts withDelegateAmountTooSmallif the delegation amount is below the minimum.
The StakeTableV2 contract ABI is a superset of the original ABI. Consumers of the
contract can use the V2 ABI, even if they would like to maintain backwards compatibility.
Governance: This contract enforces a single-admin model. owner() and
DEFAULT_ADMIN_ROLE always reference the same address and can only be
changed via transferOwnership() (directly or through
grantRole(DEFAULT_ADMIN_ROLE, ...), which delegates to the transfer). Any
attempt to revoke or renounce the default admin role, or to renounce
ownership, reverts. This removes governance drift.
All functions are marked as virtual so that future upgrades can override them.
State Variables
PAUSER_ROLE
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE")
MAX_METADATA_URI_LENGTH
Maximum length for metadata URIs (in bytes)
uint256 public constant MAX_METADATA_URI_LENGTH = 2048
MAX_COMMISSION_BPS
Maximum commission in basis points (100% = 10000 bps)
uint16 public constant MAX_COMMISSION_BPS = 10000
MIN_EXIT_ESCROW_PERIOD
Minimum exit escrow period (2 days)
This is a technical minimum bound enforced by the contract. Setting the exit escrow period to this minimum does not guarantee safety. The actual exit escrow period must be set such that the contract holds funds long enough until they are no longer staked in Espresso, allowing sufficient time for validators to exit the active validator set and for slashing evidence to be submitted. Governance should set a value appropriate for Espresso network parameters (e.g., blocksPerEpoch, blockTime, and epoch duration) to ensure security.
uint64 public constant MIN_EXIT_ESCROW_PERIOD = 2 days
MAX_EXIT_ESCROW_PERIOD
Maximum exit escrow period (14 days)
Reasonable upper bound to prevent excessive lockup periods
uint64 public constant MAX_EXIT_ESCROW_PERIOD = 14 days
minCommissionIncreaseInterval
Minimum time interval between commission increases (in seconds)
uint256 public minCommissionIncreaseInterval
maxCommissionIncrease
Maximum commission increase allowed per increase (in basis points)
uint16 public maxCommissionIncrease
activeStake
Total stake in active (not marked for exit) validators in the contract
uint256 public activeStake
minDelegateAmount
min delegate amount
uint256 public minDelegateAmount
commissionTracking
Commission tracking for each validator
mapping(address validator => CommissionTracking tracking) public commissionTracking
schnorrKeys
Schnorr keys that have been seen by the contract
ensures a bijective mapping between schnorr key and ethereum account and prevents some errors due to misconfigurations of validators the contract currently marks keys as used and only allow them to be used once. This for example prevents callers from accidentally registering the same Schnorr key twice.
mapping(bytes32 schnorrKey => bool used) public schnorrKeys
nextUndelegationId
Auto-incrementing counter for unique undelegation IDs
Initialized to 1 in initializeV2 so that 0 can be used to identify V1 undelegations
uint64 private nextUndelegationId
undelegationIds
Mapping from (validator, delegator) to undelegation ID
Separate from base Undelegation struct since base contract is immutable
mapping(address validator => mapping(address delegator => uint64 id)) private undelegationIds
Functions
constructor
Constructor
This function is overridden to disable initializers
constructor() ;
initializeV2
Reinitialize the contract
initialCommissions must be an empty array if the contract we’re upgrading has not been used before (e.g. on mainnet). On decaf (sepolia), this must be called with the current commissions of pre-existing validators read from L1 events.
Sets up roles and transfers ownership to admin. The deployer picks the admin address (timelock, multisig, etc.) based on config.
function initializeV2(
address pauser,
address admin,
uint256 initialActiveStake,
InitialCommission[] calldata initialCommissions
) public onlyOwner reinitializer(2);
Parameters
| Name | Type | Description |
|---|---|---|
pauser | address | The address to be granted the pauser role |
admin | address | The address to be granted the default admin role and ownership. This should be a timelock contract address, multisig, or another governance address. |
initialActiveStake | uint256 | The initial active stake in the contract |
initialCommissions | InitialCommission[] | commissions of validators |
getVersion
Get the version of the contract
This function is overridden to return the version of the contract
function getVersion()
public
pure
virtual
override
returns (uint8 majorVersion, uint8 minorVersion, uint8 patchVersion);
pause
Pause the contract
This function is only callable by the PAUSER_ROLE
function pause() external onlyRole(PAUSER_ROLE);
unpause
Unpause the contract
This function is only callable by the PAUSER_ROLE
function unpause() external onlyRole(PAUSER_ROLE);
transferOwnership
Transfers ownership and keeps DEFAULT_ADMIN_ROLE in sync Grants the role to new owner and revokes from old owner. Access control is enforced by both onlyRole(DEFAULT_ADMIN_ROLE) and super.transferOwnership() which requires onlyOwner. This ensures that only the current admin (who holds both ownership and DEFAULT_ADMIN_ROLE) can transfer ownership. This is intentionally not pausable for emergency governance access.
Transfers ownership of the contract to a new account (newOwner).
Can only be called by the current owner.
function transferOwnership(address newOwner)
public
virtual
override
onlyRole(DEFAULT_ADMIN_ROLE);
grantRole
Grants a role. Granting DEFAULT_ADMIN_ROLE transfers ownership first, which handles both role grant and ownership transfer atomically. This is intentionally not pausable for emergency governance access.
Grants role to account.
If account had not been already granted role, emits a {RoleGranted}
event.
Requirements:
- the caller must have
role’s admin role. May emit a {RoleGranted} event.
function grantRole(bytes32 role, address account) public virtual override;
revokeRole
Prevent revoking DEFAULT_ADMIN_ROLE to preserve the single-admin invariant.
Revokes role from account.
If account had been granted role, emits a {RoleRevoked} event.
Requirements:
- the caller must have
role’s admin role. May emit a {RoleRevoked} event.
function revokeRole(bytes32 role, address account) public virtual override;
renounceRole
Prevent renouncing DEFAULT_ADMIN_ROLE to preserve the single-admin invariant.
Revokes role from the calling account.
Roles are often managed via {grantRole} and {revokeRole}: this function’s
purpose is to provide a mechanism for accounts to lose their privileges
if they are compromised (such as when a trusted device is misplaced).
If the calling account had been revoked role, emits a {RoleRevoked}
event.
Requirements:
- the caller must be
callerConfirmation. May emit a {RoleRevoked} event.
function renounceRole(bytes32 role, address callerConfirmation) public virtual override;
claimValidatorExit
Withdraw previously delegated funds after a validator has exited
This function is overridden to deduct the amount from the validator’s delegatedAmount
and to add pausable functionality
since the delegated Amount is no longer updated during validator exit
function claimValidatorExit(address validator) public virtual override whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The validator to withdraw from |
claimWithdrawal
Withdraw previously delegated funds after an undelegation
This function is overridden to add pausable functionality and emit ID in event
function claimWithdrawal(address validator) public virtual override whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The validator to withdraw from |
delegate
Delegate funds to a validator
This function is overridden to add pausable functionality
The function body is copied from V1 to maintain checks-effects-interactions pattern.
function delegate(address validator, uint256 amount) public virtual override whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The validator to delegate to |
amount | uint256 | The amount to delegate |
undelegate
Undelegate funds from a validator
This function is overridden to add pausable functionality and emit UndelegatedV2
The undelegation ID can be retrieved from the UndelegatedV2 event or via getUndelegation()
function undelegate(address validator, uint256 amount) public virtual override whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The validator to undelegate from |
amount | uint256 | The amount to undelegate |
deregisterValidator
Deregister a validator
This function is overridden to add pausable functionality
and to ensure that the validator’s delegatedAmount is not updated until withdrawal
delegatedAmount represents the no. of tokens that have been delegated to a validator, even if it’s not participating in consensus
emits ValidatorExitV2 instead of ValidatorExit
function deregisterValidator() public virtual override whenNotPaused;
registerValidatorV2
Register a validator in the stake table
This function is overridden to add pausable functionality
and to add schnorrSig validation
function registerValidatorV2(
BN254.G2Point memory blsVK,
EdOnBN254.EdOnBN254Point memory schnorrVK,
BN254.G1Point memory blsSig,
bytes memory schnorrSig,
uint16 commission,
string memory metadataUri
) external virtual whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
blsVK | BN254.G2Point | The BLS verification key |
schnorrVK | EdOnBN254.EdOnBN254Point | The Schnorr verification key |
blsSig | BN254.G1Point | The BLS signature that authenticates the BLS VK |
schnorrSig | bytes | The Schnorr signature that authenticates the Schnorr VK |
commission | uint16 | in % with 2 decimals, from 0.00% (value 0) to 100% (value 10_000) |
metadataUri | string | The metadata URI for the validator |
updateConsensusKeysV2
Update the consensus keys of a validator
This function is overridden to add pausable functionality
and to add schnorrSig validation
function updateConsensusKeysV2(
BN254.G2Point memory blsVK,
EdOnBN254.EdOnBN254Point memory schnorrVK,
BN254.G1Point memory blsSig,
bytes memory schnorrSig
) public virtual whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
blsVK | BN254.G2Point | The new BLS verification key |
schnorrVK | EdOnBN254.EdOnBN254Point | The new Schnorr verification key |
blsSig | BN254.G1Point | The BLS signature that authenticates the blsVK |
schnorrSig | bytes | The Schnorr signature that authenticates the schnorrVK |
updateCommission
Update the commission rate for a validator
- Only one commission increase per minCommissionIncreaseInterval is allowed.
- The commission increase cannot exceed maxCommissionIncrease. These limits protect stakers from sudden large commission increases, particularly by exiting validators.
function updateCommission(uint16 newCommission) external virtual whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
newCommission | uint16 | The new commission rate in % with 2 decimals (0 to 10_000) |
updateMetadataUri
Update the metadata URI for a validator
The metadata URI is NOT stored on-chain. Off-chain indexers must listen to the MetadataUriUpdated event to track the current URI. URIs should be kept reasonably short for gas efficiency (max 2048 bytes). Only the validator (msg.sender) can update their own metadata URI.
No format validation is performed on the URI - any string within length limits (including empty string) is accepted. Consumers should validate URI format and accessibility off-chain.
function updateMetadataUri(string memory metadataUri) external virtual whenNotPaused;
Parameters
| Name | Type | Description |
|---|---|---|
metadataUri | string | The new metadata URI |
setMinCommissionUpdateInterval
Set the minimum interval between commission updates
function setMinCommissionUpdateInterval(uint256 newInterval)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE);
Parameters
| Name | Type | Description |
|---|---|---|
newInterval | uint256 | The new minimum interval in seconds |
setMaxCommissionIncrease
Set the maximum commission increase allowed per update
function setMaxCommissionIncrease(uint16 newMaxIncrease)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE);
Parameters
| Name | Type | Description |
|---|---|---|
newMaxIncrease | uint16 | The new maximum increase in basis points (e.g., 500 = 5%) |
_initializeCommissions
Initialize validator commissions during V2 migration
This function is used to retroactively initialize commission storage for validators that were registered before the V2 upgrade. On decaf, this will be called with current commission values read from L1 events. On mainnet, this will be called with an empty array since there are no pre-existing validators.
function _initializeCommissions(InitialCommission[] calldata initialCommissions) private;
Parameters
| Name | Type | Description |
|---|---|---|
initialCommissions | InitialCommission[] | Array of InitialCommission structs containing validator addresses and their commissions |
_initializeActiveStake
Initialize the active stake in the contract
function _initializeActiveStake(uint256 initialActiveStake) private;
Parameters
| Name | Type | Description |
|---|---|---|
initialActiveStake | uint256 | The initial active stake in the contract |
validateMetadataUri
Validate metadata URI length
Public view function to allow external validation before transaction submission
function validateMetadataUri(string memory metadataUri) public pure;
Parameters
| Name | Type | Description |
|---|---|---|
metadataUri | string | The metadata URI to validate |
updateExitEscrowPeriod
Update the exit escrow period
This function ensures that the exit escrow period is within the valid range (MIN_EXIT_ESCROW_PERIOD to MAX_EXIT_ESCROW_PERIOD). However, governance MUST set a value that ensures funds are held until they are no longer staked in Espresso, accounting for validator exit time and slashing evidence submission windows. This function is not pausable so that governance can perform emergency updates in the presence of system upgrades.
function updateExitEscrowPeriod(uint64 newExitEscrowPeriod)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE);
Parameters
| Name | Type | Description |
|---|---|---|
newExitEscrowPeriod | uint64 | The new exit escrow period |
getUndelegation
Get details of an undelegation for a (validator, delegator) pair
function getUndelegation(address validator, address delegator)
external
view
returns (uint64 id, uint256 amount, uint256 unlocksAt);
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | Address of the validator |
delegator | address | Address of the delegator |
Returns
| Name | Type | Description |
|---|---|---|
id | uint64 | Unique ID of the undelegation |
amount | uint256 | Amount of tokens undelegated |
unlocksAt | uint256 | Timestamp when tokens can be claimed |
_hashSchnorrKey
function _hashSchnorrKey(EdOnBN254.EdOnBN254Point memory schnorrVK)
internal
pure
returns (bytes32);
ensureNewKeys
Ensure that the BLS and Schnorr keys are not already used
function ensureNewKeys(BN254.G2Point memory blsVK, EdOnBN254.EdOnBN254Point memory schnorrVK)
internal
view;
Parameters
| Name | Type | Description |
|---|---|---|
blsVK | BN254.G2Point | The BLS verification key |
schnorrVK | EdOnBN254.EdOnBN254Point | The Schnorr verification key |
registerValidator
Deprecate previous registration function
This function is overridden to revert with a DeprecatedFunction error
users must call registerValidatorV2 instead
function registerValidator(
BN254.G2Point memory,
EdOnBN254.EdOnBN254Point memory,
BN254.G1Point memory,
uint16
) external pure override;
updateConsensusKeys
Deprecate previous updateConsensusKeys function
This function is overridden to revert with a DeprecatedFunction error
users must call updateConsensusKeysV2 instead
function updateConsensusKeys(
BN254.G2Point memory,
EdOnBN254.EdOnBN254Point memory,
BN254.G1Point memory
) external pure override;
setMinDelegateAmount
Set the minimum delegate amount
function setMinDelegateAmount(uint256 newMinDelegateAmount)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE);
Parameters
| Name | Type | Description |
|---|---|---|
newMinDelegateAmount | uint256 | The new minimum delegate amount in wei |
_authorizeUpgrade
Authorize an upgrade to a new implementation
This function is overridden to use AccessControl instead of Ownable Only addresses with DEFAULT_ADMIN_ROLE can authorize upgrades
function _authorizeUpgrade(address newImplementation)
internal
virtual
override
onlyRole(DEFAULT_ADMIN_ROLE);
Parameters
| Name | Type | Description |
|---|---|---|
newImplementation | address | The address of the new implementation |
Events
ValidatorRegisteredV2
A validator is registered in the stake table
the blsSig and schnorrSig are validated by the Espresso Network
event ValidatorRegisteredV2(
address indexed account,
BN254.G2Point blsVK,
EdOnBN254.EdOnBN254Point schnorrVK,
uint16 commission,
BN254.G1Point blsSig,
bytes schnorrSig,
string metadataUri
);
ConsensusKeysUpdatedV2
A validator updates their consensus keys
the blsSig and schnorrSig are validated by the Espresso Network
event ConsensusKeysUpdatedV2(
address indexed account,
BN254.G2Point blsVK,
EdOnBN254.EdOnBN254Point schnorrVK,
BN254.G1Point blsSig,
bytes schnorrSig
);
ExitEscrowPeriodUpdated
The exit escrow period is updated
event ExitEscrowPeriodUpdated(uint64 newExitEscrowPeriod);
CommissionUpdated
A validator updates their commission rate
the timestamp is emitted to simplify processing in the GCL
event CommissionUpdated(
address indexed validator, uint256 timestamp, uint16 oldCommission, uint16 newCommission
);
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The address of the validator |
timestamp | uint256 | The timestamp of the update |
oldCommission | uint16 | |
newCommission | uint16 | The new commission rate |
MinCommissionUpdateIntervalUpdated
The minimum commission update interval is updated
event MinCommissionUpdateIntervalUpdated(uint256 newInterval);
Parameters
| Name | Type | Description |
|---|---|---|
newInterval | uint256 | The new minimum update interval in seconds |
MaxCommissionIncreaseUpdated
The maximum commission increase is updated
event MaxCommissionIncreaseUpdated(uint16 newMaxIncrease);
Parameters
| Name | Type | Description |
|---|---|---|
newMaxIncrease | uint16 | The new maximum commission increase in basis points |
UndelegatedV2
A delegator undelegated funds from a validator (V2 with unlocksAt and undelegationId)
event UndelegatedV2(
address indexed delegator,
address indexed validator,
uint64 indexed undelegationId,
uint256 amount,
uint256 unlocksAt
);
Parameters
| Name | Type | Description |
|---|---|---|
delegator | address | The address of the delegator |
validator | address | The address of the validator |
undelegationId | uint64 | Unique identifier for this undelegation |
amount | uint256 | The amount undelegated |
unlocksAt | uint256 | The timestamp when the funds can be claimed |
WithdrawalClaimed
A delegator claimed an undelegation (V2 with undelegationId)
event WithdrawalClaimed(
address indexed delegator,
address indexed validator,
uint64 indexed undelegationId,
uint256 amount
);
Parameters
| Name | Type | Description |
|---|---|---|
delegator | address | The address of the delegator |
validator | address | The address of the validator |
undelegationId | uint64 | Unique identifier for this undelegation |
amount | uint256 | The amount claimed |
ValidatorExitClaimed
A delegator claimed funds after validator exit
event ValidatorExitClaimed(
address indexed delegator, address indexed validator, uint256 amount
);
Parameters
| Name | Type | Description |
|---|---|---|
delegator | address | The address of the delegator |
validator | address | The address of the validator |
amount | uint256 | The amount claimed |
ValidatorExitV2
A validator initiated an exit (V2 with unlocksAt)
event ValidatorExitV2(address indexed validator, uint256 unlocksAt);
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The address of the validator |
unlocksAt | uint256 | The timestamp when delegators can claim their funds |
MetadataUriUpdated
A validator updated their metadata URI
event MetadataUriUpdated(address indexed validator, string metadataUri);
Parameters
| Name | Type | Description |
|---|---|---|
validator | address | The address of the validator |
metadataUri | string | The new metadata URI |
MinDelegateAmountUpdated
The minimum delegate amount is updated
event MinDelegateAmountUpdated(uint256 newMinDelegateAmount);
Parameters
| Name | Type | Description |
|---|---|---|
newMinDelegateAmount | uint256 | The new minimum delegate amount in wei |
Errors
InvalidSchnorrSig
The Schnorr signature is invalid (either the wrong length or the wrong key)
error InvalidSchnorrSig();
DeprecatedFunction
The function is deprecated as it was replaced by a new function
error DeprecatedFunction();
CommissionUpdateTooSoon
The commission update is too soon after the last update
error CommissionUpdateTooSoon();
CommissionIncreaseExceedsMax
The commission increase exceeds the maximum allowed increase
error CommissionIncreaseExceedsMax();
CommissionUnchanged
The commission value is unchanged
error CommissionUnchanged();
InvalidRateLimitParameters
The rate limit parameters are invalid
error InvalidRateLimitParameters();
CommissionAlreadyInitialized
The validator commission has already been initialized
error CommissionAlreadyInitialized(address validator);
InitialActiveStakeExceedsBalance
The initial active stake exceeds the balance of the contract
error InitialActiveStakeExceedsBalance();
SchnorrKeyAlreadyUsed
The Schnorr key has been previously registered in the contract.
error SchnorrKeyAlreadyUsed();
DefaultAdminCannotBeRevoked
Attempted to revoke DEFAULT_ADMIN_ROLE which would break single-admin governance
error DefaultAdminCannotBeRevoked();
DefaultAdminCannotBeRenounced
Attempted to renounce DEFAULT_ADMIN_ROLE which would break single-admin governance
error DefaultAdminCannotBeRenounced();
NoUndelegationFound
No undelegation exists for the given validator and delegator
error NoUndelegationFound();
InvalidMetadataUriLength
The metadata URI exceeds maximum allowed length
error InvalidMetadataUriLength();
DelegateAmountTooSmall
The delegate amount is too small
error DelegateAmountTooSmall();
MinDelegateAmountTooSmall
The minimum delegate amount is too small
error MinDelegateAmountTooSmall();
Structs
CommissionTracking
Struct for tracking validator commission and last increase time
struct CommissionTracking {
uint16 commission;
uint256 lastIncreaseTime;
}
InitialCommission
Struct for initializing validator commissions during migration
struct InitialCommission {
address validator;
uint16 commission;
}