monad-revm 0.5.1

Monad-specific REVM implementation
Documentation
//! Reserve-balance precompile.

pub mod abi;
pub mod error;
pub mod interface;
pub mod tracker;

use crate::{api::exec::MonadContextTr, journal::MonadJournalTr, MonadHardfork};
use abi::{DIPPED_INTO_RESERVE_GAS, DIPPED_INTO_RESERVE_SELECTOR, RESERVE_BALANCE_ADDRESS};
use alloc::string::{String, ToString};
use error::ReserveBalanceError;
use revm::{
    context::Cfg,
    context_interface::ContextTr,
    interpreter::{CallInputs, CallScheme, Gas, InstructionResult, InterpreterResult},
    primitives::Bytes,
};

/// Runs the reserve-balance precompile.
pub fn run_reserve_balance_precompile<CTX>(
    context: &mut CTX,
    inputs: &CallInputs,
) -> Result<Option<InterpreterResult>, String>
where
    CTX: MonadContextTr,
{
    if inputs.bytecode_address != RESERVE_BALANCE_ADDRESS
        || !MonadHardfork::MonadNine.is_enabled_in(context.cfg().spec())
    {
        return Ok(None);
    }

    let is_delegated_call =
        inputs.scheme == CallScheme::Call && inputs.target_address != inputs.bytecode_address;

    if inputs.scheme != CallScheme::Call || inputs.is_static || is_delegated_call {
        return Ok(Some(revert_result(inputs.gas_limit, Bytes::new())));
    }

    if inputs.gas_limit < DIPPED_INTO_RESERVE_GAS {
        return Ok(Some(oog_result(inputs.gas_limit)));
    }

    let input = inputs.input.bytes(context);

    let selector = match input.get(..4) {
        Some(selector) if selector == DIPPED_INTO_RESERVE_SELECTOR => selector,
        _ => {
            return Ok(Some(revert_error_result(
                inputs.gas_limit,
                ReserveBalanceError::MethodNotSupported,
            )));
        }
    };

    let _ = selector;

    if !inputs.call_value().is_zero() {
        return Ok(Some(revert_error_result(inputs.gas_limit, ReserveBalanceError::ValueNonZero)));
    }

    if input.len() > 4 {
        return Ok(Some(revert_error_result(inputs.gas_limit, ReserveBalanceError::InvalidInput)));
    }

    let has_violation = context.journal().reserve_balance().has_violation();
    Ok(Some(success_result(inputs.gas_limit, DIPPED_INTO_RESERVE_GAS, has_violation)))
}

fn success_result(gas_limit: u64, gas_used: u64, value: bool) -> InterpreterResult {
    let mut gas = Gas::new(gas_limit);
    let _ = gas.record_regular_cost(gas_used);
    let mut output = [0u8; 32];
    output[31] = u8::from(value);
    InterpreterResult {
        result: InstructionResult::Return,
        gas,
        output: Bytes::copy_from_slice(&output),
    }
}

fn revert_error_result(gas_limit: u64, error: ReserveBalanceError) -> InterpreterResult {
    revert_result(gas_limit, Bytes::copy_from_slice(error.to_string().as_bytes()))
}

const fn revert_result(gas_limit: u64, output: Bytes) -> InterpreterResult {
    let mut gas = Gas::new(gas_limit);
    let _ = gas.record_regular_cost(gas_limit);
    InterpreterResult { result: InstructionResult::Revert, gas, output }
}

const fn oog_result(gas_limit: u64) -> InterpreterResult {
    let mut gas = Gas::new(gas_limit);
    let _ = gas.record_regular_cost(gas_limit);
    InterpreterResult { result: InstructionResult::PrecompileOOG, gas, output: Bytes::new() }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        api::default_ctx::{monad_context_with_db, DefaultMonad},
        journal::MonadJournalTr,
        reserve_balance::{
            abi::RESERVE_BALANCE_ADDRESS, interface::IReserveBalance::dippedIntoReserveCall,
            tracker::ReserveBalanceInit,
        },
        MonadCfgEnv, MonadHardfork,
    };
    use alloc::vec;
    use alloy_sol_types::SolCall;
    use revm::{
        bytecode::Bytecode,
        context_interface::JournalTr,
        database::InMemoryDB,
        interpreter::{CallInput, CallValue},
        primitives::{Address, B256, U256},
        state::AccountInfo,
    };

    fn reserve_balance_call_inputs_with_input(
        scheme: CallScheme,
        is_static: bool,
        value: CallValue,
        gas_limit: u64,
        input: Bytes,
    ) -> CallInputs {
        CallInputs {
            input: CallInput::Bytes(input),
            return_memory_offset: 0..0,
            gas_limit,
            reservoir: 0,
            bytecode_address: RESERVE_BALANCE_ADDRESS,
            known_bytecode: (B256::ZERO, Bytecode::default()),
            target_address: RESERVE_BALANCE_ADDRESS,
            caller: Address::ZERO,
            value,
            scheme,
            is_static,
            charged_new_account_state_gas: false,
        }
    }

    fn reserve_balance_call_inputs(
        scheme: CallScheme,
        is_static: bool,
        value: CallValue,
    ) -> CallInputs {
        reserve_balance_call_inputs_with_input(
            scheme,
            is_static,
            value,
            100_000,
            Bytes::from(dippedIntoReserveCall::SELECTOR.to_vec()),
        )
    }

    #[test]
    fn test_selector_matches_interface() {
        assert_eq!(DIPPED_INTO_RESERVE_SELECTOR, dippedIntoReserveCall::SELECTOR);
    }

    #[test]
    fn test_non_reserve_balance_address_returns_none() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let mut inputs =
            reserve_balance_call_inputs(CallScheme::Call, false, CallValue::Transfer(U256::ZERO));
        inputs.bytecode_address = Address::ZERO;

        let result = run_reserve_balance_precompile(&mut ctx, &inputs).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_monad_eight_returns_none() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadEight));
        let inputs =
            reserve_balance_call_inputs(CallScheme::Call, false, CallValue::Transfer(U256::ZERO));

        let result = run_reserve_balance_precompile(&mut ctx, &inputs).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_delegatecall_rejected() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs = reserve_balance_call_inputs(
            CallScheme::DelegateCall,
            false,
            CallValue::Transfer(U256::ZERO),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert!(result.output.is_empty());
    }

    #[test]
    fn test_call_in_static_context_rejected() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs =
            reserve_balance_call_inputs(CallScheme::Call, true, CallValue::Transfer(U256::ZERO));

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert!(result.output.is_empty());
    }

    #[test]
    fn test_delegated_call_rejected() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let mut inputs =
            reserve_balance_call_inputs(CallScheme::Call, false, CallValue::Transfer(U256::ZERO));
        inputs.target_address = Address::ZERO;

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert!(result.output.is_empty());
        assert_eq!(result.gas.remaining(), 0);
    }

    #[test]
    fn test_oog_before_selector_decode() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs = reserve_balance_call_inputs_with_input(
            CallScheme::Call,
            false,
            CallValue::Transfer(U256::ZERO),
            DIPPED_INTO_RESERVE_GAS - 1,
            Bytes::from(dippedIntoReserveCall::SELECTOR.to_vec()),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::PrecompileOOG);
    }

    #[test]
    fn test_short_input_hits_fallback() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs = reserve_balance_call_inputs_with_input(
            CallScheme::Call,
            false,
            CallValue::Transfer(U256::ZERO),
            100_000,
            Bytes::from(vec![0xde, 0xad, 0xbe]),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert_eq!(result.output, Bytes::from("method not supported"));
    }

    #[test]
    fn test_unknown_selector_hits_fallback() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs = reserve_balance_call_inputs_with_input(
            CallScheme::Call,
            false,
            CallValue::Transfer(U256::ZERO),
            100_000,
            Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert_eq!(result.output, Bytes::from("method not supported"));
    }

    #[test]
    fn test_nonzero_value_rejected() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs = reserve_balance_call_inputs(
            CallScheme::Call,
            false,
            CallValue::Transfer(U256::from(1)),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert_eq!(result.output, Bytes::from("value is nonzero"));
    }

    #[test]
    fn test_extra_input_rejected() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let mut input = dippedIntoReserveCall::SELECTOR.to_vec();
        input.extend_from_slice(&[0u8; 32]);
        let inputs = reserve_balance_call_inputs_with_input(
            CallScheme::Call,
            false,
            CallValue::Transfer(U256::ZERO),
            100_000,
            Bytes::from(input),
        );

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Revert);
        assert_eq!(result.output, Bytes::from("input is invalid"));
    }

    #[test]
    fn test_success_returns_false_when_no_violation_is_tracked() {
        let mut ctx = crate::api::default_ctx::MonadContext::monad()
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let inputs =
            reserve_balance_call_inputs(CallScheme::Call, false, CallValue::Transfer(U256::ZERO));

        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Return);
        assert_eq!(result.output.len(), 32);
        assert_eq!(result.output[31], 0);
    }

    #[test]
    fn test_success_returns_true_when_tracker_has_violation() {
        let sender = Address::from([0x11; 20]);
        let tracked = Address::from([0x22; 20]);
        let recipient = Address::from([0x33; 20]);

        let mut db = InMemoryDB::default();
        db.insert_account_info(
            sender,
            AccountInfo { balance: U256::from(1_000_000u64), ..Default::default() },
        );
        db.insert_account_info(
            tracked,
            AccountInfo { balance: U256::from(1_000u64), ..Default::default() },
        );

        let mut ctx = monad_context_with_db(db)
            .with_cfg(MonadCfgEnv::new_with_spec(MonadHardfork::MonadNine));
        let chain = ctx.chain().clone();
        ctx.journal_mut().reserve_balance_mut().init(ReserveBalanceInit {
            chain: &chain,
            spec: MonadHardfork::MonadNine,
            sender,
            effective_gas_price: 0,
            gas_limit: 0,
            sender_is_delegated: false,
            sender_account: None,
        });
        ctx.journal_mut()
            .transfer(tracked, recipient, U256::from(1u64))
            .expect("transfer should succeed");

        let inputs =
            reserve_balance_call_inputs(CallScheme::Call, false, CallValue::Transfer(U256::ZERO));
        let result = run_reserve_balance_precompile(&mut ctx, &inputs)
            .unwrap()
            .expect("should dispatch reserve-balance precompile");
        assert_eq!(result.result, InstructionResult::Return);
        assert_eq!(result.output.len(), 32);
        assert_eq!(result.output[31], 1);
    }
}