Skip to content

Commit

Permalink
Add BasicTxDetails and CallTargetDetails struct, add Function always …
Browse files Browse the repository at this point in the history
…to call details and use it to generate counterexample
  • Loading branch information
grandizzy committed Apr 26, 2024
1 parent c73ed83 commit 55fd876
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 90 deletions.
25 changes: 17 additions & 8 deletions crates/evm/evm/src/executors/invariant/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ impl FailedInvariantCaseData {
set_up_inner_replay(&mut executor, &self.inner_sequence);

// Replay each call from the sequence until we break the invariant.
for (sender, (addr, bytes, _, _)) in calls.iter() {
let call_result =
executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?;
for tx in calls.iter() {
let call_result = executor.call_raw_committing(
tx.sender,
tx.call_details.address,
tx.call_details.calldata.clone(),
U256::ZERO,
)?;

logs.extend(call_result.logs);
traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
Expand All @@ -184,9 +188,7 @@ impl FailedInvariantCaseData {
));

counterexample_sequence.push(BaseCounterExample::create(
*sender,
*addr,
bytes,
tx,
&ided_contracts,
call_result.traces,
));
Expand Down Expand Up @@ -260,8 +262,15 @@ impl FailedInvariantCaseData {
) -> bool {
// Apply the shrinked candidate sequence.
sequence.iter().for_each(|call_index| {
let (sender, (addr, bytes, _, _)) = &calls[*call_index];
executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO).unwrap();
let tx = &calls[*call_index];
executor
.call_raw_committing(
tx.sender,
tx.call_details.address,
tx.call_details.calldata.clone(),
U256::ZERO,
)
.unwrap();
});

// Check the invariant for candidate sequence.
Expand Down
10 changes: 7 additions & 3 deletions crates/evm/evm/src/executors/invariant/funcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,13 @@ pub fn replay_run(
// set_up_inner_replay(&mut executor, &inputs);

// Replay each call from the sequence until we break the invariant.
for (sender, (addr, bytes, _, _)) in inputs.iter() {
let call_result =
executor.call_raw_committing(*sender, *addr, bytes.clone(), U256::ZERO)?;
for tx in inputs.iter() {
let call_result = executor.call_raw_committing(
tx.sender,
tx.call_details.address,
tx.call_details.calldata.clone(),
U256::ZERO,
)?;

logs.extend(call_result.logs);
traces.push((TraceKind::Execution, call_result.traces.clone().unwrap()));
Expand Down
39 changes: 22 additions & 17 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::{
executors::{Executor, RawCallResult},
inspectors::Fuzzer,
};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, FixedBytes, U256};
use alloy_sol_types::{sol, SolCall};
use eyre::{eyre, ContextCompat, Result};
Expand Down Expand Up @@ -220,17 +219,26 @@ impl<'a> InvariantExecutor<'a> {
let mut assume_rejects_counter = 0;

while current_run < self.config.depth {
let (sender, (address, calldata, func, abi)) =
inputs.last().expect("no input generated");
let tx = inputs.last().expect("no input generated");

// Executes the call from the randomly generated sequence.
let call_result = if self.config.preserve_state {
executor
.call_raw_committing(*sender, *address, calldata.clone(), U256::ZERO)
.call_raw_committing(
tx.sender,
tx.call_details.address,
tx.call_details.calldata.clone(),
U256::ZERO,
)
.expect("could not make raw evm call")
} else {
executor
.call_raw(*sender, *address, calldata.clone(), U256::ZERO)
.call_raw(
tx.sender,
tx.call_details.address,
tx.call_details.calldata.clone(),
U256::ZERO,
)
.expect("could not make raw evm call")
};

Expand All @@ -251,9 +259,7 @@ impl<'a> InvariantExecutor<'a> {
if !&call_result.reverted {
collect_data(
&mut state_changeset,
sender,
func,
abi,
tx,
&call_result,
&fuzz_state,
self.config.depth,
Expand All @@ -278,7 +284,7 @@ impl<'a> InvariantExecutor<'a> {
executor.backend.commit(state_changeset.clone());

fuzz_runs.push(FuzzCase {
calldata: calldata.clone(),
calldata: tx.call_details.calldata.clone(),
gas: call_result.gas_used,
stipend: call_result.stipend,
});
Expand Down Expand Up @@ -677,29 +683,28 @@ impl<'a> InvariantExecutor<'a> {
/// randomly generated addresses.
fn collect_data(
state_changeset: &mut HashMap<Address, revm::primitives::Account>,
sender: &Address,
function: &Option<Function>,
abi: &JsonAbi,
tx: &BasicTxDetails,
call_result: &RawCallResult,
fuzz_state: &EvmFuzzState,
run_depth: u32,
) {
// Verify it has no code.
let mut has_code = false;
if let Some(Some(code)) = state_changeset.get(sender).map(|account| account.info.code.as_ref())
if let Some(Some(code)) =
state_changeset.get(&tx.sender).map(|account| account.info.code.as_ref())
{
has_code = !code.is_empty();
}

// We keep the nonce changes to apply later.
let mut sender_changeset = None;
if !has_code {
sender_changeset = state_changeset.remove(sender);
sender_changeset = state_changeset.remove(&tx.sender);
}

fuzz_state.collect_state_from_call(
function,
abi,
&tx.call_details.function,
&tx.call_details.abi,
&call_result.result,
&call_result.logs,
&*state_changeset,
Expand All @@ -708,7 +713,7 @@ fn collect_data(

// Re-add changes
if let Some(changed) = sender_changeset {
state_changeset.insert(*sender, changed);
state_changeset.insert(tx.sender, changed);
}
}

Expand Down
14 changes: 6 additions & 8 deletions crates/evm/fuzz/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,14 @@ impl Fuzzer {
!call_generator.used
{
// There's only a 30% chance that an override happens.
if let Some((sender, (contract, input, _, _))) =
call_generator.next(call.context.caller, call.contract)
{
*call.input = input.0;
call.context.caller = sender;
call.contract = contract;
if let Some(tx) = call_generator.next(call.context.caller, call.contract) {
*call.input = tx.call_details.calldata.0;
call.context.caller = tx.sender;
call.contract = tx.call_details.address;

// TODO: in what scenarios can the following be problematic
call.context.code_address = contract;
call.context.address = contract;
call.context.code_address = tx.call_details.address;
call.context.address = tx.call_details.address;

call_generator.used = true;
}
Expand Down
14 changes: 8 additions & 6 deletions crates/evm/fuzz/src/invariant/call_override.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::{BasicTxDetails, CallDetails};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, Bytes};
use alloy_primitives::Address;
use parking_lot::{Mutex, RwLock};
use proptest::{
option::weighted,
Expand Down Expand Up @@ -34,7 +33,7 @@ impl RandomCallGenerator {
pub fn new(
test_address: Address,
runner: TestRunner,
strategy: SBoxedStrategy<(Address, Bytes, Option<Function>, JsonAbi)>,
strategy: SBoxedStrategy<CallDetails>,
target_reference: Arc<RwLock<Address>>,
) -> Self {
let strategy = weighted(0.9, strategy).sboxed();
Expand Down Expand Up @@ -78,9 +77,12 @@ impl RandomCallGenerator {
*self.target_reference.write() = original_caller;

// `original_caller` has a 80% chance of being the `new_target`.
let choice = self.strategy.new_tree(&mut self.runner.lock()).unwrap().current().map(
|(new_target, calldata, func, abi)| (new_caller, (new_target, calldata, func, abi)),
);
let choice = self
.strategy
.new_tree(&mut self.runner.lock())
.unwrap()
.current()
.map(|call_details| BasicTxDetails::new(new_caller, call_details));

self.last_sequence.write().push(choice.clone());
choice
Expand Down
41 changes: 33 additions & 8 deletions crates/evm/fuzz/src/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,39 @@ impl FuzzRunIdentifiedContracts {
}
}

/// (Sender, CallDetails)
/// TODO: replace type with struct
pub type BasicTxDetails = (Address, CallDetails);

/// (TargetContractAddress, Calldata, Function, TargetContractAbi)
/// Function and contract abi are used for collecting sample values from result and logs.
/// Function is set for calls that returns values.
pub type CallDetails = (Address, Bytes, Option<Function>, JsonAbi);
/// Details of a transaction generated by invariant strategy for fuzzing a target.
#[derive(Clone, Debug)]
pub struct BasicTxDetails {
// Transaction sender address.
pub sender: Address,
// Transaction call details.
pub call_details: CallDetails,
}

impl BasicTxDetails {
pub fn new(sender: Address, call_details: CallDetails) -> Self {
Self { sender, call_details }
}
}

/// Call details of a transaction generated to fuzz invariant target.
#[derive(Clone, Debug)]
pub struct CallDetails {
// Target contract address.
pub address: Address,
// The data of the transaction.
pub calldata: Bytes,
// Target function, used to decode and collect values from result and to create counterexample.
pub function: Function,
// Target contract abi, used to decode and collect values from logs.
pub abi: JsonAbi,
}

impl CallDetails {
pub fn new(address: Address, calldata: Bytes, function: Function, abi: JsonAbi) -> Self {
Self { address, calldata, function, abi }
}
}

/// Test contract which is testing its invariants.
#[derive(Clone, Debug)]
Expand Down
39 changes: 19 additions & 20 deletions crates/evm/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod invariant;
pub mod strategies;

mod inspector;
use crate::invariant::BasicTxDetails;
pub use inspector::Fuzzer;

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -56,33 +57,31 @@ pub struct BaseCounterExample {

impl BaseCounterExample {
pub fn create(
sender: Address,
addr: Address,
bytes: &Bytes,
tx: &BasicTxDetails,
contracts: &ContractsByAddress,
traces: Option<CallTraceArena>,
) -> Self {
if let Some((name, abi)) = &contracts.get(&addr) {
if let Some(func) = abi.functions().find(|f| f.selector() == bytes[..4]) {
// skip the function selector when decoding
if let Ok(args) = func.abi_decode_input(&bytes[4..], false) {
return BaseCounterExample {
sender: Some(sender),
addr: Some(addr),
calldata: bytes.clone(),
signature: Some(func.signature()),
contract_name: Some(name.clone()),
traces,
args,
};
}
if let Some((name, _)) = &contracts.get(&tx.call_details.address) {
// skip the function selector when decoding
if let Ok(args) =
tx.call_details.function.abi_decode_input(&tx.call_details.calldata[4..], false)
{
return BaseCounterExample {
sender: Some(tx.sender),
addr: Some(tx.call_details.address),
calldata: tx.call_details.calldata.clone(),
signature: Some(tx.call_details.function.signature()),
contract_name: Some(name.clone()),
traces,
args,
};
}
}

BaseCounterExample {
sender: Some(sender),
addr: Some(addr),
calldata: bytes.clone(),
sender: Some(tx.sender),
addr: Some(tx.call_details.address),
calldata: tx.call_details.calldata.clone(),
signature: None,
contract_name: None,
traces,
Expand Down
16 changes: 6 additions & 10 deletions crates/evm/fuzz/src/strategies/invariants.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use super::{fuzz_calldata, fuzz_param_from_state};
use crate::{
invariant::{BasicTxDetails, FuzzRunIdentifiedContracts, SenderFilters},
invariant::{BasicTxDetails, CallDetails, FuzzRunIdentifiedContracts, SenderFilters},
strategies::{fuzz_calldata_from_state, fuzz_param, EvmFuzzState},
FuzzFixtures,
};
use alloy_json_abi::{Function, JsonAbi};
use alloy_primitives::{Address, Bytes};
use alloy_primitives::Address;
use parking_lot::RwLock;
use proptest::prelude::*;
use std::{rc::Rc, sync::Arc};
Expand All @@ -16,7 +16,7 @@ pub fn override_call_strat(
contracts: FuzzRunIdentifiedContracts,
target: Arc<RwLock<Address>>,
fuzz_fixtures: FuzzFixtures,
) -> SBoxedStrategy<(Address, Bytes, Option<Function>, JsonAbi)> {
) -> SBoxedStrategy<CallDetails> {
let contracts_ref = contracts.targets.clone();
proptest::prop_oneof![
80 => proptest::strategy::LazyJust::new(move || *target.read()),
Expand Down Expand Up @@ -112,6 +112,7 @@ fn generate_call(
(sender, contract)
})
})
.prop_map(|(sender, call_details)| BasicTxDetails::new(sender, call_details))
.boxed()
}

Expand Down Expand Up @@ -180,7 +181,7 @@ pub fn fuzz_contract_with_calldata(
contract: Address,
func: Function,
contract_abi: JsonAbi,
) -> impl Strategy<Value = (Address, Bytes, Option<Function>, JsonAbi)> {
) -> impl Strategy<Value = CallDetails> {
// We need to compose all the strategies generated for each parameter in all possible
// combinations.
// `prop_oneof!` / `TupleUnion` `Arc`s for cheap cloning.
Expand All @@ -191,11 +192,6 @@ pub fn fuzz_contract_with_calldata(
]
.prop_map(move |calldata| {
trace!(input=?calldata);
if !func.outputs.is_empty() {
// If function has outputs then return it for decoding result.
(contract, calldata, Some(func.clone()), contract_abi.clone())
} else {
(contract, calldata, None, contract_abi.clone())
}
CallDetails::new(contract, calldata, func.clone(), contract_abi.clone())
})
}

0 comments on commit 55fd876

Please sign in to comment.