diff --git a/bins/revm-test/Cargo.toml b/bins/revm-test/Cargo.toml index 7540a00d6d..6f90e8099c 100644 --- a/bins/revm-test/Cargo.toml +++ b/bins/revm-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] bytes = "1.7" hex = "0.4" -revm = { path = "../../crates/revm", version = "14.0.0", default-features=false } +revm = { path = "../../crates/revm", version = "14.0.0", features=["morph-default-handler"] } microbench = "0.5" alloy-sol-macro = "0.8.0" alloy-sol-types = "0.8.0" diff --git a/bins/revm-test/src/bin/morph.rs b/bins/revm-test/src/bin/morph.rs new file mode 100644 index 0000000000..f9c8d8c5bb --- /dev/null +++ b/bins/revm-test/src/bin/morph.rs @@ -0,0 +1,363 @@ +use revm::{ + db::{CacheDB, EmptyDB}, + primitives::{ + address, bytes, keccak256, AccountInfo, Address, BlockEnv, Bytecode, Bytes, TxEnv, U256, + }, + Database, Evm, +}; + +static ERC20_DEPLOYED_CODE : Bytes = bytes!("608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461016857806370a082311461019857806395d89b41146101c8578063a457c2d7146101e6578063a9059cbb14610216578063dd62ed3e14610246576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b6610276565b6040516100c39190610b0c565b60405180910390f35b6100e660048036038101906100e19190610bc7565b610308565b6040516100f39190610c22565b60405180910390f35b61010461032b565b6040516101119190610c4c565b60405180910390f35b610134600480360381019061012f9190610c67565b610335565b6040516101419190610c22565b60405180910390f35b610152610364565b60405161015f9190610cd6565b60405180910390f35b610182600480360381019061017d9190610bc7565b61036d565b60405161018f9190610c22565b60405180910390f35b6101b260048036038101906101ad9190610cf1565b6103a4565b6040516101bf9190610c4c565b60405180910390f35b6101d06103ec565b6040516101dd9190610b0c565b60405180910390f35b61020060048036038101906101fb9190610bc7565b61047e565b60405161020d9190610c22565b60405180910390f35b610230600480360381019061022b9190610bc7565b6104f5565b60405161023d9190610c22565b60405180910390f35b610260600480360381019061025b9190610d1e565b610518565b60405161026d9190610c4c565b60405180910390f35b60606003805461028590610d8d565b80601f01602080910402602001604051908101604052809291908181526020018280546102b190610d8d565b80156102fe5780601f106102d3576101008083540402835291602001916102fe565b820191906000526020600020905b8154815290600101906020018083116102e157829003601f168201915b5050505050905090565b60008061031361059f565b90506103208185856105a7565b600191505092915050565b6000600254905090565b60008061034061059f565b905061034d858285610770565b6103588585856107fc565b60019150509392505050565b60006006905090565b60008061037861059f565b905061039981858561038a8589610518565b6103949190610ded565b6105a7565b600191505092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b6060600480546103fb90610d8d565b80601f016020809104026020016040519081016040528092919081815260200182805461042790610d8d565b80156104745780601f1061044957610100808354040283529160200191610474565b820191906000526020600020905b81548152906001019060200180831161045757829003601f168201915b5050505050905090565b60008061048961059f565b905060006104978286610518565b9050838110156104dc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016104d390610e93565b60405180910390fd5b6104e982868684036105a7565b60019250505092915050565b60008061050061059f565b905061050d8185856107fc565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603610616576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161060d90610f25565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603610685576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161067c90610fb7565b60405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040516107639190610c4c565b60405180910390a3505050565b600061077c8484610518565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146107f657818110156107e8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107df90611023565b60405180910390fd5b6107f584848484036105a7565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361086b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610862906110b5565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036108da576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108d190611147565b60405180910390fd5b6108e5838383610a72565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490508181101561096b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610962906111d9565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051610a599190610c4c565b60405180910390a3610a6c848484610a77565b50505050565b505050565b505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610ab6578082015181840152602081019050610a9b565b60008484015250505050565b6000601f19601f8301169050919050565b6000610ade82610a7c565b610ae88185610a87565b9350610af8818560208601610a98565b610b0181610ac2565b840191505092915050565b60006020820190508181036000830152610b268184610ad3565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610b5e82610b33565b9050919050565b610b6e81610b53565b8114610b7957600080fd5b50565b600081359050610b8b81610b65565b92915050565b6000819050919050565b610ba481610b91565b8114610baf57600080fd5b50565b600081359050610bc181610b9b565b92915050565b60008060408385031215610bde57610bdd610b2e565b5b6000610bec85828601610b7c565b9250506020610bfd85828601610bb2565b9150509250929050565b60008115159050919050565b610c1c81610c07565b82525050565b6000602082019050610c376000830184610c13565b92915050565b610c4681610b91565b82525050565b6000602082019050610c616000830184610c3d565b92915050565b600080600060608486031215610c8057610c7f610b2e565b5b6000610c8e86828701610b7c565b9350506020610c9f86828701610b7c565b9250506040610cb086828701610bb2565b9150509250925092565b600060ff82169050919050565b610cd081610cba565b82525050565b6000602082019050610ceb6000830184610cc7565b92915050565b600060208284031215610d0757610d06610b2e565b5b6000610d1584828501610b7c565b91505092915050565b60008060408385031215610d3557610d34610b2e565b5b6000610d4385828601610b7c565b9250506020610d5485828601610b7c565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610da557607f821691505b602082108103610db857610db7610d5e565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610df882610b91565b9150610e0383610b91565b9250828201905080821115610e1b57610e1a610dbe565b5b92915050565b7f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760008201527f207a65726f000000000000000000000000000000000000000000000000000000602082015250565b6000610e7d602583610a87565b9150610e8882610e21565b604082019050919050565b60006020820190508181036000830152610eac81610e70565b9050919050565b7f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b6000610f0f602483610a87565b9150610f1a82610eb3565b604082019050919050565b60006020820190508181036000830152610f3e81610f02565b9050919050565b7f45524332303a20617070726f766520746f20746865207a65726f20616464726560008201527f7373000000000000000000000000000000000000000000000000000000000000602082015250565b6000610fa1602283610a87565b9150610fac82610f45565b604082019050919050565b60006020820190508181036000830152610fd081610f94565b9050919050565b7f45524332303a20696e73756666696369656e7420616c6c6f77616e6365000000600082015250565b600061100d601d83610a87565b915061101882610fd7565b602082019050919050565b6000602082019050818103600083015261103c81611000565b9050919050565b7f45524332303a207472616e736665722066726f6d20746865207a65726f20616460008201527f6472657373000000000000000000000000000000000000000000000000000000602082015250565b600061109f602583610a87565b91506110aa82611043565b604082019050919050565b600060208201905081810360008301526110ce81611092565b9050919050565b7f45524332303a207472616e7366657220746f20746865207a65726f206164647260008201527f6573730000000000000000000000000000000000000000000000000000000000602082015250565b6000611131602383610a87565b915061113c826110d5565b604082019050919050565b6000602082019050818103600083015261116081611124565b9050919050565b7f45524332303a207472616e7366657220616d6f756e742065786365656473206260008201527f616c616e63650000000000000000000000000000000000000000000000000000602082015250565b60006111c3602683610a87565b91506111ce82611167565b604082019050919050565b600060208201905081810360008301526111f2816111b6565b905091905056fea2646970667358221220bd76a0877c61d26a928dd36a2ac3491d00e9086a429df7883853cc988a8c1cbf64736f6c63430008120033"); +const L2_FEE_VAULT: Address = address!("530000000000000000000000000000000000000a"); + +fn main() { + println!("start test eth_fee_normal"); + eth_fee_normal(); + println!("start test alt_fee_normal"); + alt_fee_normal(); +} + +fn alt_fee_normal() { + let account_from = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + let account_to = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"); + + // use erc20 gas token for txn. + let tx = TxEnv { + caller: account_from, + gas_limit: 31000u64, + transact_to: account_to.into(), + value: U256::from(1_000u64), + data: Bytes::new(), + nonce: None, + chain_id: None, + fee_token_id: Some(1), + fee_limit: None, + gas_price: U256::from(10u64.pow(9)), + ..Default::default() + }; + + // 1 ETH = 4000 USDT. + let (account_from_balance, erc20_value, erc20_value_vault, erc20_balance_evm) = + exec_alt_fee_txn(1, U256::from(1u64), U256::from(250000000u64), tx); + + assert!( + account_from_balance.to::() == 999999999999999000, + "Only 1000wei must have been transferred." + ); //Only the value 1_000 wei was transferred. + + assert!( + erc20_value.to::() == 199916000, + "Gas fees should use: 84,000" + ); //Gas fees used: 84,000 + + assert!( + erc20_value_vault.to::() == 84000, + "Gas fees should use: 84,000" + ); //recive gas fees: 84,000 + + assert!(erc20_value.eq(&erc20_balance_evm), "Gas fees used: 84,000") //Gas fees used: 84,000 +} + +fn exec_alt_fee_txn( + token_id: u16, + scale_value: U256, + price_ratio_value: U256, + tx: TxEnv, +) -> (U256, U256, U256, U256) { + let account_from = tx.caller; + let account_to = *tx.transact_to.to().unwrap(); + + let mut cache_db = CacheDB::new(EmptyDB::default()); + let token_account = address!("fab77965cAfB593Bd86E2e8073407CAb7fD2f6c4"); + let token_account_info = AccountInfo { + nonce: 0_u64, + balance: U256::from(1_000_000_000_000_000_000u128), + code_hash: keccak256(Bytes::new()), + code: Some(Bytecode::new_legacy(ERC20_DEPLOYED_CODE.clone())), + code_size: 0, + }; + // cache_db.insert_contract(token_account); + cache_db.insert_account_info(token_account, token_account_info.clone()); + + // Calculate the storage location of account_from in the _balances mapping + // Storage location of Solidity mapping = keccak256(abi.encode(key, slot)) + let balance_slot = U256::ZERO; // slot of _balances mapping in ERC20. + let mut data = [0u8; 64]; + data[12..32].copy_from_slice(account_from.as_slice()); // The address occupies 20 bytes, left-padded to 32 bytes. + data[32..64].copy_from_slice(&balance_slot.to_be_bytes::<32>()); // The slot occupies 32 bytes. + + let storage_key = keccak256(&data); + let storage_key_u256 = U256::from_be_bytes(storage_key.0); + + // Set the balance to 200000000 + let balance_value = U256::from(200000000u64); + let _ = cache_db.insert_account_storage(token_account, storage_key_u256, balance_value); + + let balance_slot = U256::ZERO; // slot of _balances mapping in ERC20. + let mut data = [0u8; 64]; + data[12..32].copy_from_slice(L2_FEE_VAULT.as_slice()); // The address occupies 20 bytes, left-padded to 32 bytes. + data[32..64].copy_from_slice(&balance_slot.to_be_bytes::<32>()); // The slot occupies 32 bytes. + + let storage_key = keccak256(&data); + let storage_key_u256_vault = U256::from_be_bytes(storage_key.0); + + // Set ERC20PriceOracle storage + let oracle_address = address!("5300000000000000000000000000000000000021"); + + // Calculate base slot for tokenRegistry[token_id] + // tokenRegistry is at slot 0 + let token_registry_slot = U256::from(151u64).to_be_bytes_vec(); + let mut token_id_bytes = [0u8; 32]; + token_id_bytes[30..32].copy_from_slice(&token_id.to_be_bytes()); + + let mut token_registry_pre_image = token_id_bytes.to_vec(); + token_registry_pre_image.extend_from_slice(&token_registry_slot); + let token_registry_base = keccak256(&token_registry_pre_image); + let token_registry_base_u256 = U256::from_be_bytes(token_registry_base.0); + + // TokenInfo struct layout: + // slot + 0: tokenAddress (address, 20 bytes) + 12 bytes padding + // slot + 1: balanceSlot (bytes32, 32 bytes) + // slot + 2: isActive (bool, 1 byte) + decimals (uint8, 1 byte) + 30 bytes padding + // slot + 3: scale (uint256, 32 bytes) + + // Set tokenAddress at slot + 0 + let token_address_value = U256::from_be_bytes(token_account.into_word().into()); + let _ = cache_db.insert_account_storage( + oracle_address, + token_registry_base_u256, + token_address_value, + ); + + // Set balanceSlot at slot + 1 (using slot 0 for ERC20 balance mapping) + let balance_slot_value = U256::ZERO; + let _ = cache_db.insert_account_storage( + oracle_address, + token_registry_base_u256 + U256::from(1), + balance_slot_value, + ); + + // Set isActive and decimals at slot + 2 + // isActive = true (1), decimals = 6 + // In storage: rightmost byte (byte 31) is isActive, byte 30 is decimals + let mut slot_2_bytes = [0u8; 32]; + slot_2_bytes[30] = 6; // decimals + slot_2_bytes[31] = 1; // isActive = true + let slot_2_value = U256::from_be_bytes(slot_2_bytes); + let _ = cache_db.insert_account_storage( + oracle_address, + token_registry_base_u256 + U256::from(2), + slot_2_value, + ); + + // Set scale at slot + 3 + let _ = cache_db.insert_account_storage( + oracle_address, + token_registry_base_u256 + U256::from(3), + scale_value, + ); + + // Set priceRatio for tokenID + // priceRatio is at slot 2 + let price_ratio_slot = U256::from(153u64).to_be_bytes_vec(); + let mut price_ratio_pre_image = token_id_bytes.to_vec(); + price_ratio_pre_image.extend_from_slice(&price_ratio_slot); + + let price_ratio_storage_slot = keccak256(&price_ratio_pre_image); + let price_ratio_storage_slot_u256 = U256::from_be_bytes(price_ratio_storage_slot.0); + + let _ = cache_db.insert_account_storage( + oracle_address, + price_ratio_storage_slot_u256, + price_ratio_value, + ); + + let acc_info = AccountInfo { + nonce: 0_u64, + balance: U256::from(1_000_000_000_000_000_000u128), + code_hash: keccak256(Bytes::new()), + code: None, + code_size: 0, + }; + cache_db.insert_account_info(account_from, acc_info.clone()); + let mut block_env = BlockEnv::default(); + block_env.coinbase = L2_FEE_VAULT; + let mut evm = Evm::builder() + .with_db(&mut cache_db) + .with_block_env(block_env) + .build(); + + // use erc20 gas token for txn. + let mut tx = tx; + + // process txn + tx.morph.is_l1_msg = false; + tx.morph.rlp_bytes = Some(Bytes::default()); + evm.context.evm.env.tx = tx; + let _ = evm.transact_commit(); + + let account_from_balance = evm + .context + .evm + .inner + .db + .load_account(account_from) + .unwrap() + .info + .balance; + println!("account_from_balance: {:?}", account_from_balance); + + let account_to_balance = evm + .context + .evm + .inner + .db + .load_account(account_to) + .unwrap() + .info + .balance; + println!("account_to_balance: {:?}", account_to_balance); + + let erc20_value = evm + .context + .evm + .db + .storage(token_account, storage_key_u256) + .unwrap_or_default(); + println!("account_from_erc20_value: {:?}", erc20_value); + + let erc20_value_vault = evm + .context + .evm + .db + .storage(token_account, storage_key_u256_vault) + .unwrap_or_default(); + + let method_id = [0x70u8, 0xa0, 0x82, 0x31]; + let mut calldata = Vec::with_capacity(36); + calldata.extend_from_slice(&method_id); + calldata.extend_from_slice(&[0u8; 12]); // Pad address to 32 bytes + calldata.extend_from_slice(account_from.as_slice()); + + let mut token_balance_tx = TxEnv { + caller: Address::default(), + gas_limit: u64::MAX, + transact_to: token_account.into(), + value: U256::ZERO, + data: Bytes::from(calldata), + nonce: None, + chain_id: None, + ..Default::default() + }; + token_balance_tx.morph.is_l1_msg = false; + token_balance_tx.morph.rlp_bytes = Some(Bytes::default()); + evm.context.evm.env.tx = token_balance_tx; + + let erc20_balance_evm = match evm.transact() { + Ok(result) => { + if result.result.is_success() { + // Parse the returned balance (32 bytes) + if let Some(output) = result.result.output() { + if output.len() >= 32 { + U256::from_be_slice(&output[..32]) + } else { + U256::ZERO + } + } else { + U256::ZERO + } + } else { + U256::ZERO + } + } + Err(_) => { + println!("get_erc20_balance error"); + U256::ZERO + } + }; + println!("account_from_erc20_value_evm: {:?}", erc20_balance_evm); + + ( + account_from_balance, + erc20_value, + erc20_value_vault, + erc20_balance_evm, + ) +} + +fn eth_fee_normal() { + let mut cache_db = CacheDB::new(EmptyDB::default()); + + let account_from = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + let account_to = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"); + + let acc_info = AccountInfo { + nonce: 0_u64, + balance: U256::from(1_000_000_000_000_000_000u128), + code_hash: keccak256(Bytes::new()), + code: None, + code_size: 0, + }; + cache_db.insert_account_info(account_from, acc_info.clone()); + + let acc_info_to = AccountInfo { + nonce: 0_u64, + balance: U256::ZERO, + code_hash: keccak256(Bytes::new()), + code: None, + code_size: 0, + }; + cache_db.insert_account_info(account_to, acc_info_to.clone()); + + let mut evm = Evm::builder().with_db(cache_db).build(); + + // use eth gas token. + let mut tx = TxEnv { + caller: account_from, + gas_limit: 21000u64, + transact_to: account_to.into(), + value: U256::from(1_000u64), + data: Bytes::new(), + nonce: None, + chain_id: None, + fee_token_id: None, + ..Default::default() + }; + + // process txn + tx.morph.is_l1_msg = false; + tx.morph.rlp_bytes = Some(Bytes::default()); + evm.context.evm.env.tx = tx; + let _ = evm.transact_commit(); + + let account_from_balance = evm + .context + .evm + .inner + .db + .load_account(account_from) + .unwrap() + .info + .balance; + println!("account_from_balance: {:?}", account_from_balance); + + let account_to_balance = evm + .context + .evm + .inner + .db + .load_account(account_to) + .unwrap() + .info + .balance; + println!("account_to_balance: {:?}", account_to_balance); + + assert!( + account_from_balance.to::() == 999999999999999000, + "Only 1000wei must have been transferred." + ); //Only the value 1_000 wei was transferred. + + assert!( + account_to_balance.to::() == 1000, + "account_to should receive 1000 wei." + ); //account_to should receive 1000 wei. +} diff --git a/bins/revme/src/cmd/statetest/runner.rs b/bins/revme/src/cmd/statetest/runner.rs index 80ab935d5d..a0e9a6b609 100644 --- a/bins/revme/src/cmd/statetest/runner.rs +++ b/bins/revme/src/cmd/statetest/runner.rs @@ -274,8 +274,7 @@ pub fn execute_test_suite( let bytecode = to_analysed(Bytecode::new_raw(info.code)); let acc_info = revm::primitives::AccountInfo { balance: info.balance, - #[cfg(feature = "morph")] - code_size, + code_size: 0, code_hash: keccak_code_hash, #[cfg(feature = "morph-poseidon-codehash")] poseidon_code_hash, diff --git a/crates/precompile/Cargo.toml b/crates/precompile/Cargo.toml index 761e5f6126..d111ac94c3 100644 --- a/crates/precompile/Cargo.toml +++ b/crates/precompile/Cargo.toml @@ -71,6 +71,12 @@ rstest = "0.22.0" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" +secp256k1 = { version = ">=0.28, <=0.29", default-features = false, features = [ + "alloc", + "recovery", + "rand", + "global-context", +] } [features] default = ["std", "kzg-rs", "portable"] diff --git a/crates/primitives/src/env.rs b/crates/primitives/src/env.rs index 13be6a974d..11318c95c0 100644 --- a/crates/primitives/src/env.rs +++ b/crates/primitives/src/env.rs @@ -241,6 +241,7 @@ impl Env { pub fn validate_tx_against_state( &self, account: &mut Account, + token_info: (U256, U256, U256), ) -> Result<(), InvalidTransaction> { // EIP-3607: Reject transactions from senders with deployed code // This EIP is introduced after london but there was no collision in past @@ -268,9 +269,12 @@ impl Env { } } - let mut balance_check = U256::from(self.tx.gas_limit) + let gas_cost = U256::from(self.tx.gas_limit) .checked_mul(self.tx.gas_price) - .and_then(|gas_cost| gas_cost.checked_add(self.tx.value)) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; + + let mut balance_check = gas_cost + .checked_add(self.tx.value) .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; if SPEC::enabled(SpecId::CANCUN) { @@ -283,7 +287,17 @@ impl Env { // Check if account has enough balance for gas_limit*gas_price and value transfer. // Transfer will be done inside `*_inner` functions. - if balance_check > account.info.balance { + let lack_of_fund_for_max_fee = if self.tx.fee_token_id.unwrap_or_default() != 0 { + let mut fee_limit = U256::from(self.tx.fee_limit.unwrap_or_default()); + if fee_limit.is_zero() || fee_limit > token_info.0 { + fee_limit = token_info.0 + } + let token_check = eth_to_token(gas_cost, token_info.1, token_info.2); + token_check.is_zero() || token_check > fee_limit || self.tx.value > account.info.balance + } else { + balance_check > account.info.balance + }; + if lack_of_fund_for_max_fee { cfg_if::cfg_if! { if #[cfg(not(feature = "morph"))] { if self.cfg.is_balance_check_disabled() { @@ -635,6 +649,13 @@ pub struct TxEnv { #[cfg(feature = "morph")] /// Morph fields pub morph: MorphFields, + + #[cfg(feature = "morph")] + /// For AltFeeType + pub fee_token_id: Option, + #[cfg(feature = "morph")] + /// For AltFeeType + pub fee_limit: Option, } pub enum TxType { @@ -680,6 +701,8 @@ impl Default for TxEnv { optimism: OptimismFields::default(), #[cfg(feature = "morph")] morph: MorphFields::default(), + fee_token_id: None, + fee_limit: None, } } } @@ -781,6 +804,21 @@ pub enum AnalysisKind { Analyse, } +pub fn eth_to_token(eth_amount: U256, rate: U256, token_scale: U256) -> U256 { + if rate.is_zero() { + return U256::ZERO; + } + // EthToToken token_amount = ethAmount / (tokenRate / tokenScale) = ethAmount * tokenScale / tokenRate + // Calculate: (eth_amount * token_scale) / rate + let (token_amount, remainder) = eth_amount.saturating_mul(token_scale).div_rem(rate); + // If there's a remainder, round up by adding 1 + if !remainder.is_zero() { + token_amount.saturating_add(U256::from(1)) + } else { + token_amount + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 2357d3f507..bec3517ba9 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -67,7 +67,7 @@ rstest = "0.22.0" alloy-provider = "0.3" [features] -default = ["std", "kzg-rs", "portable"] +default = ["std", "kzg-rs", "portable", "morph-default-handler"] std = [ "serde?/std", "serde_json?/std", diff --git a/crates/revm/src/context/evm_context.rs b/crates/revm/src/context/evm_context.rs index 6b651758d1..e527917592 100644 --- a/crates/revm/src/context/evm_context.rs +++ b/crates/revm/src/context/evm_context.rs @@ -530,6 +530,8 @@ pub(crate) mod test_utils { error: Ok(()), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: None, + #[cfg(feature = "morph")] + token_fee_info: None, }, precompiles: ContextPrecompiles::default(), } @@ -545,6 +547,8 @@ pub(crate) mod test_utils { error: Ok(()), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: None, + #[cfg(feature = "morph")] + token_fee_info: None, }, precompiles: ContextPrecompiles::default(), } diff --git a/crates/revm/src/context/inner_evm_context.rs b/crates/revm/src/context/inner_evm_context.rs index ab86a9ee1f..6d54eb4b43 100644 --- a/crates/revm/src/context/inner_evm_context.rs +++ b/crates/revm/src/context/inner_evm_context.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "morph")] +use crate::morph::TokenFeeInfo; use crate::{ db::Database, interpreter::{ @@ -33,6 +35,9 @@ pub struct InnerEvmContext { /// Used as temporary value holder to store L1 block info. #[cfg(feature = "morph")] pub l1_block_info: Option, + /// Used as temporary value holder to store alt fee info. + #[cfg(feature = "morph")] + pub token_fee_info: Option, } impl Clone for InnerEvmContext @@ -47,6 +52,8 @@ where error: self.error.clone(), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: self.l1_block_info.clone(), + #[cfg(feature = "morph")] + token_fee_info: self.token_fee_info.clone(), } } } @@ -60,6 +67,8 @@ impl InnerEvmContext { error: Ok(()), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: None, + #[cfg(feature = "morph")] + token_fee_info: None, } } @@ -73,6 +82,8 @@ impl InnerEvmContext { error: Ok(()), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: None, + #[cfg(feature = "morph")] + token_fee_info: None, } } @@ -88,6 +99,8 @@ impl InnerEvmContext { error: Ok(()), #[cfg(any(feature = "optimism", feature = "morph"))] l1_block_info: self.l1_block_info, + #[cfg(feature = "morph")] + token_fee_info: self.token_fee_info, } } diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 55781adddd..d55926492d 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -5,13 +5,16 @@ use crate::{ interpreter::{ CallInputs, CreateInputs, EOFCreateInputs, Host, InterpreterAction, SharedMemory, }, + morph::{get_mapping_account_slot, TokenFeeInfo}, primitives::{ - specification::SpecId, BlockEnv, CfgEnv, EVMError, EVMResult, EnvWithHandlerCfg, - ExecutionResult, HandlerCfg, ResultAndState, TxEnv, TxKind, EOF_MAGIC_BYTES, + eth_to_token, specification::SpecId, BlockEnv, Bytes, CfgEnv, EVMError, EVMResult, + EnvWithHandlerCfg, ExecutionResult, HandlerCfg, ResultAndState, TxEnv, TxKind, + EOF_MAGIC_BYTES, U256, }, Context, ContextWithHandlerCfg, Frame, FrameOrResult, FrameResult, }; use core::fmt; +use revm_interpreter::Gas; use std::{boxed::Box, vec::Vec}; /// EVM call stack limit. @@ -227,6 +230,15 @@ impl Evm<'_, EXT, DB> { /// This function will validate the transaction. #[inline] pub fn transact(&mut self) -> EVMResult { + let context = &mut self.context; + let fee_token_id = context.evm.inner.env().tx.fee_token_id.unwrap_or_default(); + if fee_token_id != 0 { + let caller = context.evm.inner.env.tx.caller; + let token_fee_info = + TokenFeeInfo::try_fetch(&mut context.evm.inner.db, fee_token_id, caller) + .map_err(EVMError::Database)?; + context.evm.inner.token_fee_info = token_fee_info; + } let initial_gas_spend = self.preverify_transaction_inner().inspect_err(|_| { self.clear(); })?; @@ -323,9 +335,9 @@ impl Evm<'_, EXT, DB> { /// Transact pre-verified transaction. fn transact_preverified_inner(&mut self, initial_gas_spend: u64) -> EVMResult { let spec_id = self.spec_id(); + let ctx = &mut self.context; let pre_exec = self.handler.pre_execution(); - // load access list and beneficiary if needed. pre_exec.load_accounts(ctx)?; @@ -334,9 +346,16 @@ impl Evm<'_, EXT, DB> { ctx.evm.set_precompiles(precompiles); // deduce caller balance with its limit. - pre_exec.deduct_caller(ctx)?; + let token_fee_info = ctx.evm.inner.token_fee_info.clone(); + if ctx.evm.inner.env.tx.fee_token_id.unwrap_or_default() != 0 { + self.deduct_caller_with_token(token_fee_info)?; + } else { + self.handler.pre_execution().deduct_caller(ctx)?; + } + let ctx = &mut self.context; let gas_limit = ctx.evm.env.tx.gas_limit - initial_gas_spend; + let pre_exec = self.handler.pre_execution(); // apply EIP-7702 auth list. let eip7702_gas_refund = pre_exec.apply_eip7702_auth_list(ctx)? as i64; @@ -383,12 +402,204 @@ impl Evm<'_, EXT, DB> { // calculate final refund and add EIP-7702 refund to gas. post_exec.refund(ctx, result.gas_mut(), eip7702_gas_refund); // Reimburse the caller - post_exec.reimburse_caller(ctx, result.gas())?; + + let ctx = &mut self.context; + let token_fee_info = ctx.evm.inner.token_fee_info.clone(); + if ctx.evm.inner.env.tx.fee_token_id.unwrap_or_default() != 0 { + self.reimburse_caller_with_token(token_fee_info, result.gas())?; + } else { + post_exec.reimburse_caller(ctx, result.gas())?; + } + + let ctx = &mut self.context; + let post_exec = self.handler.post_execution(); // Reward beneficiary - post_exec.reward_beneficiary(ctx, result.gas())?; + let token_fee_info = ctx.evm.inner.token_fee_info.clone(); + if token_fee_info.is_none() { + post_exec.reward_beneficiary(ctx, result.gas())?; + } // Returns output of transaction. post_exec.output(ctx, result) } + + pub fn deduct_caller_with_token( + &mut self, + token_info: Option, + ) -> Result<(), EVMError> { + let Some(token_info) = token_info else { + return Err(EVMError::Custom( + "[MORPH] Failed to calculate token gas.".to_string(), + )); + }; + + let ctx: &Context = &self.context; + let Some(rlp_bytes) = &ctx.evm.inner.env.tx.morph.rlp_bytes else { + return Err(EVMError::Custom( + "[MORPH] Failed to load transaction rlp_bytes.".to_string(), + )); + }; + + let tx_l1_cost = self + .context + .evm + .inner + .l1_block_info + .as_ref() + .expect("L1BlockInfo should be loaded") + .calculate_tx_l1_cost(rlp_bytes, self.spec_id()); + let gas_cost = + U256::from(ctx.evm.env.tx.gas_limit).saturating_mul(ctx.evm.env.effective_gas_price()); + let amount = tx_l1_cost + gas_cost; + let token_amount = eth_to_token(amount, token_info.price_ratio, token_info.scale); + if token_amount.is_zero() { + return Err(EVMError::Custom( + "[MORPH] Failed to calculate token gas.".to_string(), + )); + } + if token_amount > token_info.balance { + return Err(EVMError::Custom( + "[MORPH] Token balance is insufficient to pay gas.".to_string(), + )); + } + + if token_info.balance_slot.is_zero() { + let _ = self.transfer_token_evm(token_info, token_amount, true)?; + } else { + let ctx = &mut self.context; + let mut acc = ctx.evm.load_account(token_info.token_address)?; + acc.mark_touch(); + transfer_token_sstore(token_info.clone(), token_amount, ctx, true)? + } + + let ctx = &mut self.context; + let mut caller_account = ctx + .evm + .inner + .journaled_state + .load_account(ctx.evm.inner.env.tx.caller, &mut ctx.evm.inner.db)?; + + if matches!(ctx.evm.inner.env.tx.transact_to, TxKind::Call(_)) { + // Nonce is already checked + caller_account.data.info.nonce = caller_account.data.info.nonce.saturating_add(1); + } + caller_account.mark_touch(); + + Ok(()) + } + + pub fn reimburse_caller_with_token( + &mut self, + token_info: Option, + gas: &Gas, + ) -> Result<(), EVMError> { + let Some(token_info) = token_info else { + return Err(EVMError::Custom( + "[MORPH] Failed to calculate token gas.".to_string(), + )); + }; + let ctx = &mut self.context; + let effective_gas_price = ctx.evm.env.effective_gas_price(); + let amount = effective_gas_price * U256::from(gas.remaining() + gas.refunded() as u64); + if amount.is_zero() { + return Ok(()); + } + let token_amount = eth_to_token(amount, token_info.price_ratio, token_info.scale); + if token_amount.is_zero() { + return Err(EVMError::Custom( + "[MORPH] Failed to calculate token reimburse.".to_string(), + )); + } + if token_info.balance_slot.is_zero() { + let _ = self.transfer_token_evm(token_info, token_amount, false)?; + } else { + transfer_token_sstore(token_info.clone(), token_amount, ctx, false)?; + } + + Ok(()) + } + + fn transfer_token_evm( + &mut self, + token_info: TokenFeeInfo, + amount: U256, + forward: bool, + ) -> Result> { + let l2_fee_vault = self.context.env().block.coinbase; + let (from, to) = if forward { + (token_info.caller, l2_fee_vault) + } else { + (l2_fee_vault, token_info.caller) + }; + // Call transfer(address,uint256) method via EVM + // Method signature: transfer(address,uint256) -> 0xa9059cbb + let method_id = [0xa9u8, 0x05, 0x9c, 0xbb]; + + // Encode calldata: method_id + padded to address + amount + let mut calldata = Vec::with_capacity(68); + calldata.extend_from_slice(&method_id); + let mut address_bytes = [0u8; 32]; + address_bytes[12..32].copy_from_slice(to.as_slice()); + calldata.extend_from_slice(&address_bytes); + calldata.extend_from_slice(&amount.to_be_bytes::<32>()); + let mut tx = TxEnv { + caller: from, + gas_limit: 1_000_00u64, + gas_price: U256::ZERO, + transact_to: TxKind::Call(token_info.token_address), + value: U256::ZERO, + data: Bytes::from(calldata), + nonce: None, + chain_id: None, + ..Default::default() + }; + tx.morph.is_l1_msg = false; + tx.morph.rlp_bytes = Some(Bytes::default()); + let ctx = &mut self.context; + let exec = self.handler.execution(); + let call = exec.call(ctx, CallInputs::new_boxed(&tx, 1_000_000_000u64).unwrap())?; + match call { + FrameOrResult::Frame(first_frame) => self.run_the_loop(first_frame), + FrameOrResult::Result(result) => Ok(result), + } + } +} + +fn transfer_token_sstore( + token_info: TokenFeeInfo, + amount: U256, + ctx: &mut Context, + forward: bool, +) -> Result<(), EVMError> { + let l2_fee_vault = ctx.env().block.coinbase; + let (from, to) = if forward { + (token_info.caller, l2_fee_vault) + } else { + (l2_fee_vault, token_info.caller) + }; + // sub amount + let balance_slot = get_mapping_account_slot(token_info.balance_slot, from); + let balance = ctx + .evm + .sload(token_info.token_address, balance_slot) + .unwrap_or_default(); + ctx.evm.sstore( + token_info.token_address, + balance_slot, + balance.saturating_sub(amount), + )?; + + // add amount + let balance_slot = get_mapping_account_slot(token_info.balance_slot, to); + let balance = ctx + .evm + .sload(token_info.token_address, balance_slot) + .unwrap_or_default(); + ctx.evm.sstore( + token_info.token_address, + balance_slot, + balance.saturating_add(amount), + )?; + Ok(()) } #[cfg(test)] diff --git a/crates/revm/src/handler/mainnet/validation.rs b/crates/revm/src/handler/mainnet/validation.rs index 8aa4c86c23..8c80d20899 100644 --- a/crates/revm/src/handler/mainnet/validation.rs +++ b/crates/revm/src/handler/mainnet/validation.rs @@ -1,3 +1,4 @@ +use crate::primitives::U256; use revm_interpreter::gas; use crate::{ @@ -25,11 +26,24 @@ pub fn validate_tx_against_state( .journaled_state .load_code(tx_caller, &mut context.evm.inner.db)?; + if context.evm.inner.env.tx.fee_token_id.unwrap_or_default() != 0 + && context.evm.inner.token_fee_info.is_none() + { + return Err(EVMError::Custom( + "[MORPH] Failed to load token_fee_info.".to_string(), + )); + } + + let token_fee_info = match &context.evm.inner.token_fee_info { + Some(fee_info) => (fee_info.balance, fee_info.price_ratio, fee_info.scale), + None => (U256::ZERO, U256::ZERO, U256::ZERO), + }; + context .evm .inner .env - .validate_tx_against_state::(caller_account.data) + .validate_tx_against_state::(caller_account.data, token_fee_info) .map_err(EVMError::Transaction)?; Ok(()) diff --git a/crates/revm/src/morph.rs b/crates/revm/src/morph.rs index 2097c88845..5c6c3fdbab 100644 --- a/crates/revm/src/morph.rs +++ b/crates/revm/src/morph.rs @@ -1,7 +1,9 @@ mod handler_register; mod l1block; +pub mod token_fee; pub use crate::morph::handler_register::{ deduct_caller, load_accounts, morph_handle_register, reward_beneficiary, }; pub use crate::morph::l1block::{L1BlockInfo, L1_GAS_PRICE_ORACLE_ADDRESS}; +pub use crate::morph::token_fee::{get_mapping_account_slot, get_mapping_slot, TokenFeeInfo}; diff --git a/crates/revm/src/morph/handler_register.rs b/crates/revm/src/morph/handler_register.rs index b4b38c0847..f828826a29 100644 --- a/crates/revm/src/morph/handler_register.rs +++ b/crates/revm/src/morph/handler_register.rs @@ -1,5 +1,4 @@ //! Handler related to Morph chain - use crate::handler::mainnet; use crate::handler::mainnet::deduct_caller_inner; use crate::{ @@ -34,7 +33,6 @@ pub fn load_accounts( crate::morph::L1BlockInfo::try_fetch(&mut context.evm.inner.db, SPEC::SPEC_ID) .map_err(EVMError::Database)?; context.evm.inner.l1_block_info = Some(l1_block_info); - mainnet::load_accounts::(context) } diff --git a/crates/revm/src/morph/token_fee.rs b/crates/revm/src/morph/token_fee.rs new file mode 100644 index 0000000000..8b7c09da3f --- /dev/null +++ b/crates/revm/src/morph/token_fee.rs @@ -0,0 +1,193 @@ +use crate::primitives::{address, Address, EVMError, U256}; +use crate::primitives::{keccak256, Bytes, TxEnv, TxKind}; +use crate::{Database, Evm}; + +// https://github.com/morph-l2/morph/blob/main/contracts/contracts/l2/system/L2TokenRegistry.sol +// TokenRegistry is the storage slot for mapping(uint16 => TokenInfo) - slot 151 +const TOKEN_REGISTRY_SLOT: U256 = U256::from_limbs([151u64, 0, 0, 0]); +// PriceRatio is the storage slot for mapping(uint16 => uint256) - slot 153 +const PRICE_RATIO_SLOT: U256 = U256::from_limbs([153u64, 0, 0, 0]); +// System address for L2 token registry +pub const L2_TOKEN_REGISTRY_ADDRESS: Address = address!("5300000000000000000000000000000000000021"); + +#[derive(Clone, Debug, Default)] +pub struct TokenFeeInfo { + /// The fee token address + pub token_address: Address, + /// Whether the token is active + pub is_active: bool, + /// Token decimals + pub decimals: u8, + /// The price ratio of the token + pub price_ratio: U256, + /// The scale of the token + pub scale: U256, + /// The caller address + pub caller: Address, + /// The token balance of caller + pub balance: U256, + /// The users' erc20 balance slot + pub balance_slot: U256, +} + +impl TokenFeeInfo { + // Get the token information for gas payment from the state db. + pub fn try_fetch( + db: &mut DB, + token_id: u16, + caller: Address, + ) -> Result, DB::Error> { + // Get the base slot for this token_id in tokenRegistry mapping + let mut token_id_bytes = [0u8; 32]; + token_id_bytes[30..32].copy_from_slice(&token_id.to_be_bytes()); + let token_registry_base = get_mapping_slot(TOKEN_REGISTRY_SLOT, token_id_bytes.to_vec()); + // TokenInfo struct layout in storage (following Solidity storage packing rules): + // slot + 0: tokenAddress (address, 20 bytes) + 12 bytes padding + // slot + 1: balanceSlot (bytes32, 32 bytes) + // slot + 2: isActive (bool, 1 byte) + decimals (uint8, 1 byte) + 30 bytes padding + // slot + 3: scale (uint256, 32 bytes) + + // Read tokenAddress from slot + 0 + let slot_0 = db.storage(L2_TOKEN_REGISTRY_ADDRESS, token_registry_base)?; + let token_address = Address::from_word(slot_0.into()); + if token_address == Address::default() { + return Ok(None); + } + + // Read balanceSlot from slot + 1 + let mut token_balance_slot = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(1), + )?; + if !token_balance_slot.is_zero() { + token_balance_slot = token_balance_slot.saturating_sub(U256::from(1u64)); + } + + // Read isActive and decimals from slot + 2 + // In big-endian representation, rightmost byte is the lowest position + // isActive is at the rightmost (byte 31), decimals is to its left (byte 30) + let slot_2 = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(2), + )?; + let slot_2_bytes = slot_2.to_be_bytes::<32>(); + let is_active = slot_2_bytes[31] != 0; + let decimals = slot_2_bytes[30]; + + // Read scale from slot + 3 + let scale = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(3), + )?; + + // Get price ratio from priceRatio mapping + let price_ratio = load_mapping_value( + db, + L2_TOKEN_REGISTRY_ADDRESS, + PRICE_RATIO_SLOT, + token_id_bytes.to_vec(), + )?; + + // Get caller's token balance + let caller_token_balance = + get_erc20_balance(db, token_address, caller, token_balance_slot)?; + let token_fee = TokenFeeInfo { + token_address, + is_active, + decimals, + price_ratio, + scale, + caller, + balance: caller_token_balance, + balance_slot: token_balance_slot, + }; + + Ok(Some(token_fee)) + } +} + +/// Calculate the storage slot for a mapping value +pub fn get_mapping_slot(slot_index: U256, mut key: Vec) -> U256 { + let mut pre_image = slot_index.to_be_bytes_vec(); + key.append(&mut pre_image); + let storage_key = keccak256(key); + U256::from_be_bytes(storage_key.0) +} + +/// Calculate the account's storage slot for a mapping value +#[inline] +pub fn get_mapping_account_slot(slot_index: U256, account: Address) -> U256 { + let mut key = [0u8; 32]; + key[12..32].copy_from_slice(account.as_slice()); + get_mapping_slot(slot_index, key.to_vec()) +} + +fn load_mapping_value( + db: &mut DB, + account: Address, + slot_index: U256, + key: Vec, +) -> Result::Error> { + let storage_slot = get_mapping_slot(slot_index, key); + let storage_value = db.storage(account, storage_slot)?; + Ok(storage_value) +} + +pub(super) fn get_erc20_balance( + db: &mut DB, + token: Address, + account: Address, + token_balance_slot: U256, +) -> Result { + // If balance slot is provided, try to read directly from storage + if !token_balance_slot.is_zero() { + let mut data = [0u8; 32]; + data[12..32].copy_from_slice(account.as_slice()); + if let Ok(balance) = load_mapping_value(db, token, token_balance_slot, data.to_vec()) { + return Ok(balance); + } + } + + // Fallback: call balanceOf(address) method + // Method signature: balanceOf(address) -> 0x70a08231 + let method_id = [0x70u8, 0xa0, 0x82, 0x31]; + + // Encode calldata: method_id + padded address + let mut calldata = Vec::with_capacity(36); + calldata.extend_from_slice(&method_id); + calldata.extend_from_slice(&[0u8; 12]); // Pad address to 32 bytes + calldata.extend_from_slice(account.as_slice()); + + let db: &mut dyn Database = db; + let mut evm = Evm::builder().with_db(db).build(); + let mut tx = TxEnv { + caller: Address::default(), + gas_limit: u64::MAX, + transact_to: TxKind::Call(token), + value: U256::ZERO, + data: Bytes::from(calldata), + nonce: None, + chain_id: None, + ..Default::default() + }; + tx.morph.is_l1_msg = false; + tx.morph.rlp_bytes = Some(Bytes::default()); + evm.context.evm.env.tx = tx; + + // Execute transaction and extract balance from output + match evm.transact() { + Ok(result) => { + if result.result.is_success() { + // Parse the returned balance (32 bytes) + if let Some(output) = result.result.output() { + if output.len() >= 32 { + return Ok(U256::from_be_slice(&output[..32])); + } + } + } + Ok(U256::ZERO) + } + Err(EVMError::Database(db_err)) => Err(db_err), + Err(_) => Ok(U256::ZERO), + } +}