Skip to main content
This page is a technical reference for the staking precompile interface. For a developer-oriented guide to common workflows, visit the overview.

Address

ContractAddress
Staking Precompile

Functions and selectors

Syscall functionSelector
syscallOnEpochChange(uint64)0x1d4e9f02
syscallReward(address)0x791bdcf3
syscallSnapshot()0x157eeb21

Constants

ConstantMeaningValue
BOUNDARY_BLOCK_PERIODBlocks from boundary block to boundary block50,000 blocks
EPOCH_DELAY_ROUNDSRounds between the boundary block and the start of each epoch5,000 rounds
WITHDRAWAL_DELAYNumber of epochs before unstaked tokens can be withdrawn1 epoch
MIN_AUTH_ADDRESS_STAKEMin MON self-delegated by a validator for active set eligibility100,000 MON
ACTIVE_VALIDATOR_STAKEMin total MON staked with a validator for active set eligibility10,000,000 MON
ACTIVE_VALSET_SIZENumber of validators in the active set200
REWARDMON reward per block25 MON
DUST_THRESHOLDMinimum amount per individual delegation1 gwei (1e9 wei)
MAX_COMMISSIONMaximum commission rate100% (1e18)
ACCUMULATOR_DENOMINATORAccumulator unit multiplier for accuracy1e36
PAGINATED_RESULTS_SIZEMax results returned per paginated call100

State-modifying functions

addValidator

Creates a validator with an associated delegator account and returns the resultant validatorId. 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
  1. payload - consists of the following fields, packed together in big endian (equivalent to abi.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 equal msg.value)
    • uint256 commission (commission charged to delegators multiplied by 1e18, e.g. 10% = 1e17)
  2. signedSecpMessage - SECP signature over payload
  3. signedBlsMessage - BLS signature over payload
Gas cost 505,125 Behavior The function 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 and signedBlsMessage) must be valid and must sign over the payload.
  • Multiple validators may share the same authAddress.
  • msg.value must be equal or greater than MIN_AUTH_ADDRESS_STAKE or the call will revert.
  • If the msg.value is also equal or greater than ACTIVE_VALIDATOR_STAKE then the validator will become active in the future:
    • If addValidator was called before the boundary block, then in epoch n+1;
    • Otherwise it will become active in epoch n+2.
secp_pubkey, bls_pubkey, auth_address, amount, commission = payload

assert amount == msg.value

// increment validator id
last_val_id = last_val_id + 1;

// set uniqueness of keys
secp_to_val_id[secp_eth_address] = last_val_id;
bls_to_val_id[bls_eth_address] = last_val_id;

// set validator info
val_execution[last_val_id] = ValExecution{
    uint256 stake = msg.value;
    uint256 commission = commission;
    bytes secp_pubkey = secp_pubkey;
    bytes bls_pubkey = bls_pubkey;
    uint256 address_flags = set_flags();
}

// set authority delegator info
delegator[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 accumulator
epoch_acc[last_val_id][getEpoch()] = Accumulator{
    uint256 ref_count += 1;
}

// set flags
set_flags();

// push validator id
if (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_AUTH_ADDRESS_STAKE
        return ValidatorFlagsStakeTooLow;

def set_stake():
    if in_epoch_delay_rounds:
        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

Creates a delegator account if it does not exist and increases the delegator’s balance. Function selector
delegate(uint64) : 0x84994fec
Function signature
function delegate(
    uint64 validatorId
) external payable returns (bool success);
Parameters
  1. validatorId - id of the validator that delegator would like to delegate to
  2. msg.value - the amount to delegate
Gas cost 260,850 Behavior
  • The delegator account is determined by msg.sender.
  • validatorId must correspond to a valid validator.
  • msg.value must be >= DUST_THRESHOLD.
  • If this delegation causes the validator’s total stake to exceed ACTIVE_VALIDATOR_STAKE, then the validator will be added to execution_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
validator_id = msg.input.val_id;

// set validator information
val_execution[validator_id] =  ValExecution{
    uint256 stake += msg.value();
}

// set delegator information
DelInfo 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 accumulator
epoch_acc[validator_id][getEpoch()].ref_count += 1;

// set flags
set_flags();

// push validator id
if 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_epoch_delay_rounds:
        _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

Deducts amount from the delegator account and moves it to a withdrawal request object, where it remains in a pending state for WITHDRAWAL_DELAY epochs before the funds are claimable via the withdraw function. Function selector
undelegate(uint64,uint256,uint8) : 0x5cf41514
Function signature
function undelegate(
    uint64 validatorId,
    uint256 amount,
    uint8 withdrawId
) external returns (bool success);
Parameters
  1. validatorId - id of the validator to which sender previously delegated, from which we are removing delegation
  2. amount - amount to undelegate, in Monad wei
  3. withdrawId - integer between 0 and 255, inclusive, which serves as the identifier for a delegator’s withdrawal. For each (validator, delegator) tuple, there can be a maximum of 256 in-flight withdrawal requests
Gas cost 147,750 Behavior
  • 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 below ACTIVE_VALIDATOR_STAKE, then the validator is scheduled to be removed from the valset.
  • If the authAddress on a validator undelegates enough of their own stake to drop below MIN_AUTH_ADDRESS_STAKE, then the validator is scheduled to be removed from the valset.
  • The function will revert if there is a pending withdrawal with the same withdrawId. withdrawIds can be reused after calling withdraw.
  • 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
  • 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
timeline of undelegation and withdrawal

Timeline of withdrawability of stake relative to undelegate command

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 information
current_validator =  ValExecution{
    uint256 stake -= amount;
}

// apply get_current_stake() first.
get_current_stake();

DelInfo current_delegator = delegator[validator_id][msg.sender];
// set delegator information
current_delegator = DelInfo{
    uint256 stake -= amount;
}

// set withdraw request
withdrawal[validator_id][msg.sender][withdraw_id] = WithdrawalRequest{
    uint256 amount = amount;
    uint256 acc = current_validator.acc();
    uint64 epoch = getEpoch();
};

// set epoch accumulator
epoch_acc[validator_id][getEpoch()].ref_count += 1;

// schedule validator to leave set
if current_validator.stake
 < ACTIVE_VALIDATOR_STAKE and validator_id in execution_valset:
    current_validator.set_flag(INSUFFICIENT_STAKE);

if (current_delegator.stake <= MIN_AUTH_ADDRESS_STAKE and validator_id in execution_valset) and msg.sender == current_validator.auth_address:
    current_validator.set_flag(INSUFFICIENT_VALIDATOR_STAKE);

withdraw

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. Function selector
withdraw(uint64,uint8) : 0xaed2ee73
Function signature
function withdraw(
    uint64 validatorId,
    uint8 withdrawId
) external returns (bool success);
Parameters
  1. validatorId - id of the validator to which sender previously delegated, from which we previously issued an undelegate command
  2. withdrawId - identifier for a delegator’s previously created withdrawal; the same id previously supplied to undelegate. For each (validator, delegator) tuple, there can be a maximum of 256 in-flight withdrawal requests.
Gas cost 68,675 Behavior
  • The delegator is msg.sender. The withdrawal is identified by msg.sender, validatorId, and withdrawId
  • 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
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 delegator
transfer(msg.sender, current_withdraw.amount + get_withdraw_rewards());

// unset withdraw request
withdrawal[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());

compound

Converts the delegator’s accumulated rewards into additional stake. Function selector
compound(uint64) : 0xb34fea67
Function signature
function compound(
    uint64 validatorId
) external returns (bool success);
Parameters
  1. validatorId - id of the validator to which sender previously delegated, for which we are compounding rewards
Gas cost 289,325 Behavior
  • 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.
validator_id = msg.input.val_id;

// set delegator information
DelInfo 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 information
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];
    uint256 rewards = 0;
}

// set validator information
val_execution[validator_id] = ValExecution{
    uint256 stake += rewards_compounded;
}

// set accumulator
epoch_acc[validator_id][getEpoch()] = Accumulator{
    uint256 ref_count += 1;
}

// set flags
set_flags();

// push validator id
if val_execution[validator_id].stake() >= ACTIVE_VALIDATOR_STAKE and validator_id not in execution_valset:
    execution_valset.push(validator_id);

claimRewards

Allows a delegator to claim any rewards instead of compounding them. Function selector
claimRewards(uint64) : 0xa76e2ca5
Function signature
function claimRewards(
    uint64 validatorId
) external returns (bool success);
Parameters
  1. validatorId - id of the validator to which sender previously delegated, for which we are claiming rewards
Gas cost 155,375 Behavior
  • validatorId must correspond to a valid validator to which the sender previously delegated
  • If delegator account does not exist for this (validatorId, msg.sender) tuple, then the call reverts
  • The delegator’s accumulated rewards are transferred to their delegation
// set delegator information
DelInfo current_delegator = delegator[validator_id][msg.sender];

// apply get_current_stake() first.
uint256 current_rewards = get_current_stake();

// set delegator information
current_delegator = DelInfo{
    uint256 rewards = 0;
)

// send rewards to delegator
transfer(msg.sender, current_rewards);

changeCommission

Allows the authAddress for a validator to modify the commission for the validator. Function selector
changeCommission(uint64,uint256) : 0x9bdcc3c8
Function signature
function changeCommission(
    uint64 validatorId,
    uint256 commission
) external returns (bool success);
Parameters
  1. validatorId - id of the validator, who would like to change their commission rate
  2. commission - commission rate taken from block rewards, expressed in 1e18 units (e.g., 10% = 1e17)
Gas cost 39,475 Behavior
  • The msg.sender must be the authAddress 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 rounds.
    • in epoch n+2 if request is in the epoch delay rounds.
validator_id = msg.input.val_id;


val_execution[validator_id] = ValExecution{
    uint256 commission = msg.input.commission;
}


externalReward

Allows anyone to send extra MON to the stakers of a particular validator, typically called by the validator themselves to share extra tips to their delegators. Function selector
externalReward(uint64) : 0xe4b3303b
Function signature
function externalReward(
    uint64 validatorId
) external payable returns (bool success);
Parameters
  1. validatorId - id of the validator
  2. msg.value - the MON to add to unclaimed rewards
Gas cost 66,575 Behavior
  • This can only be called for a validator currently in the consensus validator set; otherwise the transaction reverts.
  • msg.value must be between 1 MON and 1,000,000 MON; otherwise the transaction reverts.
  • Commission is not deducted from this and diverted to the validator’s auth_address. If you wish for a portion to be deducted, it should be deducted before sending.
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();

View functions

Because only CALLs are allowed to the staking precompile, all view functions are given the default nonpayable state mutability.

getValidator

Returns a complete view of the validator’s state across execution, consensus, and snapshot contexts.
  • ValExecution (execution view)
  • Stake and commission (consensus view)
  • Stake and commission (snapshot view)
Function selector
getValidator(uint64) : 0x2b6d639a
Function signature
function getValidator(
    uint64 validatorId
) external returns (
    address authAddress,
    uint64 flags,
    uint256 stake,
    uint256 accRewardPerToken,
    uint256 commission,
    uint256 unclaimedRewards,
    uint256 consensusStake,
    uint256 consensusCommission,
    uint256 snapshotStake,
    uint256 snapshotCommission,
    bytes memory secpPubkey,
    bytes memory blsPubkey
);
Parameters
  1. validatorId - id of the validator
Gas cost 97,200

getDelegator

Returns the delegator’s DelInfo for the specified validator, providing a view of the delegator’s stake, accumulated rewards, and pending changes in stake. Function selector
getDelegator(uint64,address) : 0x573c1ce0
Function signature
function getDelegator(
    uint64 validatorId,
    address delegator
) external returns (
    uint256 stake,
    uint256 accRewardPerToken,
    uint256 unclaimedRewards,
    uint256 deltaStake,
    uint256 nextDeltaStake,
    uint64 deltaEpoch,
    uint64 nextDeltaEpoch
);
Parameters
  1. validatorId - id of the validator
  2. delegator - address of the delegator about whose stake we are inquiring
Gas cost 184,900

getWithdrawalRequest

Returns the pending WithdrawalRequest for the (validatorId, delegator, withdrawId) tuple. Function selector
getWithdrawalRequest(uint64,address,uint8) : 0x56fa2045
Function signature
function getWithdrawalRequest(
    uint64 validatorId,
    address delegator,
    uint8 withdrawId
) external returns (
    uint256 withdrawalAmount,
    uint256 accRewardPerToken,
    uint64 withdrawEpoch
);
Gas cost 24,300

get*ValidatorSet

Returns the consensus, snapshot, and execution validator IDs, respectively. Function selectors
getConsensusValidatorSet(uint32) : 0xfb29b729
getSnapshotValidatorSet(uint32) : 0xde66a368
getExecutionValidatorSet(uint32) : 0x7cb074df
Function signatures
function getConsensusValidatorSet(
    uint32 startIndex
) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);

function getSnapshotValidatorSet(
    uint32 startIndex
) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);

function getExecutionValidatorSet(
    uint32 startIndex
) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);
Parameters
  1. startIndex - 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. Pass startIndex to indicate where in the list to start.
Gas cost 814,000 gas (assuming PAGINATED_RESULTS_SIZE = 100). Behavior getExecutionValidatorSet() returns the entire active validator set of all validators that meet the staking criteria to be considered for inclusion. getSnapshotValidatorSet() returns the subset of validators that have been chosen to be leaders in the next epoch. getConsensusValidatorSet() returns the subset of validators that are leaders in the current epoch. Each call retrieves up to PAGINATED_RESULTS_SIZE validator IDs starting from startIndex and returns a tuple (bool done, uint32 nextIndex, uint256[] valids). The bool isDone indicates whether the end of the list was reached. The uint32 nextIndex is the last slot in the array.

getDelegations

Returns a paginated list of validator IDs that a given delegator address has delegated to. Function selector
getDelegations(address,uint64) : 0x4fd66050
Function signature
function getDelegations(
    address delegator,
    uint64 startValId
) external returns (bool isDone, uint64 nextValId, uint64[] memory valIds);
Parameters
  1. delegator - the address whose delegations we want to look up
  2. startValId
Gas cost 814,000 Behavior Each call retrieves up to PAGINATED_RESULTS_SIZE validator ids starting from startValId and returns a tuple (bool isDone, uint64 nextValId, uint64[] valIds) with delegation from the input delegator address. The bool isdone indicates whether the end of the list was reached. The uint64 nextValId is the id after the last element in valIds. Use it as the startValId for the next call. If delegator has delegated to over PAGINATED_RESULTS_SIZE validator ids, multiple calls are required (while isDone is false). To capture the full set, make the first function call using startValId = 0.

getDelegators

Returns a paginated list of delegator addresses for a given validator. Function selector
getDelegators(uint64,address) : 0xa0843a26
Function signature
function getDelegators(
    uint64 validatorId,
    address startDelegator
) external returns (bool isDone, address nextDelegator, address[] memory delegators);
Parameters
  1. validatorId - the id of the validator for which we want to know the delegators
  2. startDelegator
Gas cost 814,000 Behavior Each call retrieves up to PAGINATED_RESULTS_SIZE delegator addresses starting from startDelegator and returns a tuple (bool isDone, address nextDelegator, address[] delegators) with delegation to the input validatorId. The bool isDone indicates the end of the list was reached. The nextDelegator is the address immediately after the last element in delegators. Use it as startDelegator for the next call. To capture the full set, the function should be called with startDelegator = 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

Returns the current epoch and timing within the epoch (before or after the boundary block). Function selector
getEpoch() : 0x757991a8
Function signature
function getEpoch() external returns (uint64 epoch, bool inEpochDelayPeriod);
Gas cost 200 Behavior If inEpochDelayPeriod is false, the boundary block has not been reached yet and write operations at that time should be effective for epoch + 1. If inEpochDelayPeriod is true, the network is past the boundary block and and write operations at that time should be effective for epoch + 2

getProposerValId

Returns the validator ID of the current block proposer, corresponding to the SECP value of the block author. Function selector
getProposerValId() : 0xfbacb0be
Function signature
function getProposerValId() external returns (uint64 val_id);
Gas cost 100

Syscalls

There are currently three syscalls. Users cannot invoke these directly. They are only triggered through special system transactions.

syscallOnEpochChange

Triggered at the end of the epoch delay rounds to finalize accumulator values and update epoch state. Function selector
syscallOnEpochChange(uint64) : 0x1d4e9f02
Function signature
function syscallOnEpochChange(uint64 epoch) external;
Parameters
  1. epoch - the new consensus epoch being entered
Behavior
  1. 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
  2. If any validator was active in the previous epoch but becomes inactive in the current epoch, it also saves their current accumulator value
  3. Sets the current epoch in state
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_epoch_delay_rounds = false;
epoch = current_epoch;

syscallReward

Rewards the block-producing validator and their delegators with the configured block reward for every block. Function selector
syscallReward(address) : 0x791bdcf3
Function signature
function syscallReward(address blockAuthor) external;
Parameters
  1. blockAuthor — the address of the validator that produced the block.
Behavior
  1. If the validator has a nonzero commission, a portion of the reward is allocated to the validator’s authAddress.
  2. 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.
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 update
auth_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

Sorts the current execution-layer validator set, selects the top N staked validators as the upcoming consensus validator set, stores the updated set in state, and clears the previous consensus set. Function selector
syscallSnapshot() : 0x157eeb21
Function signature
function syscallSnapshot() external;
Behavior

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.

ValidatorRewarded

Emitted when block reward is allocated via syscallReward.
event ValidatorRewarded(
        uint64 indexed validatorId,
        address indexed from,
        uint256 amount,
        uint64 epoch
);

ValidatorCreated

Emitted when a validator is added via addValidator.
event ValidatorCreated(
    uint64  indexed validatorId,
    address indexed authAddress,
    uint256 commission
);

ValidatorStatusChanged

Emitted during addValidator, delegate, undelegate, or compound. if the validator’s flags change.
event ValidatorStatusChanged(
    uint64  indexed validatorId,
    uint64  flags
);

Delegate

Emitted when delegation amount is increased, i.e. during addValidator, delegate, or compound.
event Delegate(
    uint64  indexed validatorId,
    address indexed delegator,
    uint256 amount,
    uint64  activationEpoch
);

Undelegate

Emitted when a delegator calls undelegate.
event Undelegate(
    uint64  indexed validatorId,
    address indexed delegator,
    uint8   withdrawId,
    uint256 amount,
    uint64  activationEpoch
);

Withdraw

Emitted when a delegator executes withdraw successfully.
event Withdraw(
    uint64 indexed validatorId,
    address indexed delegator,
    uint8   withdrawId,
    uint256 amount,
    uint64  withdrawEpoch
);

ClaimRewards

Emitted when a delegator claims rewards via claimRewards.
event ClaimRewards(
    uint64 indexed validatorId,
    address indexed delegator,
    uint256 amount,
    uint64 epoch
);

CommissionChanged

Emitted when a validator changes commission via changeCommission.
event CommissionChanged(
    uint64 indexed validatorId,
    uint256 oldCommission,
    uint256 newCommission
);

EpochChanged

Emitted when epoch changes via syscallOnEpochChange.
    event EpochChanged(
        uint64 oldEpoch,
        uint64 newEpoch
    );

Precompile internals

Validator structs

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
    bytes   secp_pubkey;        // Secp256k1 public key used by consensus
    bytes   bls_pubkey;         // Bls public key used by consensus
    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             // A subset of validator state for the consensus system
{
    uint256 stake;              // Current active stake
    uint256 commission;         // Commission rate for current epoch
    bytes   secp_pubkey;        // Secp256k1 public key used by consensus
    bytes   bls_pubkey;         // Bls public key used by consensus
}

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 epoch
uint64 epoch;

// Flag indicating if currently in epoch delay rounds
bool in_epoch_delay_rounds;

// Counter for validator ids
uint64 last_val_id;

// Current execution view of validator set
StorageArray<uint64> execution_valset;

// Previous consensus view of validator set
StorageArray<uint64> snapshot_valset;

// Current consensus view of validator set
StorageArray<uint64> consensus_valset;

Mappings

//These mappings only exist to ensure the SECP/BLS Keys are unique
mapping (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)
// Contains the validator info for the execution view. Changes to stake
// or commission are reflected immediately.
mapping(uint64 => ValExecution) val_execution;

// Key(val_id)
// Contains a subset of the validator info relevant to consensus. Changes to
// stake or commission are reflected in the following epoch. This is referenced
// by the reward system call *before* the epoch delay rounds.
mapping(uint64 => ValConsensus) val_consensus;

// Key(val_id)
// Contains a subset of the validator info relevant to consensus. Changes to
// stake or commission are reflected in the following epoch. This is referenced
// by the reward system call *during* the epoch delay rounds.
mapping(uint64 => ValConsensus) val_snapshot;

// Keys(val_id,msg.sender) => DelInfo
mapping(uint64 => mapping(address => DelInfo)) delegator;

// Keys(val_id,msg.sender,withdrawal_id) => WithdrawalRequest
mapping(uint64 => mapping(address => mapping (uint8 => WithdrawalRequest))) withdrawal;

Solidity Staking Interface

To copy to clipboard, click the button in the top right of the code block.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

interface IMonadStaking {
    function addValidator(
        bytes calldata payload,
        bytes calldata signedSecpMessage,
        bytes calldata signedBlsMessage
    ) external payable returns (uint64 validatorId);

    function delegate(
        uint64 validatorId
    ) external payable returns (bool success);

    function undelegate(
        uint64 validatorId,
        uint256 amount,
        uint8 withdrawId
    ) external returns (bool success);

    function compound(
        uint64 validatorId
    ) external returns (bool success);

    function withdraw(
        uint64 validatorId,
        uint8 withdrawId
    ) external returns (bool success);

    function claimRewards(
        uint64 validatorId
    ) external returns (bool success);

    function changeCommission(
        uint64 validatorId,
        uint256 commission
    ) external returns (bool success);

    function externalReward(
        uint64 validatorId
    ) external payable returns (bool success);


    function getValidator(
        uint64 validatorId
    ) external returns (
        address authAddress,
        uint64 flags,
        uint256 stake,
        uint256 accRewardPerToken,
        uint256 commission,
        uint256 unclaimedRewards,
        uint256 consensusStake,
        uint256 consensusCommission,
        uint256 snapshotStake,
        uint256 snapshotCommission,
        bytes memory secpPubkey,
        bytes memory blsPubkey
    );

    function getDelegator(
        uint64 validatorId,
        address delegator
    ) external returns (
        uint256 stake,
        uint256 accRewardPerToken,
        uint256 unclaimedRewards,
        uint256 deltaStake,
        uint256 nextDeltaStake,
        uint64 deltaEpoch,
        uint64 nextDeltaEpoch
    );

    function getWithdrawalRequest(
        uint64 validatorId,
        address delegator,
        uint8 withdrawId
    ) external returns (
        uint256 withdrawalAmount,
        uint256 accRewardPerToken,
        uint64 withdrawEpoch
    );

    function getConsensusValidatorSet(
        uint32 startIndex
    ) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);

    function getSnapshotValidatorSet(
        uint32 startIndex
    ) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);

    function getExecutionValidatorSet(
        uint32 startIndex
    ) external returns (bool isDone, uint32 nextIndex, uint64[] memory valIds);

    function getDelegations(
        address delegator,
        uint64 startValId
    ) external returns (bool isDone, uint64 nextValId, uint64[] memory valIds);

    function getDelegators(
        uint64 validatorId,
        address startDelegator
    ) external returns (bool isDone, address nextDelegator, address[] memory delegators);

    function getEpoch() external returns (uint64 epoch, bool inEpochDelayPeriod);

    function getProposerValId() external returns (uint64 val_id);

    function syscallOnEpochChange(uint64 epoch) external;

    function syscallReward(address blockAuthor) external;

    function syscallSnapshot() external;

     event ValidatorRewarded(
        uint64 indexed validatorId,
        address indexed from,
        uint256 amount,
        uint64 epoch
    );
    event ValidatorCreated(
        uint64  indexed validatorId,
        address indexed authAddress,
        uint256 commission

    );
    event ValidatorStatusChanged(
        uint64  indexed validatorId,
        uint64  flags
    );
    event Delegate(
        uint64  indexed validatorId,
        address indexed delegator,
        uint256 amount,
        uint64  activationEpoch
    );
    event Undelegate(
        uint64  indexed validatorId,
        address indexed delegator,
        uint8   withdrawId,
        uint256 amount,
        uint64  activationEpoch
    );
    event Withdraw(
        uint64 indexed validatorId,
        address indexed delegator,
        uint8   withdrawId,
        uint256 amount,
        uint64  withdrawEpoch
    );
    event ClaimRewards(
        uint64 indexed validatorId,
        address indexed delegator,
        uint256 amount,
        uint64  epoch
    );
    event CommissionChanged(
        uint64 indexed validatorId,
        uint256 oldCommission,
        uint256 newCommission
    );
    event EpochChanged(
        uint64 oldEpoch,
        uint64 newEpoch
    );
}

Staking ABI JSON

To copy to clipboard, click the button in the top right of the code block.
[
  {"type":"function","name":"addValidator","inputs":[{"name":"payload","type":"bytes","internalType":"bytes"},{"name":"signedSecpMessage","type":"bytes","internalType":"bytes"},{"name":"signedBlsMessage","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"stateMutability":"payable"},
  {"type":"function","name":"changeCommission","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"commission","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"claimRewards","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"compound","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"delegate","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"payable"},
  {"type":"function","name":"externalReward","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"payable"},
  {"type":"function","name":"getConsensusValidatorSet","inputs":[{"name":"startIndex","type":"uint32","internalType":"uint32"}],"outputs":[{"name":"isDone","type":"bool","internalType":"bool"},{"name":"nextIndex","type":"uint32","internalType":"uint32"},{"name":"valIds","type":"uint64[]","internalType":"uint64[]"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getDelegations","inputs":[{"name":"delegator","type":"address","internalType":"address"},{"name":"startValId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"isDone","type":"bool","internalType":"bool"},{"name":"nextValId","type":"uint64","internalType":"uint64"},{"name":"valIds","type":"uint64[]","internalType":"uint64[]"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getDelegator","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"delegator","type":"address","internalType":"address"}],"outputs":[{"name":"stake","type":"uint256","internalType":"uint256"},{"name":"accRewardPerToken","type":"uint256","internalType":"uint256"},{"name":"unclaimedRewards","type":"uint256","internalType":"uint256"},{"name":"deltaStake","type":"uint256","internalType":"uint256"},{"name":"nextDeltaStake","type":"uint256","internalType":"uint256"},{"name":"deltaEpoch","type":"uint64","internalType":"uint64"},{"name":"nextDeltaEpoch","type":"uint64","internalType":"uint64"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getDelegators","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"startDelegator","type":"address","internalType":"address"}],"outputs":[{"name":"isDone","type":"bool","internalType":"bool"},{"name":"nextDelegator","type":"address","internalType":"address"},{"name":"delegators","type":"address[]","internalType":"address[]"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getEpoch","inputs":[],"outputs":[{"name":"epoch","type":"uint64","internalType":"uint64"},{"name":"inEpochDelayPeriod","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getProposerValId","inputs":[],"outputs":[{"name":"val_id","type":"uint64","internalType": "uint64"}],
  "stateMutability":"nonpayable"},
  {"type":"function","name":"getExecutionValidatorSet","inputs":[{"name":"startIndex","type":"uint32","internalType":"uint32"}],"outputs":[{"name":"isDone","type":"bool","internalType":"bool"},{"name":"nextIndex","type":"uint32","internalType":"uint32"},{"name":"valIds","type":"uint64[]","internalType":"uint64[]"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getSnapshotValidatorSet","inputs":[{"name":"startIndex","type":"uint32","internalType":"uint32"}],"outputs":[{"name":"isDone","type":"bool","internalType":"bool"},{"name":"nextIndex","type":"uint32","internalType":"uint32"},{"name":"valIds","type":"uint64[]","internalType":"uint64[]"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getValidator","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"}],"outputs":[{"name":"authAddress","type":"address","internalType":"address"},{"name":"flags","type":"uint64","internalType":"uint64"},{"name":"stake","type":"uint256","internalType":"uint256"},{"name":"accRewardPerToken","type":"uint256","internalType":"uint256"},{"name":"commission","type":"uint256","internalType":"uint256"},{"name":"unclaimedRewards","type":"uint256","internalType":"uint256"},{"name":"consensusStake","type":"uint256","internalType":"uint256"},{"name":"consensusCommission","type":"uint256","internalType":"uint256"},{"name":"snapshotStake","type":"uint256","internalType":"uint256"},{"name":"snapshotCommission","type":"uint256","internalType":"uint256"},{"name":"secpPubkey","type":"bytes","internalType":"bytes"},{"name":"blsPubkey","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"getWithdrawalRequest","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"delegator","type":"address","internalType":"address"},{"name":"withdrawId","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"withdrawalAmount","type":"uint256","internalType":"uint256"},{"name":"accRewardPerToken","type":"uint256","internalType":"uint256"},{"name":"withdrawEpoch","type":"uint64","internalType":"uint64"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"syscallOnEpochChange","inputs":[{"name":"epoch","type":"uint64","internalType":"uint64"}],"outputs":[],"stateMutability":"nonpayable"},
  {"type":"function","name":"syscallReward","inputs":[{"name":"blockAuthor","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},
  {"type":"function","name":"syscallSnapshot","inputs":[],"outputs":[],"stateMutability":"nonpayable"},
  {"type":"function","name":"undelegate","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"withdrawId","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"function","name":"withdraw","inputs":[{"name":"validatorId","type":"uint64","internalType":"uint64"},{"name":"withdrawId","type":"uint8","internalType":"uint8"}],"outputs":[{"name":"success","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},
  {"type":"event","name":"ClaimRewards","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"delegator","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"epoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"CommissionChanged","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"oldCommission","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"newCommission","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},
  {"type":"event","name":"Delegate","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"delegator","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"activationEpoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"EpochChanged","inputs":[{"name":"oldEpoch","type":"uint64","indexed":false,"internalType":"uint64"},{"name":"newEpoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"Undelegate","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"delegator","type":"address","indexed":true,"internalType":"address"},{"name":"withdrawId","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"activationEpoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"ValidatorCreated","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"authAddress","type":"address","indexed":true,"internalType":"address"},{"name":"commission","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},
  {"type":"event","name":"ValidatorRewarded","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"epoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"ValidatorStatusChanged","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"flags","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},
  {"type":"event","name":"Withdraw","inputs":[{"name":"validatorId","type":"uint64","indexed":true,"internalType":"uint64"},{"name":"delegator","type":"address","indexed":true,"internalType":"address"},{"name":"withdrawId","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"withdrawEpoch","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false}
]

FAQ

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 rounds.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.
A validator can change their commission by calling changeCommission.
See getDelegator.For pending withdrawals by that delegator from that validator, see getWithdrawalRequest.
Despite using Solidity selectors and ABI, it is a precompile. Accessing code at returns empty code. Its account is always accessed warm, and calls with invalid arguments consume all gas.Exception: if an account attempts to delegate to the staking precompile using EIP-7702, all calls to it will revert.