Skip to main content

foundry_evm_core/evm/
monad.rs

1use alloy_evm::{Evm, EvmEnv, EvmFactory};
2use alloy_monad_evm::{MonadEvm, MonadEvmFactory, MonadPrecompilesMap};
3use alloy_sol_types::SolCall;
4use eyre::WrapErr;
5use foundry_fork_db::DatabaseError;
6use monad_revm::{
7    MonadBuilder, MonadCfgEnv, MonadChainContext, MonadContext, MonadEvm as RevmMonadEvm,
8    MonadHardfork, MonadJournal, MonadJournalTr,
9    api::block::{
10        syscall_on_epoch_change_calldata, syscall_reward_calldata, syscall_snapshot_calldata,
11    },
12    handler::MonadHandler,
13    instructions::MonadInstructions,
14    monad_context_with_db,
15    staking::{
16        STAKING_ADDRESS,
17        constants::SYSTEM_ADDRESS,
18        interface::IMonadStaking::{
19            syscallOnEpochChangeCall, syscallRewardCall, syscallSnapshotCall,
20        },
21    },
22};
23use revm::{
24    context::{
25        BlockEnv, ContextTr, LocalContextTr, Transaction, TransactionType, TxEnv,
26        journaled_state::account::JournaledAccountTr,
27        result::{EVMError, ResultAndState},
28    },
29    context_interface::{Cfg, ContextSetters, transaction::AuthorizationTr},
30    handler::{EthFrame, EvmTr, FrameResult, Handler},
31    inspector::{InspectSystemCallEvm, Inspector, InspectorHandler},
32    interpreter::{FrameInput, GasTracker, SharedMemory, interpreter_action::FrameInit},
33    primitives::{Address, Bytes, HashSet, U256},
34};
35
36use crate::{
37    FoundryChain, FoundryContextExt, FoundryInspectorExt, FoundryJournal,
38    backend::{DatabaseExt, JournaledState},
39    evm::{FoundryEvmFactory, NestedEvm, NestedEvmFor},
40};
41
42impl FoundryChain<TxEnv> for MonadChainContext {
43    fn for_transaction(tx: &TxEnv) -> Self {
44        monad_context_from_participants(
45            Default::default(),
46            Default::default(),
47            std::slice::from_ref(tx),
48            0,
49        )
50    }
51
52    fn for_block(
53        grandparent: &[TxEnv],
54        parent: &[TxEnv],
55        current: &[TxEnv],
56        current_tx_index: usize,
57    ) -> Self {
58        monad_context_from_participants(
59            monad_block_participants(grandparent),
60            monad_block_participants(parent),
61            current,
62            current_tx_index,
63        )
64    }
65
66    fn refresh_journal<J: FoundryJournal>(&self, journal: &mut J) {
67        let mut tracker = journal.capture_reserve_balance();
68        tracker.rebase(self, journal.evm_state());
69        journal.restore_reserve_balance(tracker);
70    }
71}
72
73/// Refreshes journal state derived from a nested EVM's active Monad chain position.
74pub fn refresh_nested_chain_journal<E: NestedEvm + ?Sized>(evm: &mut E) {
75    let chain = evm.chain_mut().clone();
76    chain.refresh_journal(evm.journal_mut());
77}
78
79type MonadEvmHandler<'db, I> =
80    MonadHandler<MonadRevmEvm<'db, I>, EVMError<DatabaseError>, EthFrame>;
81
82pub type MonadRevmEvm<'db, I> = RevmMonadEvm<
83    MonadContext<&'db mut dyn DatabaseExt<MonadEvmFactory>>,
84    I,
85    MonadInstructions<MonadContext<&'db mut dyn DatabaseExt<MonadEvmFactory>>>,
86    MonadPrecompilesMap,
87>;
88
89/// Senders and EIP-7702 authorities that participated in one Monad block.
90pub type MonadBlockParticipants = HashSet<Address>;
91
92/// Collects all senders and EIP-7702 authorities from a block's transactions.
93pub fn monad_block_participants(transactions: &[TxEnv]) -> MonadBlockParticipants {
94    transactions
95        .iter()
96        .flat_map(|tx| {
97            std::iter::once(tx.caller())
98                .chain(tx.authorization_list().filter_map(|auth| auth.authority()))
99        })
100        .collect()
101}
102
103/// Builds Monad context from cached ancestor participants and the current block transactions.
104pub fn monad_context_from_participants(
105    grandparent_senders_and_authorities: MonadBlockParticipants,
106    parent_senders_and_authorities: MonadBlockParticipants,
107    current: &[TxEnv],
108    current_tx_index: usize,
109) -> MonadChainContext {
110    MonadChainContext {
111        grandparent_senders_and_authorities,
112        parent_senders_and_authorities,
113        current_block_senders: current.iter().map(Transaction::caller).collect(),
114        current_block_authorities: current
115            .iter()
116            .map(|tx| tx.authorization_list().filter_map(|auth| auth.authority()).collect())
117            .collect(),
118        current_tx_index,
119        ..Default::default()
120    }
121}
122
123/// A canonical Monad protocol system transaction.
124#[derive(Clone, Debug)]
125pub struct ProtocolSystemCall {
126    /// Reserved caller used by the protocol.
127    pub caller: Address,
128    /// Native system contract or precompile being called.
129    pub contract: Address,
130    /// Calldata passed to the dedicated system-call entry point.
131    pub data: Bytes,
132    /// Sender nonce encoded by the canonical envelope.
133    pub nonce: u64,
134    /// Optional EIP-155 chain ID encoded by the canonical envelope.
135    pub chain_id: Option<u64>,
136    /// Optional protocol mint applied before system-call execution.
137    pub balance_increment: Option<(Address, U256)>,
138}
139
140impl ProtocolSystemCall {
141    fn validate_chain_id(&self, chain_id: u64) -> eyre::Result<()> {
142        if let Some(envelope_chain_id) = self.chain_id
143            && envelope_chain_id != chain_id
144        {
145            eyre::bail!(
146                "protocol system transaction chain ID mismatch: envelope {envelope_chain_id}, \
147                 environment {chain_id}"
148            );
149        }
150        Ok(())
151    }
152
153    fn apply_prestate<DB: alloy_evm::Database>(
154        &self,
155        db: &mut DB,
156        journal: &mut JournaledState,
157    ) -> eyre::Result<()> {
158        let next_nonce = self
159            .nonce
160            .checked_add(1)
161            .ok_or_else(|| eyre::eyre!("protocol system transaction nonce overflow"))?;
162        let caller_nonce = journal.load_account(db, self.caller)?.data.info.nonce;
163        if caller_nonce != self.nonce {
164            eyre::bail!(
165                "protocol system transaction nonce mismatch: envelope {}, state {}",
166                self.nonce,
167                caller_nonce
168            );
169        }
170
171        let balance = if let Some((address, amount)) = self.balance_increment {
172            let balance = journal
173                .load_account(db, address)?
174                .data
175                .info
176                .balance
177                .checked_add(amount)
178                .ok_or_else(|| eyre::eyre!("protocol system transaction balance overflow"))?;
179            Some((address, balance))
180        } else {
181            None
182        };
183
184        journal.load_account_mut(db, self.caller)?.data.set_nonce(next_nonce);
185        if let Some((address, balance)) = balance {
186            journal.load_account_mut(db, address)?.data.set_balance(balance);
187        }
188
189        Ok(())
190    }
191}
192
193/// Converts a canonical Monad envelope into its dedicated system call.
194///
195/// Returns an error when the transaction uses Monad's reserved protocol sender but does not
196/// satisfy the canonical envelope rules.
197pub fn protocol_system_call<T: Transaction>(tx: &T) -> eyre::Result<Option<ProtocolSystemCall>> {
198    if tx.caller() != SYSTEM_ADDRESS {
199        return Ok(None);
200    }
201
202    eyre::ensure!(
203        tx.tx_type() == TransactionType::Legacy as u8,
204        "invalid Monad protocol system transaction: transaction type must be legacy"
205    );
206    eyre::ensure!(
207        tx.kind() == revm::primitives::TxKind::Call(STAKING_ADDRESS),
208        "invalid Monad protocol system transaction: target must be the staking contract"
209    );
210    eyre::ensure!(
211        tx.gas_limit() == 0,
212        "invalid Monad protocol system transaction: gas limit must be zero"
213    );
214    eyre::ensure!(
215        tx.gas_price() == 0,
216        "invalid Monad protocol system transaction: gas price must be zero"
217    );
218    eyre::ensure!(
219        tx.max_priority_fee_per_gas().is_none(),
220        "invalid Monad protocol system transaction: priority fee must be absent"
221    );
222    eyre::ensure!(
223        tx.access_list().is_none_or(|mut list| list.next().is_none()),
224        "invalid Monad protocol system transaction: access list must be empty"
225    );
226    eyre::ensure!(
227        tx.blob_versioned_hashes().is_empty(),
228        "invalid Monad protocol system transaction: blob hashes must be empty"
229    );
230    eyre::ensure!(
231        tx.max_fee_per_blob_gas() == 0,
232        "invalid Monad protocol system transaction: blob gas fee must be zero"
233    );
234    eyre::ensure!(
235        tx.authorization_list_len() == 0,
236        "invalid Monad protocol system transaction: authorization list must be empty"
237    );
238
239    let selector: [u8; 4] = tx
240        .input()
241        .get(..4)
242        .ok_or_else(|| {
243            eyre::eyre!(
244                "invalid Monad protocol system transaction: calldata is shorter than a selector"
245            )
246        })?
247        .try_into()
248        .expect("slice has exactly four bytes");
249    let (data, balance_increment) = match selector {
250        syscallRewardCall::SELECTOR => {
251            eyre::ensure!(
252                tx.input().len() == 36,
253                "invalid Monad protocol system transaction: reward calldata must be 36 bytes"
254            );
255            let call = syscallRewardCall::abi_decode_raw(&tx.input()[4..])
256                .wrap_err("invalid Monad protocol system reward calldata")?;
257            eyre::ensure!(
258                call.abi_encode().as_slice() == tx.input(),
259                "invalid Monad protocol system reward calldata"
260            );
261            (
262                syscall_reward_calldata(call.blockAuthor, tx.value()),
263                Some((STAKING_ADDRESS, tx.value())),
264            )
265        }
266        syscallSnapshotCall::SELECTOR => {
267            eyre::ensure!(
268                tx.input().len() == 4,
269                "invalid Monad protocol system transaction: snapshot calldata must be 4 bytes"
270            );
271            eyre::ensure!(
272                tx.value().is_zero(),
273                "invalid Monad protocol system transaction: snapshot value must be zero"
274            );
275            syscallSnapshotCall::abi_decode_raw(&tx.input()[4..])
276                .wrap_err("invalid Monad protocol system snapshot calldata")?;
277            (syscall_snapshot_calldata(), None)
278        }
279        syscallOnEpochChangeCall::SELECTOR => {
280            eyre::ensure!(
281                tx.input().len() == 36,
282                "invalid Monad protocol system transaction: epoch calldata must be 36 bytes"
283            );
284            eyre::ensure!(
285                tx.value().is_zero(),
286                "invalid Monad protocol system transaction: epoch value must be zero"
287            );
288            let call = syscallOnEpochChangeCall::abi_decode_raw(&tx.input()[4..])
289                .wrap_err("invalid Monad protocol system epoch calldata")?;
290            eyre::ensure!(
291                call.abi_encode().as_slice() == tx.input(),
292                "invalid Monad protocol system epoch calldata"
293            );
294            (syscall_on_epoch_change_calldata(call.epoch), None)
295        }
296        _ => {
297            return Err(eyre::eyre!(
298                "invalid Monad protocol system transaction: unknown staking syscall selector"
299            ));
300        }
301    };
302
303    Ok(Some(ProtocolSystemCall {
304        caller: SYSTEM_ADDRESS,
305        contract: STAKING_ADDRESS,
306        data,
307        nonce: tx.nonce(),
308        chain_id: tx.chain_id(),
309        balance_increment,
310    }))
311}
312
313fn finish_protocol_system_call<H>(
314    mut result: ResultAndState<H>,
315) -> eyre::Result<ResultAndState<H>> {
316    if !result.result.is_success() {
317        eyre::bail!("protocol system transaction reverted or halted");
318    }
319
320    if let revm::context_interface::result::ExecutionResult::Success { gas, .. } =
321        &mut result.result
322    {
323        *gas = Default::default();
324    }
325
326    Ok(result)
327}
328
329fn try_transact_monad_system_replay<DB, I>(
330    evm: &mut MonadEvm<DB, I>,
331    tx: &TxEnv,
332) -> eyre::Result<Option<ResultAndState>>
333where
334    DB: alloy_evm::Database,
335    I: Inspector<MonadContext<DB>>,
336{
337    let Some(system_call) = protocol_system_call(tx)? else {
338        return Ok(None);
339    };
340
341    system_call.validate_chain_id(evm.chain_id())?;
342    let journal = evm.ctx().journal_inner().clone();
343    let chain = evm.ctx().chain.clone();
344    let reserve_balance = evm.ctx().journaled_state.reserve_balance().clone();
345    let result = (|| {
346        let (db, journal) = evm.ctx_mut().db_journal_inner_mut();
347        system_call.apply_prestate(db, journal)?;
348        let result = evm
349            .transact_system_call(system_call.caller, system_call.contract, system_call.data)
350            .wrap_err("failed to execute protocol system transaction")?;
351        finish_protocol_system_call(result)
352    })();
353    if result.is_err() {
354        evm.ctx_mut().set_journal_inner(journal);
355        evm.ctx_mut().chain = chain;
356        *evm.ctx_mut().journaled_state.reserve_balance_mut() = reserve_balance;
357    }
358    result.map(Some)
359}
360
361impl FoundryEvmFactory for MonadEvmFactory {
362    type Chain = MonadChainContext;
363
364    type FoundryContext<'db> = MonadContext<&'db mut dyn DatabaseExt<Self>>;
365
366    type FoundryEvm<'db, I: FoundryInspectorExt<Self::FoundryContext<'db>>> =
367        MonadEvm<&'db mut dyn DatabaseExt<Self>, I>;
368
369    fn create_evm_with_context<DB: alloy_evm::Database>(
370        &self,
371        db: DB,
372        evm_env: EvmEnv<Self::Spec, Self::BlockEnv>,
373        chain_context: Self::Chain,
374    ) -> Self::Evm<DB, revm::inspector::NoOpInspector> {
375        let mut evm = self.create_evm(db, evm_env);
376        evm.ctx_mut().chain = chain_context;
377        evm
378    }
379
380    fn create_foundry_evm_with_inspector<'db, I: FoundryInspectorExt<Self::FoundryContext<'db>>>(
381        &self,
382        db: &'db mut dyn DatabaseExt<Self>,
383        evm_env: EvmEnv<Self::Spec, Self::BlockEnv>,
384        chain_context: Self::Chain,
385        inspector: I,
386    ) -> Self::FoundryEvm<'db, I> {
387        let mut monad_evm = self.create_evm_with_inspector(db, evm_env, inspector);
388        monad_evm.ctx_mut().chain = chain_context;
389        monad_evm.cfg.tx_chain_id_check = true;
390        monad_evm.inspector().get_networks().inject_precompiles(monad_evm.precompiles_mut());
391        monad_evm
392    }
393
394    fn try_transact_system_replay<DB, I>(
395        &self,
396        evm: &mut Self::Evm<DB, I>,
397        tx: &Self::Tx,
398    ) -> eyre::Result<Option<ResultAndState<Self::HaltReason>>>
399    where
400        DB: alloy_evm::Database,
401        I: Inspector<Self::Context<DB>>,
402    {
403        try_transact_monad_system_replay(evm, tx)
404    }
405
406    fn create_foundry_nested_evm<'db>(
407        &self,
408        db: &'db mut dyn DatabaseExt<Self>,
409        evm_env: EvmEnv<Self::Spec, Self::BlockEnv>,
410        chain_context: Self::Chain,
411        inspector: &'db mut dyn FoundryInspectorExt<Self::FoundryContext<'db>>,
412    ) -> NestedEvmFor<'db, Self> {
413        let spec = evm_env.cfg_env.spec;
414        let monad_cfg = MonadCfgEnv::from(evm_env.cfg_env);
415        let mut evm = monad_context_with_db(db)
416            .with_block(evm_env.block_env)
417            .with_cfg(monad_cfg)
418            .build_monad_with_inspector(inspector)
419            .with_precompiles(MonadPrecompilesMap::new_with_spec(spec));
420
421        evm.0.ctx.chain = chain_context;
422        evm.0.ctx.cfg.tx_chain_id_check = true;
423        evm.0.inspector.get_networks().inject_precompiles(&mut evm.0.precompiles);
424
425        Box::new(evm)
426    }
427}
428
429impl<'db, I: FoundryInspectorExt<MonadContext<&'db mut dyn DatabaseExt<MonadEvmFactory>>>> NestedEvm
430    for MonadRevmEvm<'db, I>
431{
432    type Spec = MonadHardfork;
433    type Block = BlockEnv;
434    type Tx = TxEnv;
435    type Chain = MonadChainContext;
436    type Journal = MonadJournal<&'db mut dyn DatabaseExt<MonadEvmFactory>>;
437
438    fn tx_mut(&mut self) -> &mut Self::Tx {
439        self.ctx_mut().tx_mut()
440    }
441
442    fn journal_inner_mut(&mut self) -> &mut JournaledState {
443        &mut self.ctx_mut().journaled_state.inner
444    }
445
446    fn chain_mut(&mut self) -> &mut Self::Chain {
447        &mut self.ctx_mut().chain
448    }
449
450    fn journal_mut(&mut self) -> &mut Self::Journal {
451        &mut self.ctx_mut().journaled_state
452    }
453
454    fn run_execution(&mut self, frame: FrameInput) -> Result<FrameResult, EVMError<DatabaseError>> {
455        let mut handler = MonadEvmHandler::<I>::new();
456
457        let memory =
458            SharedMemory::new_with_buffer(self.ctx_ref().local().shared_memory_buffer().clone());
459        let first_frame_input = FrameInit { depth: 0, memory, frame_input: frame };
460
461        let mut frame_result = handler.inspect_run_exec_loop(self, first_frame_input)?;
462
463        let mut parent_gas = GasTracker::new(
464            frame_result.gas().limit(),
465            frame_result.gas().remaining(),
466            frame_result.gas().reservoir(),
467        );
468        handler.last_frame_result(self, &mut frame_result, &mut parent_gas)?;
469
470        Ok(frame_result)
471    }
472
473    fn transact_raw(&mut self, tx: Self::Tx) -> eyre::Result<ResultAndState> {
474        let Some(system_call) = protocol_system_call(&tx)? else {
475            ContextSetters::set_tx(&mut self.0.ctx, tx);
476
477            let mut handler = MonadEvmHandler::<I>::new();
478            let result = handler.inspect_run(self)?;
479
480            return Ok(ResultAndState::new(
481                result,
482                self.ctx_ref().journaled_state.inner.state.clone(),
483            ));
484        };
485
486        system_call.validate_chain_id(self.ctx_ref().cfg().chain_id())?;
487        let journal = self.ctx_ref().journal_inner().clone();
488        let chain = self.ctx_ref().chain.clone();
489        let reserve_balance = self.ctx_ref().journaled_state.reserve_balance().clone();
490        let result = (|| {
491            let (db, journal) = self.0.ctx.db_journal_inner_mut();
492            system_call.apply_prestate(db, journal)?;
493            let result = self
494                .inspect_system_call_with_caller(
495                    system_call.caller,
496                    system_call.contract,
497                    system_call.data,
498                )
499                .wrap_err("failed to execute protocol system transaction")?;
500            finish_protocol_system_call(result)
501        })();
502        if result.is_err() {
503            self.ctx_mut().set_journal_inner(journal);
504            self.ctx_mut().chain = chain;
505            *self.ctx_mut().journaled_state.reserve_balance_mut() = reserve_balance;
506        }
507        result
508    }
509
510    fn to_evm_env(&self) -> EvmEnv<Self::Spec, Self::Block> {
511        self.ctx_ref().evm_clone()
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use crate::evm::{BlockContext, MonadEvmNetwork};
519    use alloy_sol_types::SolEvent;
520    use monad_revm::{
521        reserve_balance::tracker::ReserveBalanceInit,
522        staking::{
523            constants::MON,
524            interface::IMonadStaking::ValidatorRewarded,
525            storage::{
526                consensus_view_key, global_slots, val_id_secp_key, validator_key, validator_offsets,
527            },
528        },
529    };
530    use revm::{
531        Database, DatabaseCommit,
532        context::CfgEnv,
533        context_interface::{
534            either::Either,
535            transaction::{
536                AccessListItem, Authorization, RecoveredAuthority, RecoveredAuthorization,
537            },
538        },
539        database::InMemoryDB,
540        interpreter::{CallInputs, CallOutcome},
541        primitives::{B256, TxKind, address},
542        state::{Account, AccountInfo, EvmState},
543    };
544
545    #[derive(Default)]
546    struct ProtocolPrestateInspector {
547        call_count: usize,
548        staking_balance: Option<U256>,
549    }
550
551    impl Inspector<MonadContext<InMemoryDB>> for ProtocolPrestateInspector {
552        fn call(
553            &mut self,
554            context: &mut MonadContext<InMemoryDB>,
555            _inputs: &mut CallInputs,
556        ) -> Option<CallOutcome> {
557            self.call_count += 1;
558            self.staking_balance = context
559                .journaled_state
560                .inner
561                .state
562                .get(&STAKING_ADDRESS)
563                .map(|account| account.info.balance);
564            None
565        }
566    }
567
568    fn transaction(caller: Address, authority: Address) -> TxEnv {
569        let authorization = RecoveredAuthorization::new_unchecked(
570            Authorization { chain_id: U256::from(1), address: Address::ZERO, nonce: 0 },
571            RecoveredAuthority::Valid(authority),
572        );
573        TxEnv {
574            caller,
575            authorization_list: vec![Either::Right(authorization)],
576            ..Default::default()
577        }
578    }
579
580    fn system_transaction(data: Vec<u8>, value: U256) -> TxEnv {
581        TxEnv {
582            tx_type: TransactionType::Legacy as u8,
583            caller: SYSTEM_ADDRESS,
584            gas_limit: 0,
585            kind: revm::primitives::TxKind::Call(STAKING_ADDRESS),
586            data: data.into(),
587            value,
588            nonce: 3,
589            chain_id: None,
590            ..Default::default()
591        }
592    }
593
594    fn assert_invalid_system_transaction(tx: TxEnv, expected: &str) {
595        let err = protocol_system_call(&tx).unwrap_err();
596        assert!(err.to_string().contains(expected), "expected {expected:?} in error, got {err:?}");
597    }
598
599    #[test]
600    fn monad_evm_factory_implements_foundry_evm_factory() {
601        fn assert_foundry_factory<F: FoundryEvmFactory>() {}
602
603        assert_foundry_factory::<MonadEvmFactory>();
604    }
605
606    #[test]
607    fn monad_context_transition_rebases_live_tracker() {
608        let sender = Address::with_last_byte(1);
609        let old_chain = MonadChainContext::default();
610        let new_chain = MonadChainContext {
611            parent_senders_and_authorities: [sender].into_iter().collect(),
612            ..Default::default()
613        };
614        let mut account =
615            Account::from(AccountInfo { balance: U256::from(12), ..Default::default() });
616        account.info.balance = U256::from(9);
617
618        let factory = MonadEvmFactory::default();
619        let mut evm = factory.create_evm(
620            revm::database::EmptyDB::default(),
621            EvmEnv::new(
622                revm::context::CfgEnv::new_with_spec(MonadHardfork::MonadNine),
623                BlockEnv::default(),
624            ),
625        );
626        evm.ctx_mut().chain = old_chain.clone();
627        evm.ctx_mut().journaled_state.reserve_balance_mut().init(ReserveBalanceInit {
628            chain: &old_chain,
629            spec: MonadHardfork::MonadNine,
630            sender,
631            effective_gas_price: 0,
632            gas_limit: 0,
633            sender_is_delegated: false,
634            sender_account: Some(&account),
635        });
636        assert!(!evm.ctx().journaled_state.reserve_balance().has_violation());
637
638        evm.ctx_mut().chain = new_chain.clone();
639        evm.ctx_mut().journaled_state.inner.state = EvmState::from_iter([(sender, account)]);
640        crate::refresh_chain_journal(evm.ctx_mut());
641
642        assert_eq!(evm.ctx().chain, new_chain);
643        assert!(evm.ctx().journaled_state.reserve_balance().has_violation());
644    }
645
646    #[test]
647    fn monad_factory_classifies_canonical_system_envelopes() {
648        let reward = U256::from(25);
649        let reward_tx = system_transaction(
650            syscallRewardCall { blockAuthor: Address::with_last_byte(1) }.abi_encode(),
651            reward,
652        );
653        let reward_call = protocol_system_call(&reward_tx).unwrap().unwrap();
654        assert_eq!(reward_call.data.len(), 68);
655        assert_eq!(reward_call.balance_increment, Some((STAKING_ADDRESS, reward)));
656
657        let snapshot_tx = system_transaction(syscallSnapshotCall {}.abi_encode(), U256::ZERO);
658        assert!(protocol_system_call(&snapshot_tx).unwrap().is_some());
659
660        let epoch_tx =
661            system_transaction(syscallOnEpochChangeCall { epoch: 9 }.abi_encode(), U256::ZERO);
662        assert!(protocol_system_call(&epoch_tx).unwrap().is_some());
663
664        let mut unrelated = snapshot_tx;
665        unrelated.caller = Address::with_last_byte(2);
666        unrelated.tx_type = TransactionType::Eip1559 as u8;
667        assert!(protocol_system_call(&unrelated).unwrap().is_none());
668    }
669
670    #[test]
671    fn monad_replay_decline_leaves_evm_untouched() {
672        let tx = TxEnv {
673            caller: foundry_common::OPTIMISM_SYSTEM_ADDRESS,
674            kind: TxKind::Call(Address::with_last_byte(1)),
675            ..Default::default()
676        };
677        let factory = MonadEvmFactory::default();
678        let evm_env =
679            EvmEnv::new(CfgEnv::new_with_spec(MonadHardfork::MonadNine), BlockEnv::default());
680        let mut evm = factory.create_evm(InMemoryDB::default(), evm_env);
681        let tx_before = evm.tx().clone();
682        let journal_before = evm.ctx().journal_inner().clone();
683        let chain_before = evm.ctx().chain.clone();
684        let tracker_before = evm.ctx().journaled_state.reserve_balance().clone();
685
686        assert!(factory.try_transact_system_replay(&mut evm, &tx).unwrap().is_none());
687        assert_eq!(evm.tx(), &tx_before);
688        assert_eq!(evm.ctx().journal_inner().state, journal_before.state);
689        assert_eq!(evm.ctx().chain, chain_before);
690        assert_eq!(evm.ctx().journaled_state.reserve_balance(), &tracker_before);
691    }
692
693    #[test]
694    fn monad_factory_rejects_noncanonical_system_envelope_fields() {
695        let canonical = system_transaction(syscallSnapshotCall {}.abi_encode(), U256::ZERO);
696
697        let mut tx = canonical.clone();
698        tx.tx_type = TransactionType::Eip1559 as u8;
699        assert_invalid_system_transaction(tx, "transaction type must be legacy");
700
701        let mut tx = canonical.clone();
702        tx.kind = revm::primitives::TxKind::Call(Address::ZERO);
703        assert_invalid_system_transaction(tx, "target must be the staking contract");
704
705        let mut tx = canonical.clone();
706        tx.gas_limit = 1;
707        assert_invalid_system_transaction(tx, "gas limit must be zero");
708
709        let mut tx = canonical.clone();
710        tx.gas_price = 1;
711        assert_invalid_system_transaction(tx, "gas price must be zero");
712
713        let mut tx = canonical.clone();
714        tx.gas_priority_fee = Some(0);
715        assert_invalid_system_transaction(tx, "priority fee must be absent");
716
717        let mut tx = canonical.clone();
718        tx.access_list.0.push(AccessListItem::default());
719        assert_invalid_system_transaction(tx, "access list must be empty");
720
721        let mut tx = canonical.clone();
722        tx.blob_hashes.push(B256::ZERO);
723        assert_invalid_system_transaction(tx, "blob hashes must be empty");
724
725        let mut tx = canonical.clone();
726        tx.max_fee_per_blob_gas = 1;
727        assert_invalid_system_transaction(tx, "blob gas fee must be zero");
728
729        let mut tx = canonical;
730        tx.authorization_list =
731            transaction(Address::ZERO, Address::with_last_byte(1)).authorization_list;
732        assert_invalid_system_transaction(tx, "authorization list must be empty");
733    }
734
735    #[test]
736    fn monad_factory_rejects_noncanonical_system_call_data_and_value() {
737        assert_invalid_system_transaction(
738            system_transaction(Vec::new(), U256::ZERO),
739            "calldata is shorter than a selector",
740        );
741        assert_invalid_system_transaction(
742            system_transaction(vec![0xff; 4], U256::ZERO),
743            "unknown staking syscall selector",
744        );
745
746        let mut reward = syscallRewardCall { blockAuthor: Address::with_last_byte(1) }.abi_encode();
747        reward.push(0);
748        assert_invalid_system_transaction(
749            system_transaction(reward, U256::ZERO),
750            "reward calldata must be 36 bytes",
751        );
752
753        let mut malformed_reward =
754            syscallRewardCall { blockAuthor: Address::with_last_byte(1) }.abi_encode();
755        malformed_reward[4] = 1;
756        assert_invalid_system_transaction(
757            system_transaction(malformed_reward, U256::ZERO),
758            "invalid Monad protocol system reward calldata",
759        );
760
761        let mut snapshot = syscallSnapshotCall {}.abi_encode();
762        snapshot.push(0);
763        assert_invalid_system_transaction(
764            system_transaction(snapshot, U256::ZERO),
765            "snapshot calldata must be 4 bytes",
766        );
767        assert_invalid_system_transaction(
768            system_transaction(syscallSnapshotCall {}.abi_encode(), U256::from(1)),
769            "snapshot value must be zero",
770        );
771
772        let mut epoch = syscallOnEpochChangeCall { epoch: 9 }.abi_encode();
773        epoch.push(0);
774        assert_invalid_system_transaction(
775            system_transaction(epoch, U256::ZERO),
776            "epoch calldata must be 36 bytes",
777        );
778        let mut malformed_epoch = syscallOnEpochChangeCall { epoch: 9 }.abi_encode();
779        malformed_epoch[4] = 1;
780        assert_invalid_system_transaction(
781            system_transaction(malformed_epoch, U256::ZERO),
782            "invalid Monad protocol system epoch calldata",
783        );
784        assert_invalid_system_transaction(
785            system_transaction(syscallOnEpochChangeCall { epoch: 9 }.abi_encode(), U256::from(1)),
786            "epoch value must be zero",
787        );
788    }
789
790    #[test]
791    fn monad_factory_validates_system_envelope_chain_id_at_execution() {
792        let mut tx = system_transaction(syscallSnapshotCall {}.abi_encode(), U256::ZERO);
793        tx.chain_id = Some(143);
794        let system_call = protocol_system_call(&tx).unwrap().unwrap();
795
796        system_call.validate_chain_id(143).unwrap();
797        assert!(
798            system_call.validate_chain_id(1).unwrap_err().to_string().contains("chain ID mismatch")
799        );
800    }
801
802    #[test]
803    fn protocol_prestate_updates_nonce_and_balance() {
804        let caller = address!("00000000000000000000000000000000000000fe");
805        let recipient = address!("0000000000000000000000000000000000001000");
806        let mut db = InMemoryDB::default();
807        db.insert_account_info(caller, AccountInfo { nonce: 7, ..Default::default() });
808        db.insert_account_info(
809            recipient,
810            AccountInfo { balance: U256::from(10), ..Default::default() },
811        );
812        let call = ProtocolSystemCall {
813            caller,
814            contract: recipient,
815            data: Bytes::new(),
816            nonce: 7,
817            chain_id: None,
818            balance_increment: Some((recipient, U256::from(25))),
819        };
820        let mut journal = JournaledState::default();
821
822        call.apply_prestate(&mut db, &mut journal).unwrap();
823
824        assert_eq!(journal.state[&caller].info.nonce, 8);
825        assert_eq!(journal.state[&recipient].info.balance, U256::from(35));
826        assert_eq!(db.basic(caller).unwrap().unwrap().nonce, 7);
827        assert_eq!(db.basic(recipient).unwrap().unwrap().balance, U256::from(10));
828    }
829
830    #[test]
831    fn protocol_prestate_rejects_nonce_mismatch() {
832        let caller = address!("00000000000000000000000000000000000000fe");
833        let mut db = InMemoryDB::default();
834        db.insert_account_info(caller, AccountInfo { nonce: 3, ..Default::default() });
835        let call = ProtocolSystemCall {
836            caller,
837            contract: Address::ZERO,
838            data: Bytes::new(),
839            nonce: 4,
840            chain_id: None,
841            balance_increment: None,
842        };
843        let mut journal = JournaledState::default();
844
845        let err = call.apply_prestate(&mut db, &mut journal).unwrap_err();
846
847        assert!(err.to_string().contains("nonce mismatch"));
848        assert_eq!(db.basic(caller).unwrap().unwrap().nonce, 3);
849    }
850
851    #[test]
852    fn protocol_prestate_rejects_nonce_overflow() {
853        let caller = address!("00000000000000000000000000000000000000fe");
854        let mut db = InMemoryDB::default();
855        db.insert_account_info(caller, AccountInfo { nonce: u64::MAX, ..Default::default() });
856        let call = ProtocolSystemCall {
857            caller,
858            contract: Address::ZERO,
859            data: Bytes::new(),
860            nonce: u64::MAX,
861            chain_id: None,
862            balance_increment: None,
863        };
864        let mut journal = JournaledState::default();
865
866        let err = call.apply_prestate(&mut db, &mut journal).unwrap_err();
867
868        assert!(err.to_string().contains("nonce overflow"));
869        assert_eq!(db.basic(caller).unwrap().unwrap().nonce, u64::MAX);
870    }
871
872    #[test]
873    fn reward_envelope_replays_mint_nonce_storage_and_log() {
874        let block_author = address!("1111111111111111111111111111111111111111");
875        let validator_auth = address!("2222222222222222222222222222222222222222");
876        let validator_id = 7;
877        let reward = U256::from(25) * MON;
878        let initial_staking_balance = U256::from(3) * MON;
879        // Monad stores validator IDs and packed address/flags values left-aligned.
880        let validator_id_slot = U256::from(validator_id) << 192;
881        let address_flags_slot = U256::from_be_slice(validator_auth.as_slice()) << 96;
882        let mut db = InMemoryDB::default();
883        db.insert_account_info(SYSTEM_ADDRESS, AccountInfo { nonce: 11, ..Default::default() });
884        db.insert_account_info(
885            STAKING_ADDRESS,
886            AccountInfo { balance: initial_staking_balance, ..Default::default() },
887        );
888        db.insert_account_storage(
889            STAKING_ADDRESS,
890            val_id_secp_key(&block_author),
891            validator_id_slot,
892        )
893        .unwrap();
894        db.insert_account_storage(
895            STAKING_ADDRESS,
896            consensus_view_key(validator_id, 0),
897            U256::from(100) * MON,
898        )
899        .unwrap();
900        db.insert_account_storage(STAKING_ADDRESS, consensus_view_key(validator_id, 1), U256::ZERO)
901            .unwrap();
902        db.insert_account_storage(
903            STAKING_ADDRESS,
904            validator_key(validator_id, validator_offsets::ADDRESS_FLAGS),
905            address_flags_slot,
906        )
907        .unwrap();
908
909        let tx = TxEnv {
910            tx_type: 0,
911            caller: SYSTEM_ADDRESS,
912            gas_limit: 0,
913            kind: TxKind::Call(STAKING_ADDRESS),
914            value: reward,
915            data: syscallRewardCall { blockAuthor: block_author }.abi_encode().into(),
916            nonce: 11,
917            ..Default::default()
918        };
919        let factory = MonadEvmFactory::default();
920        let evm_env =
921            EvmEnv::new(CfgEnv::new_with_spec(MonadHardfork::MonadNine), BlockEnv::default());
922        let mut evm =
923            factory.create_evm_with_inspector(db, evm_env, ProtocolPrestateInspector::default());
924
925        let result = factory.try_transact_system_replay(&mut evm, &tx).unwrap().unwrap();
926
927        assert!(result.result.is_success());
928        assert_eq!(result.result.tx_gas_used(), 0);
929        assert!(evm.inspector().call_count > 0);
930        assert_eq!(evm.inspector().staking_balance, Some(initial_staking_balance + reward));
931        assert_eq!(result.result.logs().len(), 1);
932        assert_eq!(result.result.logs()[0].address, STAKING_ADDRESS);
933        assert_eq!(result.result.logs()[0].topics()[0], ValidatorRewarded::SIGNATURE_HASH);
934        evm.db_mut().commit(result.state);
935        let mut db = evm.into_db();
936        assert_eq!(db.basic(SYSTEM_ADDRESS).unwrap().unwrap().nonce, 12);
937        assert_eq!(
938            db.basic(STAKING_ADDRESS).unwrap().unwrap().balance,
939            initial_staking_balance + reward
940        );
941        assert_eq!(
942            db.storage(STAKING_ADDRESS, global_slots::PROPOSER_VAL_ID).unwrap(),
943            validator_id_slot
944        );
945        assert_eq!(
946            db.storage(
947                STAKING_ADDRESS,
948                validator_key(validator_id, validator_offsets::UNCLAIMED_REWARDS),
949            )
950            .unwrap(),
951            reward
952        );
953    }
954
955    #[test]
956    fn failed_reward_envelope_does_not_commit_prestate() {
957        let unknown_author = address!("1111111111111111111111111111111111111111");
958        let reward = U256::from(25) * MON;
959        let initial_staking_balance = U256::from(3) * MON;
960        let mut db = InMemoryDB::default();
961        db.insert_account_info(SYSTEM_ADDRESS, AccountInfo { nonce: 11, ..Default::default() });
962        db.insert_account_info(
963            STAKING_ADDRESS,
964            AccountInfo { balance: initial_staking_balance, ..Default::default() },
965        );
966        let tx = TxEnv {
967            tx_type: 0,
968            caller: SYSTEM_ADDRESS,
969            gas_limit: 0,
970            kind: TxKind::Call(STAKING_ADDRESS),
971            value: reward,
972            data: syscallRewardCall { blockAuthor: unknown_author }.abi_encode().into(),
973            nonce: 11,
974            ..Default::default()
975        };
976        let factory = MonadEvmFactory::default();
977        let evm_env =
978            EvmEnv::new(CfgEnv::new_with_spec(MonadHardfork::MonadNine), BlockEnv::default());
979        let mut evm =
980            factory.create_evm_with_inspector(db, evm_env, ProtocolPrestateInspector::default());
981        let journal_before = evm.ctx().journal_inner().clone();
982        let chain_before = evm.ctx().chain.clone();
983        let tracker_before = evm.ctx().journaled_state.reserve_balance().clone();
984
985        let error = factory.try_transact_system_replay(&mut evm, &tx).unwrap_err();
986
987        assert!(error.to_string().contains("reverted or halted"));
988        assert!(evm.inspector().call_count > 0);
989        assert_eq!(evm.inspector().staking_balance, Some(initial_staking_balance + reward));
990        assert_eq!(evm.ctx().journal_inner().state, journal_before.state);
991        assert_eq!(evm.ctx().chain, chain_before);
992        assert_eq!(evm.ctx().journaled_state.reserve_balance(), &tracker_before);
993        assert_eq!(evm.db_mut().basic(SYSTEM_ADDRESS).unwrap().unwrap().nonce, 11);
994        assert_eq!(
995            evm.db_mut().basic(STAKING_ADDRESS).unwrap().unwrap().balance,
996            initial_staking_balance
997        );
998        assert_eq!(
999            evm.db_mut().storage(STAKING_ADDRESS, global_slots::PROPOSER_VAL_ID).unwrap(),
1000            U256::ZERO
1001        );
1002    }
1003
1004    #[test]
1005    fn monad_context_tracks_senders_authorities_and_current_index() {
1006        let grandparent_sender = Address::from([1; 20]);
1007        let grandparent_authority = Address::from([2; 20]);
1008        let parent_sender = Address::from([3; 20]);
1009        let parent_authority = Address::from([4; 20]);
1010        let current_sender = Address::from([5; 20]);
1011        let current_authority = Address::from([6; 20]);
1012        let next_sender = Address::from([7; 20]);
1013        let next_authority = Address::from([8; 20]);
1014
1015        let grandparent = [transaction(grandparent_sender, grandparent_authority)];
1016        let parent = [transaction(parent_sender, parent_authority)];
1017        let current = [
1018            transaction(current_sender, current_authority),
1019            transaction(next_sender, next_authority),
1020        ];
1021
1022        let context = monad_context_from_participants(
1023            monad_block_participants(&grandparent),
1024            monad_block_participants(&parent),
1025            &current,
1026            1,
1027        );
1028
1029        assert_eq!(context.current_tx_index, 1);
1030        assert_eq!(context.grandparent_senders_and_authorities.len(), 2);
1031        assert!(context.grandparent_senders_and_authorities.contains(&grandparent_sender));
1032        assert!(context.grandparent_senders_and_authorities.contains(&grandparent_authority));
1033        assert_eq!(context.parent_senders_and_authorities.len(), 2);
1034        assert!(context.parent_senders_and_authorities.contains(&parent_sender));
1035        assert!(context.parent_senders_and_authorities.contains(&parent_authority));
1036        assert_eq!(context.current_block_senders, vec![current_sender, next_sender]);
1037        assert_eq!(context.current_block_authorities.len(), 2);
1038        assert!(context.current_block_authorities[0].contains(&current_authority));
1039        assert!(context.current_block_authorities[1].contains(&next_authority));
1040    }
1041
1042    #[test]
1043    fn child_context_advances_fork_ancestry() {
1044        let parent_sender = Address::from([1; 20]);
1045        let parent_authority = Address::from([2; 20]);
1046        let current_sender = Address::from([3; 20]);
1047        let current_authority = Address::from([4; 20]);
1048        let child_sender = Address::from([5; 20]);
1049        let child_authority = Address::from([6; 20]);
1050
1051        let context = BlockContext::<MonadEvmNetwork>::new(
1052            Vec::new(),
1053            vec![transaction(parent_sender, parent_authority)],
1054            vec![transaction(current_sender, current_authority)],
1055        )
1056        .into_child()
1057        .next_transaction(&transaction(child_sender, child_authority));
1058
1059        assert_eq!(context.current_tx_index, 0);
1060        assert_eq!(context.grandparent_senders_and_authorities.len(), 2);
1061        assert!(context.grandparent_senders_and_authorities.contains(&parent_sender));
1062        assert!(context.grandparent_senders_and_authorities.contains(&parent_authority));
1063        assert_eq!(context.parent_senders_and_authorities.len(), 2);
1064        assert!(context.parent_senders_and_authorities.contains(&current_sender));
1065        assert!(context.parent_senders_and_authorities.contains(&current_authority));
1066        assert_eq!(context.current_block_senders, vec![child_sender]);
1067        assert!(context.current_block_authorities[0].contains(&child_authority));
1068    }
1069
1070    #[test]
1071    fn transaction_cursor_replaces_target_and_excludes_future_transactions() {
1072        let preceding_sender = Address::from([1; 20]);
1073        let target_sender = Address::from([2; 20]);
1074        let future_sender = Address::from([3; 20]);
1075        let synthetic_sender = Address::from([4; 20]);
1076
1077        let cursor = BlockContext::<MonadEvmNetwork>::new(
1078            Vec::new(),
1079            Vec::new(),
1080            vec![
1081                transaction(preceding_sender, Address::ZERO),
1082                transaction(target_sender, Address::ZERO),
1083                transaction(future_sender, Address::ZERO),
1084            ],
1085        )
1086        .before_transaction(1)
1087        .unwrap();
1088        let context = cursor.next_transaction(&transaction(synthetic_sender, Address::ZERO));
1089
1090        assert_eq!(context.current_tx_index, 1);
1091        assert_eq!(context.current_block_senders, vec![preceding_sender, synthetic_sender]);
1092        assert!(!context.current_block_senders.contains(&target_sender));
1093        assert!(!context.current_block_senders.contains(&future_sender));
1094    }
1095
1096    #[test]
1097    fn transaction_cursor_accumulates_same_block_transactions() {
1098        let fork_sender = Address::from([1; 20]);
1099        let first_sender = Address::from([2; 20]);
1100        let second_sender = Address::from([3; 20]);
1101        let mut cursor = BlockContext::<MonadEvmNetwork>::new(
1102            Vec::new(),
1103            Vec::new(),
1104            vec![transaction(fork_sender, Address::ZERO)],
1105        )
1106        .into_child();
1107
1108        cursor.record_transaction(transaction(first_sender, Address::ZERO));
1109        let context = cursor.next_transaction(&transaction(second_sender, Address::ZERO));
1110
1111        assert_eq!(context.current_tx_index, 1);
1112        assert_eq!(context.current_block_senders, vec![first_sender, second_sender]);
1113        assert!(context.parent_senders_and_authorities.contains(&fork_sender));
1114    }
1115
1116    #[test]
1117    fn transaction_cursor_rotates_separate_blocks() {
1118        let fork_parent_sender = Address::from([1; 20]);
1119        let fork_sender = Address::from([2; 20]);
1120        let first_sender = Address::from([3; 20]);
1121        let second_sender = Address::from([4; 20]);
1122        let mut cursor = BlockContext::<MonadEvmNetwork>::new(
1123            Vec::new(),
1124            vec![transaction(fork_parent_sender, Address::ZERO)],
1125            vec![transaction(fork_sender, Address::ZERO)],
1126        )
1127        .into_child();
1128
1129        cursor.record_transaction(transaction(first_sender, Address::ZERO));
1130        cursor.advance_block();
1131        let context = cursor.next_transaction(&transaction(second_sender, Address::ZERO));
1132
1133        assert_eq!(context.current_tx_index, 0);
1134        assert_eq!(context.current_block_senders, vec![second_sender]);
1135        assert!(context.parent_senders_and_authorities.contains(&first_sender));
1136        assert!(context.grandparent_senders_and_authorities.contains(&fork_sender));
1137        assert!(!context.grandparent_senders_and_authorities.contains(&fork_parent_sender));
1138    }
1139}