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
)
For ease of integration, please see the Solidity Interface and the ABI JSON.
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, as described in Staking Behavior. This is to allow for the separation of consensus and execution, one of Monad's core design attributes.
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 calldata payload, bytes calldata signedSecpMessage, bytes calldata signedBlsMessage) external payable returns (uint64 validatorId);
Parameters
payload
- consists of the following fields, packed together in big endian (equivalent toabi.encodePacked()
in Solidity):bytes secpPubkey
(unique SECP public key used for consensus)bytes blsPubkey
(unique BLS public key used for consensus)address authAddress
(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
)
signedSecpMessage
- SECP signature over payloadsignedBlsMessage
- BLS signature over payload
Gas cost
505,125
Behavior
This creates a validator with an associated delegator account and returns the resultant validatorId
.
The method starts by unpacking the payload to retrieve the secpPubkey
, blsPubkey
,
authAddress
, amount
, and commission
, then verifying that the signedSecpMessage
and signedBlsMessage
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 (
signedSecpMessage
andsignedBlsMessage
) must be valid and must sign over thepayload
. - Multiple validators may share the same
authAddress
. 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 validatorId) external payable returns (bool success);
Parameters
validatorId
- 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
. validatorId
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 validatorId, uint256 amount, uint8 withdrawId) external returns (bool success);
Parameters
validatorId
- id of the validator to which sender previously delegated, from which we are removing delegationamount
- amount to unstake, in Monad weiwithdrawId
- identifier for a delegator's withdrawal. ThewithdrawId
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
. validatorId
must correspond to a valid validator to which the sender previously delegated- The delegator must have stake >= amount.
- If the withdrawal causes
Val(validatorId).stake()
to drop belowACTIVE_VALIDATOR_STAKE
, then the validator is scheduled to be removed from valset. - If the
authAddress
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 validatorId) external returns (bool success);
Parameters
validatorId
- 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 validatorId
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);