Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions crates/fbal/tests/builder/delegatecall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
//! Tests for DELEGATECALL storage pattern tracking

use std::collections::HashMap;

use alloy_primitives::{B256, TxKind, U256};
use alloy_sol_types::SolCall;
use op_revm::OpTransaction;
use revm::{
context::TxEnv,
interpreter::instructions::utility::IntoAddress,
primitives::ONE_ETHER,
state::{AccountInfo, Bytecode},
};

use super::{BASE_SEPOLIA_CHAIN_ID, Logic, Logic2, Proxy, execute_txns_build_access_list};

#[test]
/// Tests that DELEGATECALL storage changes are tracked on the calling contract (Proxy),
/// not the logic contract
fn test_delegatecall_storage_tracked_on_caller() {
let sender = U256::from(0xDEAD).into_address();
let logic_addr = U256::from(0xBEEF).into_address();
let proxy_addr = U256::from(0xCAFE).into_address();

let mut overrides = HashMap::new();
overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER)));

// Deploy logic contract first
overrides.insert(
logic_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Logic::DEPLOYED_BYTECODE.clone())),
);

// Deploy proxy contract with logic as implementation
// We need to set up the proxy's storage slot 0 to point to logic
overrides.insert(
proxy_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Proxy::DEPLOYED_BYTECODE.clone())),
);

// Call setValue(42) through the proxy
let tx = OpTransaction::builder()
.base(
TxEnv::builder()
.caller(sender)
.chain_id(Some(BASE_SEPOLIA_CHAIN_ID))
.kind(TxKind::Call(proxy_addr))
.data(Logic::setValueCall { v: U256::from(42) }.abi_encode().into())
.nonce(0)
.gas_price(0)
.gas_priority_fee(None)
.max_fee_per_gas(0)
.gas_limit(200_000),
)
.build_fill();

let access_list = execute_txns_build_access_list(
vec![tx],
Some(overrides),
Some(HashMap::from([(proxy_addr, HashMap::from([(U256::ZERO, logic_addr.into_word())]))])),
);

// Verify that proxy is in touched accounts
let proxy_changes = access_list
.account_changes
.iter()
.find(|ac| ac.address == proxy_addr)
.expect("Proxy should be in account changes");

// Verify storage change for slot 1 is on the PROXY, not the logic contract
// Slot 1 is where `value` is stored
let slot_1 = B256::from(U256::from(1));
let has_slot_1_change = proxy_changes.storage_changes.iter().any(|sc| sc.slot == slot_1);
assert!(has_slot_1_change, "Proxy should have storage change for slot 1 (value)");

// Verify logic contract has NO storage changes (it's just providing code)
let logic_changes = access_list.account_changes.iter().find(|ac| ac.address == logic_addr);
if let Some(logic) = logic_changes {
assert!(logic.storage_changes.is_empty(), "Logic contract should have no storage changes");
}
}

#[test]
/// Tests that DELEGATECALL storage reads are tracked on the calling contract
fn test_delegatecall_read_tracked_on_caller() {
let sender = U256::from(0xDEAD).into_address();
let logic_addr = U256::from(0xBEEF).into_address();
let proxy_addr = U256::from(0xCAFE).into_address();

let mut overrides = HashMap::new();
overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER)));
overrides.insert(
logic_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Logic::DEPLOYED_BYTECODE.clone())),
);
overrides.insert(
proxy_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Proxy::DEPLOYED_BYTECODE.clone())),
);

// First set a value, then read it
let set_tx = OpTransaction::builder()
.base(
TxEnv::builder()
.caller(sender)
.chain_id(Some(BASE_SEPOLIA_CHAIN_ID))
.kind(TxKind::Call(proxy_addr))
.data(Logic::setValueCall { v: U256::from(42) }.abi_encode().into())
.nonce(0)
.gas_price(0)
.gas_priority_fee(None)
.max_fee_per_gas(0)
.gas_limit(200_000),
)
.build_fill();

let get_tx = OpTransaction::builder()
.base(
TxEnv::builder()
.caller(sender)
.chain_id(Some(BASE_SEPOLIA_CHAIN_ID))
.kind(TxKind::Call(proxy_addr))
.data(Logic::getValueCall {}.abi_encode().into())
.nonce(1)
.gas_price(0)
.gas_priority_fee(None)
.max_fee_per_gas(0)
.gas_limit(200_000),
)
.build_fill();

let access_list = execute_txns_build_access_list(
vec![set_tx, get_tx],
Some(overrides),
Some(HashMap::from([(proxy_addr, HashMap::from([(U256::ZERO, logic_addr.into_word())]))])),
);

// Verify proxy has storage reads recorded
let proxy_changes = access_list
.account_changes
.iter()
.find(|ac| ac.address == proxy_addr)
.expect("Proxy should be in account changes");

// Slot 1 should have been read (for getValue)
let slot_1 = B256::from(U256::from(1));
let has_slot_1_read = proxy_changes.storage_reads.iter().any(|sr| *sr == slot_1);
assert!(has_slot_1_read, "Proxy should have storage read for slot 1");

// Verify both addresses are in touched accounts
assert!(
access_list.account_changes.iter().any(|ac| ac.address == proxy_addr),
"Proxy should be in touched accounts"
);
assert!(
access_list.account_changes.iter().any(|ac| ac.address == logic_addr),
"Logic should be in touched accounts"
);
}

#[test]
/// Tests chained DELEGATECALL: Proxy -> Logic2 -> Logic
/// Storage changes should still be tracked on the original Proxy
fn test_delegatecall_chain() {
let sender = U256::from(0xDEAD).into_address();
let logic_addr = U256::from(0xBEEF).into_address();
let logic2_addr = U256::from(0xFACE).into_address();
let proxy_addr = U256::from(0xCAFE).into_address();

let mut overrides = HashMap::new();
overrides.insert(sender, AccountInfo::from_balance(U256::from(ONE_ETHER)));
overrides.insert(
logic_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Logic::DEPLOYED_BYTECODE.clone())),
);
overrides.insert(
logic2_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Logic2::DEPLOYED_BYTECODE.clone())),
);
overrides.insert(
proxy_addr,
AccountInfo::default().with_code(Bytecode::new_raw(Proxy::DEPLOYED_BYTECODE.clone())),
);

// Storage overrides:
// - Proxy slot 0 = logic2_addr (implementation)
// - Proxy slot 3 = logic_addr (nextLogic) - Because Logic2's chainedDelegatecall
// runs in Proxy's context, it reads nextLogic from Proxy's storage slot 3
let storage_overrides = HashMap::from([(
proxy_addr,
HashMap::from([
(U256::ZERO, logic2_addr.into_word()),
(U256::from(3), logic_addr.into_word()),
]),
)]);

// Call chainedDelegatecall with setValue(99) encoded
let inner_call = Logic::setValueCall { v: U256::from(99) }.abi_encode();
let tx = OpTransaction::builder()
.base(
TxEnv::builder()
.caller(sender)
.chain_id(Some(BASE_SEPOLIA_CHAIN_ID))
.kind(TxKind::Call(proxy_addr))
.data(
Logic2::chainedDelegatecallCall { data: inner_call.into() }.abi_encode().into(),
)
.nonce(0)
.gas_price(0)
.gas_priority_fee(None)
.max_fee_per_gas(0)
.gas_limit(300_000),
)
.build_fill();

let access_list =
execute_txns_build_access_list(vec![tx], Some(overrides), Some(storage_overrides));

// Verify all three addresses are in touched accounts
assert!(
access_list.account_changes.iter().any(|ac| ac.address == proxy_addr),
"Proxy should be in touched accounts"
);
assert!(
access_list.account_changes.iter().any(|ac| ac.address == logic2_addr),
"Logic2 should be in touched accounts"
);
assert!(
access_list.account_changes.iter().any(|ac| ac.address == logic_addr),
"Logic should be in touched accounts"
);

// Storage changes should be on the PROXY (the original caller)
let proxy_changes = access_list
.account_changes
.iter()
.find(|ac| ac.address == proxy_addr)
.expect("Proxy should have account changes");

let slot_1 = B256::from(U256::from(1));
let has_value_change = proxy_changes.storage_changes.iter().any(|sc| sc.slot == slot_1);
assert!(has_value_change, "Proxy should have storage change for slot 1 (value)");

// Logic contracts should have no storage changes
if let Some(logic2) = access_list.account_changes.iter().find(|ac| ac.address == logic2_addr) {
assert!(logic2.storage_changes.is_empty(), "Logic2 should have no storage changes");
}
if let Some(logic) = access_list.account_changes.iter().find(|ac| ac.address == logic_addr) {
assert!(logic.storage_changes.is_empty(), "Logic should have no storage changes");
}
}
6 changes: 3 additions & 3 deletions crates/fbal/tests/builder/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn test_create_deployment_tracked() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);

// Verify factory is in the access list
let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory);
Expand Down Expand Up @@ -123,7 +123,7 @@ fn test_create2_deployment_tracked() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);

// Verify factory is in the access list
let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory);
Expand Down Expand Up @@ -188,7 +188,7 @@ fn test_create_and_immediate_call() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);

// Verify factory is in the access list
let factory_entry = access_list.account_changes.iter().find(|ac| ac.address() == factory);
Expand Down
29 changes: 28 additions & 1 deletion crates/fbal/tests/builder/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use alloy_eip7928::{
AccountChanges, BalanceChange, CodeChange, EMPTY_BLOCK_ACCESS_LIST_HASH, NonceChange,
SlotChanges, StorageChange,
};
use alloy_primitives::{Address, B256};
use alloy_primitives::{Address, B256, U256};
use alloy_sol_macro::sol;
use base_fbal::{FlashblockAccessList, TouchedAccountsInspector};
use op_revm::OpTransaction;
Expand All @@ -22,6 +22,7 @@ use revm::{
state::AccountInfo,
};

mod delegatecall;
mod deployment;
mod storage;
mod transfers;
Expand Down Expand Up @@ -53,11 +54,30 @@ sol!(
)
);

sol!(
#[sol(rpc)]
Proxy,
concat!(env!("CARGO_MANIFEST_DIR"), "/../test-utils/contracts/out/Proxy.sol/Proxy.json")
);

sol!(
#[sol(rpc)]
Logic,
concat!(env!("CARGO_MANIFEST_DIR"), "/../test-utils/contracts/out/Proxy.sol/Logic.json")
);

sol!(
#[sol(rpc)]
Logic2,
concat!(env!("CARGO_MANIFEST_DIR"), "/../test-utils/contracts/out/Proxy.sol/Logic2.json")
);

const BASE_SEPOLIA_CHAIN_ID: u64 = 84532;

fn execute_txns_build_access_list(
txs: Vec<OpTransaction<TxEnv>>,
acc_overrides: Option<HashMap<Address, AccountInfo>>,
storage_overrides: Option<HashMap<Address, HashMap<U256, B256>>>,
) -> FlashblockAccessList {
let chain_spec = Arc::new(OpChainSpec::from_genesis(
serde_json::from_str(include_str!("../../../test-utils/assets/genesis.json")).unwrap(),
Expand All @@ -70,6 +90,13 @@ fn execute_txns_build_access_list(
db.insert_account_info(address, info);
}
}
if let Some(storage) = storage_overrides {
for (address, slots) in storage {
for (slot, value) in slots {
db.insert_account_storage(address, slot, U256::from_be_bytes(value.0)).unwrap();
}
}
}

let mut access_list = FlashblockAccessList {
min_tx_index: 0,
Expand Down
8 changes: 4 additions & 4 deletions crates/fbal/tests/builder/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fn test_sload_zero_value() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);
dbg!(access_list);
}

Expand Down Expand Up @@ -96,7 +96,7 @@ fn test_update_one_value() {
.build_fill(),
);

let access_list = execute_txns_build_access_list(txs, Some(overrides));
let access_list = execute_txns_build_access_list(txs, Some(overrides), None);
dbg!(access_list);
}

Expand Down Expand Up @@ -129,7 +129,7 @@ fn test_multi_sload_same_slot() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);
// TODO: dedup storage_reads
dbg!(access_list);
}
Expand Down Expand Up @@ -170,6 +170,6 @@ fn test_multi_sstore() {
)
.build_fill();

let access_list = execute_txns_build_access_list(vec![tx], Some(overrides));
let access_list = execute_txns_build_access_list(vec![tx], Some(overrides), None);
dbg!(access_list);
}
Loading