monad-revm extends revm with Monad-specific execution semantics: gas model changes, repriced precompiles, and full staking precompile support.
| Component | Version |
|---|---|
| revm | v34.0.0 |
| Monad spec | MONAD_EIGHT (Prague-compatible baseline) |
Monad uses a different cold-access model and no gas refunds.
| Access Type | Ethereum | Monad |
|---|---|---|
Cold storage (SLOAD) |
2,100 | 8,100 |
Cold account (BALANCE, EXTCODE*, CALL*) |
2,600 | 10,100 |
| Warm access | 100 | 100 |
| Precompile | Address | Ethereum | Monad | Multiplier |
|---|---|---|---|---|
ecRecover |
0x01 |
3,000 | 6,000 | 2x |
ecAdd |
0x06 |
150 | 300 | 2x |
ecMul |
0x07 |
6,000 | 30,000 | 5x |
ecPairing |
0x08 |
45,000 + 34,000/pt | 225,000 + 170,000/pt | 5x |
blake2f |
0x09 |
rounds × 1 | rounds × 2 | 2x |
| KZG point evaluation | 0x0a |
50,000 | 200,000 | 4x |
| Rule | Ethereum | Monad |
|---|---|---|
| Runtime bytecode limit | 24KB | 128KB |
| Initcode limit | 48KB | 256KB |
| EIP-4844 blob tx | Supported | Rejected (Eip4844NotSupported) |
Monad staking uses three validator sets and two reward views to keep consensus transitions deterministic:
executionset: real-time set updated by delegation/undelegation.consensusset: top validators selected at snapshot time.snapshotset: previous consensus image used during boundary-period rewards.
Validator state is split into:
- Execution state (
stake,commission,accumulated_reward_per_token, flags, unclaimed rewards, keys/auth). - Epoch views (
consensus/snapshotstake+commission) used by reward paths.
Delegator state tracks active stake, pending stake windows (delta_stake, next_delta_stake), reward cursor (accRewardPerToken), and linked-list pointers used by getDelegations / getDelegators pagination.
syscallReward(blockAuthor)distributes the per-block reward to the active validator pool.syscallSnapshot()enters boundary mode, copies consensus to snapshot, rebuilds consensus from execution sorted by stake.syscallOnEpochChange(newEpoch)finalizes the transition, updates epoch, and clears boundary mode.
blockAuthor -> validatorId resolution is via ValIdSecp mapping; rewards use consensus view outside boundary and snapshot view during boundary.
Pool rewards use an accumulator model:
acc += reward * UNIT_BIAS / active_stake- Delegator rewards are computed from accumulator deltas.
- Undelegation creates a
WithdrawalRequestwith an accumulator snapshot. (epoch, validator)accumulator snapshots are reference-counted to support delayed withdrawals and epoch-window correctness.
Constants (current MONAD_EIGHT implementation):
ACTIVE_VALIDATOR_STAKE = 10_000_000 MONMIN_AUTH_ADDRESS_STAKE = 100_000 MONWITHDRAWAL_DELAY = 1 epochMIN_EXTERNAL_REWARD = 1e9,MAX_EXTERNAL_REWARD = 1e25ACTIVE_VALSET_SIZE = 200
See implementation constants in src/staking/constants.rs.
| Method | Selector | Gas |
|---|---|---|
getEpoch() |
0x757991a8 |
200 |
getProposerValId() |
0xfbacb0be |
100 |
getValidator(uint64) |
0x2b6d639a |
97,200 |
getDelegator(uint64,address) |
0x573c1ce0 |
184,900 |
getWithdrawalRequest(uint64,address,uint8) |
0x56fa2045 |
24,300 |
getConsensusValidatorSet(uint32) |
0xfb29b729 |
814,000 |
getSnapshotValidatorSet(uint32) |
0xde66a368 |
814,000 |
getExecutionValidatorSet(uint32) |
0x7cb074df |
814,000 |
getDelegations(address,uint64) |
0x4fd66050 |
814,000 |
getDelegators(uint64,address) |
0xa0843a26 |
814,000 |
| Method | Selector | Gas | Payable |
|---|---|---|---|
addValidator(bytes,bytes,bytes) |
0xf145204c |
505,125 |
Yes |
delegate(uint64) |
0x84994fec |
260,850 |
Yes |
undelegate(uint64,uint256,uint8) |
0x5cf41514 |
147,750 |
No |
withdraw(uint64,uint8) |
0xaed2ee73 |
68,675 |
No |
compound(uint64) |
0xb34fea67 |
289,325 |
No |
claimRewards(uint64) |
0xa76e2ca5 |
155,375 |
No |
changeCommission(uint64,uint256) |
0x9bdcc3c8 |
39,475 |
No |
externalReward(uint64) |
0xe4b3303b |
66,575 |
Yes |
| Method | Selector | Gas | Caller requirement |
|---|---|---|---|
syscallReward(address) |
0x791bdcf3 |
100,000 |
SYSTEM_ADDRESS |
syscallSnapshot() |
0x157eeb21 |
500,000 |
SYSTEM_ADDRESS |
syscallOnEpochChange(uint64) |
0x1d4e9f02 |
50,000 |
SYSTEM_ADDRESS |
- Only direct
CALLis accepted.DELEGATECALL,CALLCODE, andSTATICCALLare rejected. - Unknown/short selectors route to fallback (
"method not supported", 40k fallback cost). - Read path is dispatch-first for payability, matching C++ behavior (unknown selector fallback bypasses payability guard).
getDelegatoris intentionally treated as a write selector in canonical execution because it settles delegator state viapull_delegator_up_to_date.
monad-revm tracks C++ staking behavior closely, but there are explicit implementation notes to keep in mind:
- The Rust implementation currently targets
MONAD_EIGHTconstants. addValidatorcurrently skips signature verification and uses simplified key-to-address derivation inwrite.rs. This is intentional in the current implementation and should be considered when writing integration tests.
Core modules:
src/staking/mod.rs: top-level precompile dispatcher (run_staking_precompile) and read handlers.src/staking/write.rs: all user write handlers + syscall handlers + selector/payability logic.src/staking/storage.rs: exact storage key derivation for all staking namespaces.src/staking/types.rs: validator/delegator/withdrawal/list node types.src/staking/interface.rs: ABI definitions and selectors.src/staking/constants.rs: gas-independent staking constants.
Block lifecycle helpers:
src/api/block.rsexposesapply_syscall_reward,apply_syscall_snapshot,apply_syscall_on_epoch_change, andapply_epoch_boundary.syscallRewardsupports extended calldata (selector + blockAuthor + reward) forSystemCallEvmenvironments that cannot attachmsg.valueto system calls.
Reader integration path:
run_staking_with_reader(...)supports environments that do not expose fullContextTr, and is used byalloy-monad-evmintegration.
Add to your Cargo.toml:
[dependencies]
monad-revm = { git = "https://github.com/category-labs/monad-revm", branch = "main" }Or from crates.io:
[dependencies]
monad-revm = "0.1"use monad_revm::{MonadBuilder, DefaultMonad};
use revm::{
context::{Context, TxEnv},
database::InMemoryDB,
ExecuteEvm,
};
let ctx = Context::monad();
let mut evm = ctx.build_monad();
let tx = TxEnv::builder()
.caller(caller_address)
.to(contract_address)
.value(U256::from(1000))
.gas_limit(100_000)
.gas_price(1_000_000_000)
.build_fill();
let result = evm.transact(tx).expect("Transaction failed");use monad_revm::{MonadBuilder, DefaultMonad};
use revm::{context::Context, inspector::NoOpInspector};
let ctx = Context::monad();
let mut evm = ctx.build_monad_with_inspector(NoOpInspector {});use monad_revm::{MonadBuilder, DefaultMonad};
use revm::context::Context;
let db = MyCustomDatabase::new();
let ctx = Context::monad().with_db(db);
let mut evm = ctx.build_monad();monad-revm/
├── crates/
│ └── monad-revm/
│ └── src/
│ ├── lib.rs
│ ├── spec.rs
│ ├── cfg.rs
│ ├── handler.rs
│ ├── instructions.rs
│ ├── precompiles.rs
│ ├── evm.rs
│ ├── api/
│ │ ├── block.rs
│ │ ├── builder.rs
│ │ ├── exec.rs
│ │ └── default_ctx.rs
│ └── staking/
│ ├── mod.rs
│ ├── write.rs
│ ├── abi.rs
│ ├── interface.rs
│ ├── storage.rs
│ └── types.rs
└── Cargo.toml
serde: Enable serialization forMonadSpecId.alloy-evm: Enable integration withalloy_evm::precompiles::PrecompilesMap.
alloy-monad-evm: AlloyEvm/EvmFactorywrapper overmonad-revm.monad-foundry: Foundry integration (Forge/Anvil/Cast/Chisel).
Revm is licensed under MIT License.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in these crates by you, shall be licensed as above, without any additional terms or conditions.