Staking Precompile
This page is intended for developers looking to build smart contracts or interfaces that interact with the staking system.
The entrypoint to the staking system is the stateful staking precompile.
This precompile allows delegators and validators to take actions that affect the composition of the validator set.
- join the validator set (
addValidator
) - delegate their stake to a validator (
delegate
) - undelegate their stake from a validator (a multi-step process:
undelegate
, wait the required number of epochs, thenwithdraw
) - compound the rewards they earned as a delegator (i.e. delegate the rewards)
(
compound
) - claim the rewards they earned as a delegator (
claimRewards
)
Although users may delegate or undelegate at any time, stake weight changes only take effect at epoch boundaries, and stake weight changes made too close to the epoch boundary get queued until the next epoch boundary. This is to allow for the separation of consensus and execution in Monad, as described in Consensus and Execution.
Only standard CALL
s are allowed to the staking precompile. In particular,
STATICCALL
and DELEGATECALL
are not allowed.
Because the staking system is a precompile and not a smart contract, you cannot test against it in a forked environment.
Precompile Address and Selectors
The contract address is 0x0000000000000000000000000000000000001000
.
The external functions are identified by the following 4-byte selectors.
External state-modifying methods:
addValidator(bytes,bytes,bytes)
-0xf145204c
delegate(uint64)
-0x84994fec
undelegate(uint64,uint256,uint8)
-0x5cf41514
compound(uint64)
-0xb34fea67
withdraw(uint64,uint8)
-0xaed2ee73
claimRewards(uint64)
-0xa76e2ca5
changeCommission(uint64,uint256)
-0x9bdcc3c8
externalReward(uint64)
-0xe4b3303b
External view methods:
getValidator(uint64)
-0x2b6d639a
getDelegator(uint64,address)
-0x573c1ce0
getWithdrawalRequest(uint64,address,uint8)
-0x56fa2045
getConsensusValidatorSet(uint32)
-0xfb29b729
getSnapshotValidatorSet(uint32)
-0xde66a368
getExecutionValidatorSet(uint32)
-0x7cb074df
getDelegations(address,uint64)
-0x4fd66050
getDelegators(uint64,address)
-0xa0843a26
getEpoch()
-0x757991a8
Syscalls:
syscallOnEpochChange(uint64)
-0x1d4e9f02
syscallReward(address)
-0x791bdcf3
syscallSnapshot()
-0x157eeb21
External State-Modifying Methods
addValidator
Function selector
addValidator(bytes,bytes,bytes) : 0xf145204c
Function signature
function addValidator( bytes payload bytes signed_secp_message, bytes signed_bls_message) external returns(uint64 validator_id) payable;
Parameters
payload
- consists of the following fields, packed together in big endian (equivalent toabi.encodePacked()
in Solidity):bytes secp_pubkey
(unique SECP public key used for consensus)bytes bls_pubkey
(unique BLS public key used for consensus)address auth_address
(address used for the validator’s delegator account. This address has withdrawal authority for the validator's staked amount)uint256 amount
(amount the validator is self-staking. Must equalmsg.value
)uint256 commission
(commission charged to delegators multiplied by 1e18, e.g.10% = 1e17
)
signed_secp_message
- SECP signature over payloadsigned_bls_message
- BLS signature over payload
Gas cost
505,125
Behavior
This creates a validator with an associated delegator account and returns the resultant val_id
.
The method starts by unpacking the payload to retrieve the secp_pubkey
, bls_pubkey
,
auth_address
, amount
, and commission
, then verifying that the signed_secp_message
and signed_bls_message
correspond to the payload signed by the corresponding SECP and BLS
private keys.
- The validator must provide both a unique BLS key and a unique SECP key. Submissions with any repeated public keys will revert.
- Both signatures (
signed_secp_message
andsigned_bls_message
) must be valid and must sign over thepayload
. - Multiple validators may share the same
auth_address
. msg.value
must be equal or greater thanMIN_VALIDATE_STAKE
or the call will revert.- If the
msg.value
is also equal or greater thanACTIVE_VALIDATOR_STAKE
then the validator will become active in the future:- If
addValidator
was called before the boundary block, then in epochn+1
; - Otherwise it will become active in epoch
n+2
.
- If
Pseudocode
secp_pubkey, bls_pubkey, auth_address, amount, commission = payload
assert amount == msg.value
// increment validator idlast_val_id = last_val_id + 1;
// set uniqueness of keyssecp_to_val_id[secp_eth_address] = last_val_id;bls_to_val_id[bls_eth_address] = last_val_id;
// set validator infoval_execution[last_val_id] = ValExecution{ uint256 stake = msg.value; uint256 commission = commission; KeysPacked keys = KeysPacked{secp_pubkey, bls_pubkey} uint256 address_flags = set_flags();}
// set authority delegator infodelegator[last_val_id][input.auth_address] = DelInfo{ uint256 delta_stake = set_stake()[0]; uint256 next_delta_stake = set_stake()[1]; uint64 delta_epoch = set_stake()[2]; uint64 next_delta_epoch = set_stake()[3];}
// set delegator accumulatorepoch_acc[last_val_id][getEpoch()] = Accumulator{ uint256 ref_count += 1;}
// set flagsset_flags();
// push validator idif (val_execution[last_val_id].stake() >= ACTIVE_VALIDATOR_STAKE and last_val_id not in execution_valset): execution_valset.push(last_val_id);
return last_val_id;
def set_flags(): if msg.value + val_execution[last_val_id].stake() >= ACTIVE_VALIDATOR_STAKE: return ValidatorFlagsOk; if msg.value + val_execution[last_val_id].stake() >= MIN_VALIDATE_STAKE return ValidatorFlagsStakeTooLow;
def set_stake(): if in_boundary: delta_stake = 0; next_delta_stake = msg.value; delta_epoch = 0; next_delta_epoch = current_epoch + 2; else: delta_stake = msg.value; next_delta_stake = 0; delta_epoch = current_epoch + 1; next_delta_epoch = 0; return [delta_stake, next_delta_stake, delta_epoch, next_delta_epoch];
Usage
Here is an example of assembling the payload and signing:
def generate_add_validator_call_data_and_sign( secp_pubkey: bytes, bls_pubkey: bytes, auth_address: bytes, amount: int, commission: int secp_privkey: bytes bls_privkey: bytes) -> bytes: # 1) Encode payload_parts = [ secp_pubkey, bls_pubkey, auth_address, toBigEndian32(amount), toBigEndian32(commission), ] payload = b"".join(payload_parts)
# 2) Sign with both keys secp_sig = SECP256K1_SIGN(blake3(payload), secp_privkey) bls_sig = BLS_SIGN(hash_to_curve(payload), bls_privkey)
# 3) Solidity encode the payload and two signatures return eth_abi.encode(['bytes', 'bytes', 'bytes'], [payload, secp_sig, bls_sig])
delegate
Function selector
delegate(uint64) : 0x84994fec
Function signature
function delegate( uint64 val_id) external returns(bool success) payable;
Parameters
val_id
- id of the validator that delegator would like to delegate tomsg.value
- the amount to delegate
Gas cost
260,850
Behavior
This creates a delegator account if it does not exist and increases the delegator's balance.
- The delegator account is determined by
msg.sender
. val_id
must correspond to a valid validator.msg.value
must be > 0.- If this delegation causes the validator's total stake to exceed
ACTIVE_VALIDATOR_STAKE
, then the validator will be added toexecution_valset
if not already present. - The delegator stake becomes active
- in epoch
n+1
if the request is before the boundary block - in epoch
n+2
otherwise
- in epoch
Pseudocode
validator_id = msg.input.val_id;
// set validator informationval_execution[validator_id] = ValExecution{ uint256 stake += msg.value();}
// set delegator informationDelInfo current_delegator = delegator[validator_id][msg.sender];
// apply get_current_stake() first. This updates the delegator stake// to be inline with the current stake activated in consensus.get_current_stake();
// apply add_stake() second.uint256[4] add_stake_info = add_stake(msg.value());
current_delegator = DelInfo{ uint256 delta_stake = add_stake_info[0]; uint256 next_delta_stake = add_stake_info[1]; uint64 delta_epoch = add_stake_info[2]; uint64 next_delta_epoch = add_stake_info[3];}
// set epoch accumulatorepoch_acc[validator_id][getEpoch()].ref_count += 1;
// set flagsset_flags();
// push validator idif val_execution[validator_id].stake() >= ACTIVE_VALIDATOR_STAKE and validator_id not in execution_valset: execution_valset.push(validator_id);
def add_stake(uint256 amount): uint256 _delta_stake; uint256 _next_delta_stake; uint64 _delta_epoch; uint64 _next_delta_epoch;
if not in_boundary: _delta_stake = current_delegator.delta_stake() + amount; _next_delta_stake = 0; _delta_epoch = current_epoch + 1; _next_delta_epoch = 0; else: _delta_stake = 0; _next_delta_stake = current_delegator.next_delta_stake() + amount; _delta_epoch = 0; _next_delta_epoch = current_epoch + 2; return [_delta_stake, _next_delta_stake, _delta_epoch, _next_delta_epoch];
def maybe_process_next_epoch_state(): """ Helper function to process and update rewards based on the current epoch state. """
if ( epoch_acc[validator_id][current_delegator.delta_epoch()] != 0 and current_epoch > current_delegator.delta_epoch() and current_delegator.delta_epoch() > 0 ): // Compute rewards from the last checked epoch. _rewards += current_delegator.stake() * ( epoch_acc[validator_id][current_delegator.delta_epoch()].val() - current_delegator.acc() )
// Promote stake to active in delegator view. current_delegator.stake() += current_delegator.delta_stake() current_delegator.acc() = ( epoch_acc[validator_id][current_delegator.delta_epoch()].val() ) current_delegator.delta_epoch() = current_delegator.next_delta_epoch() current_delegator.delta_stake() = current_delegator.next_delta_stake() current_delegator.next_delta_epoch() = 0 current_delegator.next_delta_stake() = 0
epoch_acc[validator_id][current_delegator.delta_epoch].ref_count -= 1
def get_current_stake(): uint256 _rewards = 0;
// Process next epoch rewards and increment stake maybe_process_next_epoch_state() // Perform again to capture max two additional epochs maybe_process_next_epoch_state()
current_delegator.rewards() += _rewards; return _rewards;
undelegate
Function selector
undelegate(uint64,uint256,uint8) : 0x5cf41514
Function signature
function undelegate( uint64 val_id, uint256 amount, uint8 withdraw_id) external returns(bool success);
Parameters
val_id
- id of the validator to which sender previously delegated, from which we are removing delegationamount
- amount to unstake, in Monad weiwithdraw_id
- identifier for a delegator's withdrawal. Thewithdraw_id
cannot be 0. For each (validator, delegator) tuple, there can be a maximum of 255 in-flight withdrawal requests
Gas cost
147,750
Behavior
This deducts amount
from the delegator account and moves it to a withdrawal request object,
where it remains in a pending state for a predefined number of epochs before the funds are
claimable.
- The delegator account is determined by
msg.sender
. val_id
must correspond to a valid validator to which the sender previously delegated- The delegator must have stake >= amount.
- If the withdrawal causes
Val(val_id).stake()
to drop belowACTIVE_VALIDATOR_STAKE
, then the validator is scheduled to be removed from valset. - If the
auth_address
on a validator undelegates enough of their own stake to drop belowMIN_VALIDATE_STAKE
, then the validator is scheduled to be removed from valset. - A delegator can only remove a stake after it has been activated. This is the stake field in the delegator struct. Pending delegations cannot be removed until they are active.
- The delegator stake becomes inactive in the valset
- in epoch
n+1
if the request is before the boundary block - in epoch
n+2
otherwise
- in epoch
- The delegator stake becomes withdrawable, and thus no longer subject to slashing
- in epoch
n + 1 + WITHDRAWAL_DELAY
if the request is before the boundary block - in epoch
n + 2 + WITHDRAWAL_DELAY
otherwise
- in epoch

undelegate
commandPseudocode
uint64 validator_id = msg.input.val_id;uint256 amount = msg.input.amount;uint8 withdraw_id = msg.input.withdraw_id;
ValExecution current_validator = val_execution[validator_id];
// set validator informationcurrent_validator = ValExecution{ uint256 stake -= amount;}
// apply get_current_stake() first.get_current_stake();
DelInfo current_delegator = delegator[validator_id][msg.sender];// set delegator informationcurrent_delegator = DelInfo{ uint256 stake -= amount;}
// set withdraw requestwithdrawal[validator_id][msg.sender][withdraw_id] = WithdrawalRequest{ uint256 amount = amount; uint256 acc = current_validator.acc(); uint64 epoch = getEpoch();});
// set epoch accumulatorepoch_acc[validator_id][getEpoch()].ref_count += 1;
// schedule validator to leave setif current_validator.stake < ACTIVE_VALIDATOR_STAKE and validator_id in execution_valset: current_validator.set_flag(INSUFFICIENT_STAKE);
if (current_delegator.stake <= MIN_VALIDATE_STAKE and validator_id in execution_valset) and msg.sender == current_validator.auth_address: current_validator.set_flag(INSUFFICIENT_VALIDATOR_STAKE);
compound
Function selector
compound(uint64) : 0xb34fea67
Function signature
function compound( uint64 val_id) external returns(bool success);
Parameters
val_id
- id of the validator to which sender previously delegated, for which we are compounding rewards
Gas cost
285,050
Behavior
This precompile converts the delegator's accumulated rewards into additional stake.
- The account compounded is determined by
msg.sender
. If a delegator account does not exist, then the call reverts val_id
must correspond to a valid validator to which the sender previously delegated- The delegator rewards become active in the valset
- in epoch
n+1
if the request is before the boundary block - in epoch
n+2
otherwise.
- in epoch
Pseudocode
validator_id = msg.input.val_id;
// set delegator informationDelInfo current_delegator = delegator[validator_id][msg.sender];
// apply get_current_stake() first. This updates the delegator stake// to be inline with the current stake activated in consensus.rewards_compounded = get_current_stake();
// apply add_stake() second.uint256[4] add_stake_info = add_stake(rewards_compounded);
// set delegator informationcurrent_delegator = DelInfo{ uint256 delta_stake = add_stake_info[0]; uint256 next_delta_stake = add_stake_info[1]; uint64 delta_epoch = add_stake_info[2]; uint64 next_delta_epoch = add_stake_info[3]; uint256 rewards = 0;}
// set validator informationval_execution[validator_id] = ValExecution{ uint256 stake += rewards_compounded;}
// set accumulatorepoch_acc[validator_id][getEpoch()] = Accumulator{ uint256 ref_count += 1;}
// set flagsset_flags();
// push validator idif val_execution[validator_id].stake() >= ACTIVE_VALIDATOR_STAKE and validator_id not in execution_valset: execution_valset.push(validator_id);
withdraw
Function selector
withdraw(uint64,uint8) : 0xaed2ee73
Function signature
function withdraw( uint64 val_id, uint8 withdraw_id) external returns (bool success);
Parameters
val_id
- id of the validator to which sender previously delegated, from which we previously issued anundelegate
commandwithdraw_id
- identifier for a delegator's previously created withdrawal. For each (validator, delegator) tuple, there can be a maximum of 255 in-flight withdrawal requests. Withdraw id cannot be zero.
Gas cost
68,675
Behavior
This completes an undelegation action (which started with a call to the undelegate
function),
sending the amount to msg.sender
, provided that sufficient epochs have passed.
- The delegator is
msg.sender
. The withdrawal is identified bymsg.sender
,val_id
, andwithdraw_id
- The withdraw action can take place once the undelegation is complete, and the withdraw delay has passed:
- in epoch
n + 1 + WITHDRAWAL_DELAY
if the undelegate request is before the boundary block - in epoch
n + 2 + WITHDRAWAL_DELAY
otherwise
- in epoch
Pseudocode
uint64 validator_id = msg.input.val_id;uint8 withdraw_id = msg.input.withdraw_id;
WithdrawalRequest current_withdraw = withdrawal[validator_id][msg.sender][withdraw_id];
// Compute any additional rewards and transfer funds to delegatortransfer(msg.sender, current_withdraw.amount + get_withdraw_rewards());
// unset withdraw requestwithdrawal[validator_id][msg.sender][withdraw_id] = WithdrawalRequest{ uint256 amount = 0, uint256 acc = 0, uint64 epoch = 0};
def get_withdraw_rewards(): epoch_acc[validator_id][current_withdraw.epoch].ref_count -= 1; return current_withdraw.amount() * (epoch_acc[validator_id][current_withdraw.epoch()].val() - current_withdraw.acc());
claimRewards
Function selector
claimRewards(uint64) : 0xa76e2ca5
Function signature
function claimRewards( uint64 val_id) external returns (bool success);
Parameters
val_id
- id of the validator to which sender previously delegated, for which we are claiming rewards
Gas cost
155,375
Behavior
This precompile allows a delegator to claim any rewards instead of redelegating them.
val_id
must correspond to a valid validator to which the sender previously delegated- If delegator account does not exist for this
(val_id, msg.sender)
tuple, then the call reverts - The delegator's accumulated rewards are transferred to their delegation
Pseudocode
// set delegator informationDelInfo current_delegator = delegator[validator_id][msg.sender];
// apply get_current_stake() first.uint256 current_rewards = get_current_stake();
// set delegator informationcurrent_delegator = DelInfo{ uint256 rewards = 0;)
// send rewards to delegatortransfer(msg.sender, current_rewards);
changeCommission
Function selector
changeCommission(uint64,uint256) : 0x9bdcc3c8
Function signature
function changeCommission( uint64 val_id, uint256 commission) external returns (bool sucess);
Parameters
val_id
- id of the validator, who would like to change their commission ratecommission
- commission rate taken from block rewards, expressed in 1e18 units (e.g., 10% = 1e17)
Gas cost
39,475
Behavior
This allows the auth_account
for a validator to modify the commission for the validator.
The commission cannot be set larger than MAX_COMMISSION
(currently 100%).
- The
msg.sender
must be theauth_address
for the respective validator Id. - The commission cannot be set larger than
MAX_COMMISSION
(currently 100%). - The change in commission occurs in the following epochs:
- in epoch
n+1
if request is not in the epoch delay period. - in epoch
n+2
if request is in the epoch delay period.
- in epoch
Pseudocode
validator_id = msg.input.val_id;
val_execution[validator_id] = ValExecution{ uint256 commission = msg.input.commission;}
externalReward
Function selector
externalReward(uint64) : 0xe4b3303b
Function signature
function externalReward( uint64 val_id) external returns (bool success);
Parameters
val_id
- id of the validatormsg.value
- the value to add to unclaimed rewards
Gas cost
62,300
Behavior
This function allows anyone to send extra MON to the stakers of a particular validator.
This typically will be called by the validator themselves to share extra tips to their delegators.
- This can only be called for a validator currently in the consensus validator set otherwise the transaction reverts.
- The msg.value is between the following values otherwise the transaction reverts: minimum reward is 1 mon and the maximum reward is 1000000 mon.
Pseudocode
validator_id = msg.input.val_id;
require(msg.value >= 1e18 && msg.value <= 1e24, "Reward out of bounds");require(val_consensus[validator_id] > 0 , "Validator not active");
val_execution[validator_id].unclaimed_reward += msg.value;val_execution[val_id].acc += msg.value / val_consensus[val_id].stake();
External View Methods
getValidator
Function selector
getValidator(uint64) : 0x2b6d639a
Function signature
function getValidator( uint64 val_id) external view returns ( address auth_address, uint256 flags, uint256 stake, uint256 acc_reward_per_token, uint256 commission, uint256 unclaimed_reward, uint256 consensus_stake, uint256 consensus_commission, uint256 snapshot_stake, uint256 snapshot_commission, bytes memory secp_pubkey, bytes memory bls_pubkey);
Parameters
val_id
- id of the validator
Gas cost
97,200
Behavior
This is the primary method to obtain information about a validator state. It provides a complete view of the validator’s state across execution, consensus, and snapshot contexts.
It returns:
ValExecution
(execution view)- Stake and commission (consensus view)
- Stake and commission (snapshot view)
getDelegator
Function selector
getDelegator(uint64,address) : 0x573c1ce0
Function signature
function getDelegator( uint64 val_id, address delegator) external returns ( uint256 stake, uint256 acc_reward_per_token, uint256 rewards, uint256 delta_stake, uint256 next_delta_stake, uint64 delta_epoch, uint64 next_delta_epoch)
Parameters
val_id
- id of the validatordelegator
- address of the delegator about whose stake we are inquiring
Gas cost
184,900
Behavior
This returns the delegator’s DelInfo
for the specified validator,
providing a view of the delegator’s stake, accumulated rewards and pending changes in stake.
getWithdrawalRequest
Function selector
getWithdrawalRequest(uint64,address,uint8) : 0x56fa2045
Function signature
function getWithdrawalRequest( uint64 val_id, address delegator, uint8 withdraw_id) external returns ( uint256 withdrawal_amount, uint256 acc_reward_per_token, uint64 withdrawal_epoch)
Gas cost
24,300
Behavior
This returns the pending WithdrawalRequest
for the (val_id, delegator, withdraw_id)
tuple.
get*ValidatorSet
Function selectors
getConsensusValidatorSet(uint32) : 0xfb29b729getSnapshotValidatorSet(uint32) : 0xde66a368getExecutionValidatorSet(uint32) : 0x7cb074df
Function signatures
getConsensusValidatorSet( uint32 start_index) returns (bool done, uint32 next_index, uint64[] valids)
getSnapshotValidatorSet( uint32 start_index) returns (bool done, uint32 next_index, uint64[] valids)
getExecutionValidatorSet( uint32 start_index) returns (bool done, uint32 next_index, uint64[] valids)
Parameters
start_index
- since the list being looked up is potentially very long, each of these functions is paginated, returning a fixed-length subset of the desired list. Passstart_index
to indicate where in the list to start.
Gas cost
814,000 gas (assuming PAGINATED_RESULTS_SIZE = 100
).
Behavior
These functions return the consensus, snapshot, and execution validator ids, respectively.
Each call retrieves up to PAGINATED_RESULTS_SIZE
validator ids starting from start_index
and
returns a tuple (bool done, uint32 next_index, uint256[] valids)
.
The bool done
indicates whether the end of the list was reached. The uint32 next_index
is the last slot in the array.
getDelegations
Function selector
getDelegations(address,uint64) : 0x4fd66050
Function signature
function getDelegations( address delegator, uint64 start_val_id) returns (bool done, uint64 next_val_id, uint64[] vals_page)
Parameters
delegator
- the address whose delegations we want to look upstart_val_id
Gas cost
814,000
Behavior
Each call retrieves up to PAGINATED_RESULTS_SIZE
validator ids starting from start_val_id
and returns a tuple (bool done, uint64 next_val_id, uint64[] vals_page)
with delegation
from the input delegator
address.
The bool done
indicates whether the end of the list was reached.
The uint64 next_val_id
is the id after the last element in vals_page
. Use it as the
start_val_id
for the next call.
If delegator
has delegated to over PAGINATED_RESULTS_SIZE
validator ids,
multiple calls are required (while done
is false).
To capture the full set, the function should be called with start_val_id = 0
.
getDelegators
Function selector
getDelegators(uint64,address) : 0xa0843a26
Function signature
function getDelegators( uint64 val_id, address start_delegator) returns (bool done, address next_del_addr, address[] dels_page)
Parameters
val_id
- the id of the validator for which we want to know the delegatorsstart_delegator
Gas cost
814,000
Behavior
Each call retrieves up to PAGINATED_RESULTS_SIZE
delegator addresses starting from
start_delegator
and returns a tuple (bool done, address next_del_addr, address[] dels_page)
with delegation to the input val_id
.
The bool done
indicates the end of the list was reached.
The next_del_addr
is the address immediately after the last element in dels_page
.
Use it as start_delegator
for the next call.
To capture the full set, the function should be called with start_delegator = 0
.
The number of delegators to a given validator can be very large, so it is recommended to maintain an updated list via the events framework, rather than periodically calling this expensive lookup.
getEpoch
Function selector
getEpoch() : 0x757991a8
Function signature
function getEpoch() external returns(uint64 epoch, in_epoch_delay_period bool)
Gas cost
16,200
Behavior
This function is a handy utility to determine the current epoch and timing within the epoch (before or after the boundary block).
Concretely, the call returns a tuple (uint64 epoch, bool in_epoch_delay_period)
.
If in_epoch_delay_period
is false, the boundary block has not been reached yet
and write operations at that time should be effective for epoch + 1
.
If in_epoch_delay_period
is true, the network is past the boundary block and
and write operations at that time should be effective for epoch + 2
Syscalls
There are currently three syscalls. Users cannot invoke these directly. They are only triggered through special system transactions.
syscallOnEpochChange
Function selector
syscallOnEpochChange(uint64) : 0x1d4e9f02
Function signature
function syscallOnEpochChange(uint64 epoch);
Parameters
epoch
- the new consensus epoch being entered
Behavior
This syscall is triggered at the end of the epoch delay period. It performs the following actions:
- If the validator received a request to change stake in the previous epoch and participated in the previous epoch’s consensus validator set then it saves the corresponding accumulator value
- If any validator was active in the previous epoch but becomes inactive in the current epoch, it also saves their current accumulator value
- Sets the current epoch in state
Pseudocode
uint64 current_epoch = msg.input.epoch;
for i in snapshot_valset: if epoch_acc[i][current_epoch] is not empty: epoch_acc[i][current_epoch].val() = execution_valset[i].acc() if epoch_acc[i][current_epoch + 1] is not empty: epoch_acc[i][current_epoch].val() = execution_valset[i].acc()
in_boundary = false;epoch = current_epoch;
syscallReward
Function selector
syscallReward(address) : 0x791bdcf3
Function signature
function syscallReward(address block_author);
Parameters
block_author
— the address of the validator that produced the block.
Behavior
This syscall is invoked for every block. It rewards the block-producing validator (and their delegators) with the configured block reward:
- If the validator has a nonzero commission, a portion of the reward is allocated to the
validator’s
auth_address
. - The remaining reward is claimable to the validator’s delegators.
Note that the commission is calculated as a percentage of the total block reward.
- Suppose that a validator's personal stake comprises 20% of the total delegation to their validator.
- The commission is set at 10% of total rewards.
Then the validator receives 10% of the total block reward as their commission. The remaining 90% of the reward is distributed to the stake pool. Since the validator owns 20% of the pool, they also receive 20% of that remaining amount.
Pseudocode
uint64 val_id = secp_to_val_id[block_author];DelInfo auth_del = delegator[val_id][val_execution[val_id].auth_address()];uint256 _commission = REWARD * val_execution[val_id].commission / 1e18;uint256 _unclaimed_rewards = REWARD - _commission;
// state updateauth_del.rewards() += _commission;val_execution[val_id].unclaimed_rewards += _unclaimed_rewards;val_execution[val_id].acc += _unclaimed_rewards / val_consensus[val_id].stake();
mint(STAKING_CONTRACT_ADDRESS, REWARD);
syscallSnapshot
Function selector
syscallSnapshot() : 0x157eeb21
Function signature
function syscallSnapshot();
Parameters
(none)
Behavior
This syscall sorts the current execution-layer validator set. It selects the top N
staked
validators as the upcoming consensus validator set. The updated set is stored in state. The
previous consensus set is cleared.
Pseudocode
uint64[] filter_top_n_validators = sort(execution_valset);
for i in snapshot_valset: _val_snapshot[i].stake = 0; _val_snapshot[i].commission = 0;
snapshot_valset = consensus_valset;consensus_valset = filter_top_n_validators;
for i in filter_top_n_validators: _val_consensus[i].stake = val_execution[i].stake; _val_consensus[i].commission = val_execution[i].commission;
Events
The staking precompiles emit standard events that appear in transaction receipts. These events provide indexed information about validator and delegator actions.
ValidatorCreated
event ValidatorCreated( uint64 indexed valId, address indexed auth_delegator);
Emitted when a validator is added via addValidator
.
ValidatorStatusChanged
event ValidatorStatusChanged( uint64 indexed valId, address indexed auth_delegator, uint64 flags);
Emitted during addValidator
, delegate
,
undelegate
, or compound
. if the validator's flags change.
Delegate
event Delegate( uint64 indexed valId, address indexed delegator, uint256 amount, uint64 activationEpoch);
Emitted when delegation amount is increased, i.e. during addValidator
,
delegate
, or compound
.
Undelegate
event Undelegate( uint64 indexed valId, address indexed delegator, uint8 withdrawal_id, uint256 amount, uint64 activationEpoch);
Emitted when a delegator calls undelegate
.
Withdraw
event Withdraw( uint64 indexed valId, address indexed delegator, uint8 withdrawal_id, uint256 amount, uint64 withdrawEpoch);
Emitted when a delegator executes withdraw
successfully.
ClaimRewards
event ClaimRewards( uint256 indexed valId, address indexed delegatorAddress uint256 amount);
Emitted when a delegator claims rewards via claimRewards
.
CommissionChanged
event CommissionChanged( uint256 indexed valId, uint256 oldCommission uint256 newCommission);
Emitted when a validator changes commission via changeCommission
.
Precompile Internals
Constants
// Minimum stake required from validator's own account// to be eligible to join the valset, in Monad weiuint256 MIN_VALIDATE_STAKE;
// Min stake required (including delegation) for validator// to be eligible to join the valset, in Monad wei.// note that ACTIVE_VALIDATOR_STAKE > MIN_VALIDATE_STAKEuint256 ACTIVE_VALIDATOR_STAKE;
// Block Rewarduint256 REWARD;
// Accumulator unit multiplier. Chosen to preserve accuracyuint256 ACCUMULATOR_DENOMINATOR = 1e36;
// Staking precompile addressAddress STAKING_CONTRACT_ADDRESS = 0x0000000000000000000000000000000000001000;
// Withdrawal delay, needed to facilitate slashinguint8 WITHDRAWAL_DELAY = 1;
// Controls the maximum number of results returned by individual// calls to valset-getters, get_delegators, and get_delegationsuint64 PAGINATED_RESULTS_SIZE = 100;
Validator structs
struct KeysPacked // A validator's consensus keys{ bytes33 secp_pubkey; bytes48 bls_pubkey;};
struct ValExecution // Realtime execution state for one validator{ uint256 stake; // Upcoming stake pool balance uint256 acc; // Current accumulator value for validator uint256 commission; // Proportion of block reward charged as commission, times 1e18; 10% = 1e17 KeysPacked keys; // Consensus keys uint256 address_flags; // Flags to represent validators' current state uint256 unclaimed_rewards; // Unclaimed rewards address auth_address; // Delegator address with authority over validator stake}
struct ValConsensus // State tracked by the consensus system{ uint256 stake; // Current active stake KeysPacked keys; // Consensus keys uint256 commission; // Commission rate for current epoch}
Delegator structs
struct DelInfo{ uint256 stake; // Current active stake uint256 acc; // Last checked accumulator uint256 rewards; // Last checked rewards uint256 delta_stake; // Stake to be activated next epoch uint256 next_delta_stake; // Stake to be activated in 2 epochs uint64 delta_epoch; // Epoch when delta_stake becomes active uint64 next_delta_epoch; // Epoch when next_delta_stake becomes active}
struct WithdrawalRequest{ uint256 amount; // Amount to undelegate from validator uint256 acc; // Validator accumulator when undelegate was called uint64 epoch; // Epoch when undelegate stake deactivates};
struct Accumulator{ uint256 val; // Current accumulator value uint256 refcount; // Reference count for this accumulator value};
State variables
// Current consensus epochuint64 epoch;
// Flag indicating if currently in epoch delay periodbool in_boundary;
// Counter for validator idsuint64 last_val_id;
// Current execution view of validator setStorageArray<uint64> execution_valset;
// Previous consensus view of validator setStorageArray<uint64> snapshot_valset;
// Current consensus view of validator setStorageArray<uint64> consensus_valset;
Mappings
//These mappings only exist to ensure the SECP/BLS Keys are uniquemapping (secp_eth_address => uint64) secp_to_val_id;mapping (bls_eth_address => uint64) bls_to_val_id;
// Keys(val_id, epoch) => Value(acc)// making note of the validator accumulator at start of epoch.mapping(uint64 => mapping(uint64 => Accumulator)) epoch_acc;
// Key(val_id)mapping(uint64 => ValExecution) val_execution;
// Key(val_id)mapping(uint64 => ValConsensus) _val_consensus;
// Key(val_id)mapping(uint64 => ValConsensus) _val_snapshot;
// Keys(val_id,msg.sender) => DelInfomapping(uint64 => mapping(address => DelInfo)) delegator;
// Keys(val_id,msg.sender,withdrawal_id) => WithdrawalRequestmapping(uint64 => mapping(address => mapping (uint8 => WithdrawalRequest))) withdrawal;
FAQ
Is there a removeValidator
function?
There is no direct removeValidator
function. Instead, if a validator’s auth_account
removes
enough stake through undelegate
, the validator is removed from the consensus set
in a future epoch.
This occurs in either epoch n+1
or epoch n+2
, depending on whether the undelegate
occurred within the epoch delay period.
Even when not active, a validator’s information is always retained. Validator ids are permanent since
other delegators may still be delegating and need to reference that val_id
to undelegate/withdraw.
How does a validator change their commission
?
A validator can change their commission
by calling changeCommission
.
What methods give visibility into the list of validator ids?
See the valset getters.
What methods give visibility into a validator's state?
See getValidator
.
What methods give visibility into a delegator's delegation to one particular validator?
See getDelegator
.
For pending withdrawals by that delegator from that validator, see getWithdrawalRequest
.