diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 5997dc9..66f7a52 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1137,14 +1137,33 @@ pub mod additional_limit_ext { /// Extends opcodes with storage gas cost on top of `compute_gas_ext`. pub mod storage_gas_ext { use super::*; + use alloy_primitives::Address; + + /// Address-selector for opcodes where the storage account is the stack `to` address (e.g. + /// CALL). + fn storage_addr_from_to(_mega_spec: MegaSpecId, _current: Address, to: Address) -> Address { + to + } + + /// Address-selector for CALLCODE: Rex5+ uses the current frame's address because CALLCODE + /// executes borrowed code in the caller's own storage context; pre-Rex5 preserves the frozen + /// behavior of metering against the code-source (stack `to`). + fn storage_addr_for_callcode(mega_spec: MegaSpecId, current: Address, to: Address) -> Address { + if mega_spec.is_enabled(MegaSpecId::REX5) { + current + } else { + to + } + } /// Macro to charge storage gas for new account creation before calling the wrapped instruction. /// /// This macro generates a wrapper function that: /// 1. Inspects the target address (stack position 1) and value (stack position 2) - /// 2. Checks if the target account is empty and value transfer is non-zero - /// 3. Charges storage gas for new account creation if applicable - /// 4. Calls the wrapped instruction implementation + /// 2. Resolves the storage account address via `$select_addr` + /// 3. Checks if the storage account is empty and value transfer is non-zero + /// 4. Charges storage gas for new account creation if applicable + /// 5. Calls the wrapped instruction implementation /// /// # Call Opcode Behavior /// @@ -1157,8 +1176,22 @@ pub mod storage_gas_ext { /// - `$fn_name`: Name of the generated function /// - `$opcode_name`: String name of the opcode (for documentation) /// - `$wrapped_fn`: Path to the wrapped instruction implementation + /// - `$has_transfer_logic`: `true` if the opcode can transfer value (inspects stack position 2) + /// - `$select_addr` (optional): Path to a `fn(MegaSpecId, current: Address, to: Address) -> + /// Address` function that returns the address to check for emptiness and charge + /// `new_account_storage_gas` against. `current` is the current frame's address; `to` is the + /// stack position-1 address. Defaults to [`storage_addr_from_to`]. macro_rules! wrap_call_with_storage_gas { ($fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr) => { + wrap_call_with_storage_gas!( + $fn_name, + $opcode_name, + $wrapped_fn, + $has_transfer_logic, + storage_addr_from_to + ); + }; + ($fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr, $select_addr:path) => { #[doc = concat!("`", $opcode_name, "` opcode implementation modified from `revm` with compute gas tracking and dynamically-scaled storage gas costs.")] pub fn $fn_name< WIRE: InterpreterTypes, @@ -1173,13 +1206,17 @@ pub mod storage_gas_ext { }; let to = to.into_address(); let mega_spec = context.host.spec_id(); - let Ok(to_account) = context.host.inspect_account_delegated(mega_spec, to) else { + let current_address = context.interpreter.input.target_address(); + let storage_address = $select_addr(mega_spec, current_address, to); + let Ok(storage_account) = + context.host.inspect_account_delegated(mega_spec, storage_address) + else { context.interpreter.halt(InstructionResult::FatalExternalError); return; }; - let is_empty = to_account.state_clear_aware_is_empty(spec); + let is_empty = storage_account.state_clear_aware_is_empty(spec); let has_transfer = if $has_transfer_logic { - let Some(value) =context.interpreter.stack.inspect::<2>() else { + let Some(value) = context.interpreter.stack.inspect::<2>() else { context.interpreter.halt(InstructionResult::StackUnderflow); return; }; @@ -1189,7 +1226,9 @@ pub mod storage_gas_ext { }; // Charge additional storage gas cost for creating a new account if is_empty && has_transfer { - let Some(new_account_storage_gas) = context.host.new_account_storage_gas(to) else { + let Some(new_account_storage_gas) = + context.host.new_account_storage_gas(storage_address) + else { context.interpreter.halt(InstructionResult::FatalExternalError); return; }; @@ -1203,7 +1242,6 @@ pub mod storage_gas_ext { } wrap_call_with_storage_gas!(call, "CALL", compute_gas_ext::call, true); - wrap_call_with_storage_gas!(call_code, "CALLCODE", compute_gas_ext::call_code, true); wrap_call_with_storage_gas!( delegate_call, "DELEGATECALL", @@ -1211,6 +1249,13 @@ pub mod storage_gas_ext { false ); wrap_call_with_storage_gas!(static_call, "STATICCALL", compute_gas_ext::static_call, false); + wrap_call_with_storage_gas!( + call_code, + "CALLCODE", + compute_gas_ext::call_code, + true, + storage_addr_for_callcode + ); /// `CREATE`/`CREATE2` opcode implementation modified from `revm` with compute gas tracking and /// dynamically-scaled storage gas costs. diff --git a/crates/mega-evm/tests/rex5/callcode_storage_gas.rs b/crates/mega-evm/tests/rex5/callcode_storage_gas.rs new file mode 100644 index 0000000..c59ad3b --- /dev/null +++ b/crates/mega-evm/tests/rex5/callcode_storage_gas.rs @@ -0,0 +1,365 @@ +//! Tests for Rex5's fix to `CALLCODE` new-account storage gas metering. +//! +//! Pre-Rex5, the storage-gas wrapper for `CALLCODE` checked emptiness and charged +//! `new_account_storage_gas` against the stack `to` address — the code-source. For +//! `CALLCODE`, however, execution happens in the caller's account context, so the +//! storage account being potentially "created" is the caller's, not the code-source. +//! Charging against the code-source can charge new-account storage gas spuriously +//! when the code-source happens to be empty. +//! +//! Rex5 changes the wrapper to meter new-account storage gas against +//! `interpreter.input.target_address()` (the caller / current frame). The stack +//! `to` is still used as the code-source for the underlying `CALLCODE` instruction. +//! Pre-Rex5 specs preserve their (frozen) prior behavior. +//! +//! `CALL` behavior is unchanged across all specs: the stack `to` is the value +//! recipient and is the correct address for emptiness / new-account metering. + +use std::convert::Infallible; + +use alloy_primitives::{address, Address, Bytes, TxKind, U256}; +use mega_evm::{ + constants::rex::NEW_ACCOUNT_STORAGE_GAS_BASE, + test_utils::{BytecodeBuilder, ErrorInjectingDatabase, InjectedDbError, MemoryDatabase}, + BucketId, EVMError, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvs, MegaContext, MegaEvm, + MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionError, SaltEnv, TestExternalEnvs, + MIN_BUCKET_SIZE, +}; +use revm::{ + bytecode::opcode::{CALL, CALLCODE, STOP}, + context::{result::ResultAndState, TxEnv}, +}; + +const CALLER: Address = address!("2000000000000000000000000000000000000001"); +const CALLEE: Address = address!("1000000000000000000000000000000000000001"); +/// An address that is not present in the database — i.e. an empty account. +const EMPTY_TARGET: Address = address!("3000000000000000000000000000000000000001"); + +/// Builds bytecode that performs `CALLCODE(gas=GAS, target, value=1, args=[], ret=[])` +/// followed by `STOP`. The CALL stipend covers gas inside the (empty-code) callee. +fn callcode_bytecode(target: Address) -> Bytes { + BytecodeBuilder::default() + .push_number(0_u64) // retSize + .push_number(0_u64) // retOffset + .push_number(0_u64) // argsSize + .push_number(0_u64) // argsOffset + .push_number(1_u64) // value = 1 wei + .push_address(target) + .push_number(100_000_u64) // gas + .append(CALLCODE) + .append(STOP) + .build() +} + +/// Builds bytecode that performs `CALL(gas=GAS, target, value=1, args=[], ret=[])` +/// followed by `STOP`. +fn call_bytecode(target: Address) -> Bytes { + BytecodeBuilder::default() + .push_number(0_u64) // retSize + .push_number(0_u64) // retOffset + .push_number(0_u64) // argsSize + .push_number(0_u64) // argsOffset + .push_number(1_u64) // value = 1 wei + .push_address(target) + .push_number(100_000_u64) // gas + .append(CALL) + .append(STOP) + .build() +} + +#[allow(clippy::too_many_arguments)] +fn transact( + spec: MegaSpecId, + db: &mut MemoryDatabase, + external_envs: &TestExternalEnvs, + caller: Address, + callee: Address, + value: U256, + gas_limit: u64, +) -> Result, EVMError> { + let mut context = + MegaContext::new(db, spec).with_external_envs(external_envs.into()).with_tx_runtime_limits( + EvmTxRuntimeLimits::no_limits() + .with_tx_data_size_limit(u64::MAX) + .with_tx_kv_updates_limit(u64::MAX), + ); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut evm = MegaEvm::new(context); + let tx = TxEnv { + caller, + kind: TxKind::Call(callee), + data: Bytes::new(), + value, + gas_limit, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + alloy_evm::Evm::transact_raw(&mut evm, tx) +} + +/// Runs the given bytecode on `spec` with a configurable bucket multiplier for the +/// empty target. Returns the transaction's `gas_used`. +fn run_with_target_multiplier(spec: MegaSpecId, bytecode: Bytes, target_multiplier: u64) -> u64 { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_000_000u64)) + .account_balance(CALLEE, U256::from(1_000_000_000u64)) + .account_code(CALLEE, bytecode); + + let target_bucket = TestExternalEnvs::::bucket_id_for_account(EMPTY_TARGET); + let external_envs = TestExternalEnvs::new() + .with_bucket_capacity(target_bucket, MIN_BUCKET_SIZE as u64 * target_multiplier); + + let result = transact(spec, &mut db, &external_envs, CALLER, CALLEE, U256::ZERO, 10_000_000) + .expect("transaction must succeed"); + assert!(result.result.is_success(), "execution must succeed: {:?}", result.result); + result.result.gas_used() +} + +// ============================================================================ +// CALLCODE: Rex5 fix — no new-account storage gas charged +// ============================================================================ + +/// Under Rex5, a value-transferring `CALLCODE` to an empty code-source must NOT +/// charge new-account storage gas, because the storage context is the (non-empty) +/// caller contract. +#[test] +fn test_rex5_callcode_to_empty_no_new_account_storage_gas() { + let bytecode = callcode_bytecode(EMPTY_TARGET); + let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX5, bytecode.clone(), 1); + let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX5, bytecode, 10); + + assert_eq!( + gas_mult10, gas_mult1, + "Rex5 CALLCODE must not charge new-account storage gas based on the code-source bucket", + ); +} + +// ============================================================================ +// CALLCODE: Pre-Rex5 frozen behavior — bug preserved +// ============================================================================ + +/// Pre-Rex5 (Rex4) preserves the original (buggy) behavior: a value-transferring +/// `CALLCODE` to an empty code-source charges new-account storage gas based on the +/// code-source's bucket. This test pins that behavior so a future regression in +/// stable-spec semantics is caught. +#[test] +fn test_rex4_callcode_to_empty_charges_new_account_storage_gas() { + let bytecode = callcode_bytecode(EMPTY_TARGET); + let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX4, bytecode.clone(), 1); + let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX4, bytecode, 10); + + let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9; + assert_eq!( + gas_mult10 - gas_mult1, + expected_extra, + "Rex4 (frozen) must keep charging new-account storage gas against the code-source bucket", + ); +} + +// ============================================================================ +// CALL: behavior unchanged — value-transferring CALL to empty target still charges +// ============================================================================ + +/// Under Rex5, a value-transferring `CALL` to an empty target still charges +/// new-account storage gas based on the target's bucket. The fix is scoped to +/// `CALLCODE` only; `CALL` semantics are unchanged. +#[test] +fn test_rex5_call_to_empty_still_charges_new_account_storage_gas() { + let bytecode = call_bytecode(EMPTY_TARGET); + let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX5, bytecode.clone(), 1); + let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX5, bytecode, 10); + + let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9; + assert_eq!( + gas_mult10 - gas_mult1, + expected_extra, + "Rex5 CALL must continue to charge new-account storage gas against the target bucket", + ); +} + +/// Pre-Rex5 (Rex4) `CALL` behavior is unchanged: value-transferring CALL to an +/// empty target charges new-account storage gas based on the target's bucket. +#[test] +fn test_rex4_call_to_empty_charges_new_account_storage_gas() { + let bytecode = call_bytecode(EMPTY_TARGET); + let gas_mult1 = run_with_target_multiplier(MegaSpecId::REX4, bytecode.clone(), 1); + let gas_mult10 = run_with_target_multiplier(MegaSpecId::REX4, bytecode, 10); + + let expected_extra = NEW_ACCOUNT_STORAGE_GAS_BASE * 9; + assert_eq!( + gas_mult10 - gas_mult1, + expected_extra, + "Rex4 CALL must charge new-account storage gas against the target bucket", + ); +} + +// ============================================================================ +// Error-path tests — coverage for FatalExternalError branches in call_code +// ============================================================================ + +/// A SALT environment that always fails `get_bucket_capacity`, triggering the +/// `new_account_storage_gas` → `None` → `FatalExternalError` path in `call_code`. +#[derive(Debug)] +struct FailingSaltEnv; + +impl SaltEnv for FailingSaltEnv { + type Error = String; + + fn get_bucket_capacity(&self, _bucket_id: BucketId) -> Result { + Err("injected salt error".into()) + } + + fn bucket_id_for_account(_account: Address) -> BucketId { + 0 + } + + fn bucket_id_for_slot(_address: Address, _key: U256) -> BucketId { + 0 + } +} + +fn transact_with_error_db( + spec: MegaSpecId, + db: ErrorInjectingDatabase, + caller: Address, + callee: Address, + gas_limit: u64, +) -> Result, EVMError> { + let external_envs = TestExternalEnvs::::new(); + let mut context = + MegaContext::new(db, spec).with_external_envs(external_envs.into()).with_tx_runtime_limits( + EvmTxRuntimeLimits::no_limits() + .with_tx_data_size_limit(u64::MAX) + .with_tx_kv_updates_limit(u64::MAX), + ); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut evm = MegaEvm::new(context); + let tx = TxEnv { + caller, + kind: TxKind::Call(callee), + data: Bytes::new(), + value: U256::ZERO, + gas_limit, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + alloy_evm::Evm::transact_raw(&mut evm, tx) +} + +fn transact_with_failing_salt( + spec: MegaSpecId, + db: &mut MemoryDatabase, + caller: Address, + callee: Address, + gas_limit: u64, +) -> Result, EVMError> { + let envs: ExternalEnvs<(FailingSaltEnv, EmptyExternalEnv)> = + ExternalEnvs { salt_env: FailingSaltEnv, oracle_env: EmptyExternalEnv }; + let mut context = MegaContext::new(db, spec).with_external_envs(envs).with_tx_runtime_limits( + EvmTxRuntimeLimits::no_limits() + .with_tx_data_size_limit(u64::MAX) + .with_tx_kv_updates_limit(u64::MAX), + ); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut evm = MegaEvm::new(context); + let tx = TxEnv { + caller, + kind: TxKind::Call(callee), + data: Bytes::new(), + value: U256::ZERO, + gas_limit, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + alloy_evm::Evm::transact_raw(&mut evm, tx) +} + +/// When `inspect_account_delegated` fails during CALLCODE (in `storage_gas_ext::call_code`), +/// the EVM should halt with `FatalExternalError` and return `EVMError::Custom`. +/// Under Rex4, the storage address is the code-source (stack `to` = `EMPTY_TARGET`). +#[test] +fn test_callcode_db_error_on_inspect_account() { + let bytecode = callcode_bytecode(EMPTY_TARGET); + let inner_db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_000_000u64)) + .account_balance(CALLEE, U256::from(1_000_000_000u64)) + .account_code(CALLEE, bytecode); + + let mut db = ErrorInjectingDatabase::new(inner_db); + db.fail_on_account = Some(EMPTY_TARGET); + + let result = transact_with_error_db(MegaSpecId::REX4, db, CALLER, CALLEE, 1_000_000); + + match result { + Err(EVMError::Custom(msg)) => { + assert!( + msg.contains("injected basic()"), + "error message should contain injected error, got: {msg}" + ); + } + Err(other) => panic!("expected EVMError::Custom, got: {other:?}"), + Ok(result) => panic!("expected error, got success: {:?}", result.result), + } +} + +/// Under Rex5, CALLCODE storage metering inspects the current frame target (`CALLEE`), +/// not the code-source. This injected database failure is triggered while loading +/// `CALLEE` during transaction setup, so it surfaces as `EVMError::Database`. +#[test] +fn test_rex5_callcode_db_error_on_inspect_account() { + let bytecode = callcode_bytecode(EMPTY_TARGET); + let inner_db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_000_000u64)) + .account_balance(CALLEE, U256::from(1_000_000_000u64)) + .account_code(CALLEE, bytecode); + + let mut db = ErrorInjectingDatabase::new(inner_db); + db.fail_on_account = Some(CALLEE); + + let result = transact_with_error_db(MegaSpecId::REX5, db, CALLER, CALLEE, 1_000_000); + + match result { + Err(EVMError::Database(err)) => { + assert!( + err.to_string().contains("injected basic()"), + "error message should contain injected error, got: {err}" + ); + } + Err(other) => panic!("expected EVMError::Database, got: {other:?}"), + Ok(result) => panic!("expected error, got success: {:?}", result.result), + } +} + +#[test] +fn test_callcode_salt_error_on_new_account_storage_gas() { + let bytecode = callcode_bytecode(EMPTY_TARGET); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_000_000u64)) + .account_balance(CALLEE, U256::from(1_000_000_000u64)) + .account_code(CALLEE, bytecode); + + let result = transact_with_failing_salt(MegaSpecId::REX4, &mut db, CALLER, CALLEE, 1_000_000); + + match result { + Err(EVMError::Custom(msg)) => { + assert!( + msg.contains("injected salt error"), + "error message should contain salt error, got: {msg}" + ); + } + Err(other) => panic!("expected EVMError::Custom, got: {other:?}"), + Ok(result) => panic!("expected error, got success: {:?}", result.result), + } +} diff --git a/crates/mega-evm/tests/rex5/main.rs b/crates/mega-evm/tests/rex5/main.rs index 721385a..aed43ff 100644 --- a/crates/mega-evm/tests/rex5/main.rs +++ b/crates/mega-evm/tests/rex5/main.rs @@ -1,4 +1,5 @@ //! Tests for `Rex5` hardfork features. mod apply_pending_changes_gas_budget; +mod callcode_storage_gas; mod frame_target_updated_dedup; diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index cabac3a..8b1225b 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -122,5 +122,6 @@ _See [Rex4 Network Upgrade](upgrades/rex4.md) for full details._ - **Oracle v2.0.0** — `onlySystemAddress` reads the authority from `SequencerRegistry`. In-place Oracle bytecode upgrades preserve existing storage instead of clearing it. - **Caller-account update deduplication** — Fixes overcounting of caller-account data-size and KV updates across multiple value-transferring sub-calls or creates from the same parent frame. - **[KeylessDeploy](system-contracts/keyless-deploy.md) trailing-bytes rejection** — RLP encodings with trailing bytes after the signed payload are rejected with `MalformedEncoding()`. +- **CALLCODE new-account storage gas fix** — New-account storage gas is now charged against the caller's storage context rather than the code-source address. _See [Rex5 Network Upgrade](upgrades/rex5.md) for full details._ diff --git a/docs/spec/upgrades/rex5.md b/docs/spec/upgrades/rex5.md index df1e4fe..2c6841d 100644 --- a/docs/spec/upgrades/rex5.md +++ b/docs/spec/upgrades/rex5.md @@ -1,5 +1,5 @@ --- -description: Rex5 network upgrade — SequencerRegistry with dual roles, dynamic system address, Oracle v2.0.0, KeylessDeploy trailing-bytes rejection, and caller-account update deduplication. +description: Rex5 network upgrade — SequencerRegistry with dual roles, dynamic system address, Oracle v2.0.0, KeylessDeploy trailing-bytes rejection, caller-account update deduplication, and CALLCODE new-account storage gas metering fix. --- # Rex5 Network Upgrade @@ -16,7 +16,11 @@ For the full normative definition, see the Rex5 spec in the mega-evm repository. Rex5 introduces the `SequencerRegistry` system contract, which tracks two independent roles: the **system address** (Oracle/system-tx authority) and the **sequencer** (mini-block signing key). It also upgrades the Oracle contract to v2.0.0 to read its authority from the registry. -Rex5 also corrects a resource-accounting bug where the caller-account update was overcounted whenever a contract performed multiple value-transferring sub-calls or contract creations from the same call frame. +Rex5 also tightens KeylessDeploy validation by rejecting signed inner transaction encodings with trailing bytes. + +Rex5 corrects a resource-accounting bug where the caller-account update was overcounted whenever a contract performed multiple value-transferring sub-calls or contract creations from the same call frame. + +Rex5 additionally corrects a `CALLCODE` storage-gas metering bug: new-account storage gas is now charged against the caller's storage context rather than the code-source address. ## What Changed @@ -73,7 +77,20 @@ Encodings with trailing data were accepted as long as the leading bytes formed a The decoder MUST reject any encoding that contains bytes after the signed RLP payload by reverting with `MalformedEncoding()`. This tightens validation so that two distinct byte strings cannot both pass as the "same" inner deployment transaction. -### 6. Caller-Account Update Deduplication (Data Size and KV Updates) +### 6. CALLCODE New-Account Storage Gas Metering + +**Previous behavior (Rex4 and earlier):** +The storage-gas wrapper for `CALLCODE` charged new-account storage gas against the stack `to` address — i.e. the code-source address. +For `CALLCODE`, however, execution happens in the caller's account context: the stack `to` only selects which code to load, while the storage / account context remains the caller's. +Charging new-account storage gas against the code-source address can therefore charge spuriously when the code-source is empty. + +**New behavior (Rex5):** +The new-account emptiness check and the `new_account_storage_gas(...)` charge are performed against the current frame's storage account (`interpreter.input.target_address()` — the caller / executing contract). +The stack `to` continues to be used solely as the code-source for the underlying `CALLCODE` instruction. +Pre-Rex5 specs preserve the (frozen) prior behavior for backward compatibility. +`CALL` semantics are unchanged: the stack `to` is still the value recipient and is the correct address for new-account metering. + +### 7. Caller-Account Update Deduplication (Data Size and KV Updates) **Previous behavior (Rex4 and earlier):** When a call frame performed a value-transferring `CALL` / `CALLCODE` or a `CREATE` / `CREATE2`, the implementation charged the _caller_ account update to the child frame's discardable budget. @@ -91,8 +108,11 @@ The discardable-on-revert mechanic is unchanged: charges recorded inside a child - Contracts that verify mini-block signatures can use `SequencerRegistry.currentSequencer()` to look up the signing authority. - Contracts that need historical information can use `systemAddressAt(blockNumber)` or `sequencerAt(blockNumber)`. - The Oracle contract's write methods (`setSlot`, `emitLog`, etc.) now accept calls from the current system address as reported by `SequencerRegistry`, not from a fixed address. +- KeylessDeploy signed inner transaction encodings with trailing bytes now revert with `MalformedEncoding()`. - Transactions that perform multiple value-transferring sub-calls or creates from the same contract now report lower data-size and KV-update usage than they did under Rex4. This only affects usage tracking; it does not change execution semantics, state transitions, or the base transaction gas model. +- Value-transferring `CALLCODE` no longer charges new-account storage gas based on the code-source address. + Contracts that previously paid spurious new-account storage gas via `CALLCODE` to an empty address see lower gas usage under Rex5. ## Safety and Compatibility @@ -100,7 +120,9 @@ The discardable-on-revert mechanic is unchanged: charges recorded inside a child - `SequencerRegistry` does not have an interceptor. It runs normal on-chain bytecode. - Both `_currentSystemAddress` and `_currentSequencer` are only updated during pre-block system calls, ensuring block-stability. - Changing one role does not affect the other. +- Rex4 and earlier retain the original KeylessDeploy trailing-bytes behavior unchanged. - Rex4 and earlier retain the original caller-account overcounting behavior unchanged. +- Rex4 and earlier retain the original `CALLCODE` new-account storage gas metering behavior unchanged. - Rex5 is the current unstable spec under active development; its semantics may still change before network activation. ## References