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.
For security, only standard CALL
s are allowed to the staking precompile. In particular,
STATICCALL
and DELEGATECALL
are not allowed.
Precompile Specification
Users take staking-related actions by interacting with a precompile.
First, let's survey the state stored in the precompile. Functions are surveyed in the following section.
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 UNIT_BIAS = 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;
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) payable;
Parameters
payload
- consists of the following fields, packed together in big endian: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
505125
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.
- The
msg.value
must meet one of the following conditions:- If
msg.value >= ACTIVE_VALIDATOR_STAKE
, the validator is added to the validator set but not immediately active. - If
MIN_VALIDATE_STAKE <= msg.value < ACTIVE_VALIDATOR_STAKE
, the validator is registered, but not added to the validator set.
- If
- Both signatures (
signed_secp_message
andsigned_bls_message
) must be valid and must sign over thepayload
. - Multiple validators may share the same
auth_address
. - Submissions with any repeated public keys will revert.
- If the validator meets the
ACTIVE_VALIDATOR_STAKE
threshold, then- If request was before the boundary block, the validator stake and associated
delegator stake become active in epoch
n+1
; - Else, the validator stake and associated delegator stake become active in epoch
n+2
.
- If request was before the boundary block, the validator stake and associated
delegator stake become active in epoch
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) payable;
Parameters
val_id
- id of the validator that delegator would like to delegate tomsg.value
- the amount to delegate
Gas cost
260850
Behavior
This creates a delegator account if it does not exist and increments 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 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 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);
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. For each (validator, delegator) tuple, there can be a maximum of 256 in-flight withdrawal requestsmsg.value
- should be 0; the transaction will revert otherwise
Gas cost
147750
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 a delegator causes
del(auth_address).stake()
to drop belowMIN_VALIDATE_STAKE
, then the validator is scheduled to be removed from valset. - Delegator can only remove stake after it has activated. This is the stake field in the delegator struct.
- 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
- Let
k
represent the withdrawal_delay, then the delegator stake becomes withdrawable and thus no longer subject to slashing- in epoch
n+1+k
if the request is before the boundary block - in epoch
n+2+k
otherwise
- in epoch
- The transaction will revert if
msg.value
is nonzero

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);
Parameters
val_id
- id of the validator to which sender previously delegated, for which we are compounding rewardsmsg.value
- should be 0; the transaction will revert otherwise
Gas cost
285050
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 revert 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
- The transaction will revert if
msg.value
is nonzero
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);
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 withdrawal. For each (validator, delegator) tuple, there can be a maximum of 256 in-flight withdrawal requestsmsg.value
- should be 0; the transaction will revert otherwise
Gas cost
68675
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
- Let
k
represent thewithdrawal_delay
. Then the withdrawal request becomes withdrawable and thus unslashable:- in epoch
n+1+k
if request is not in the epoch delay period since theundelegate
call. - in epoch
n+2+k
if request is in the epoch delay period since theundelegate
call.
- in epoch
- The transaction will revert if
msg.value
is nonzero.
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);
Parameters
val_id
- id of the validator to which sender previously delegated, for which we are claiming rewardsmsg.value
- should be 0; the transaction will revert otherwise
Gas cost
155375
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 transaction will revert if
msg.value
is nonzero - The delegator's accumulated rewards are transferred to their address and reset to zero
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);
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)msg.value
- should be 0; the transaction will revert otherwise
Gas cost
39475
Behavior
This allows (only) the auth_account
to modify the commission of the specified val_id
after the validator has been registered via addValidator
.
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
- The transaction will revert if
msg.value
is nonzero
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);
Parameters
val_id
- id of the validatormsg.value
- the value to add to unclaimed rewards
Gas cost
62300
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