-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: fuzz dictionary Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com> * fix: handle malformed bytecode * fix: limit search for push bytes * feat: collect fuzz state from logs * feat: build initial fuzz state from db * perf: use `Index` instead of `Selector` Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com>
- Loading branch information
1 parent
b672c91
commit 2837663
Showing
6 changed files
with
270 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,18 @@ | ||
use super::fuzz_param; | ||
use ethers::{abi::Function, types::Bytes}; | ||
use proptest::prelude::Strategy; | ||
use proptest::prelude::{BoxedStrategy, Strategy}; | ||
|
||
/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata | ||
/// Given a function, it returns a strategy which generates valid calldata | ||
/// for that function's input types. | ||
pub fn fuzz_calldata(func: &Function) -> impl Strategy<Value = Bytes> + '_ { | ||
pub fn fuzz_calldata(func: Function) -> BoxedStrategy<Bytes> { | ||
// We need to compose all the strategies generated for each parameter in all | ||
// possible combinations | ||
let strats = func.inputs.iter().map(|input| fuzz_param(&input.kind)).collect::<Vec<_>>(); | ||
|
||
strats.prop_map(move |tokens| { | ||
tracing::trace!(input = ?tokens); | ||
func.encode_input(&tokens).unwrap().into() | ||
}) | ||
strats | ||
.prop_map(move |tokens| { | ||
tracing::trace!(input = ?tokens); | ||
func.encode_input(&tokens).unwrap().into() | ||
}) | ||
.boxed() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
use super::fuzz_param_from_state; | ||
use crate::executor::StateChangeset; | ||
use bytes::Bytes; | ||
use ethers::{ | ||
abi::{Function, RawLog}, | ||
types::{H256, U256}, | ||
}; | ||
use proptest::prelude::{BoxedStrategy, Strategy}; | ||
use revm::{ | ||
db::{CacheDB, DatabaseRef}, | ||
opcode, spec_opcode_gas, SpecId, | ||
}; | ||
use std::{cell::RefCell, collections::HashSet, io::Write, rc::Rc}; | ||
|
||
/// A set of arbitrary 32 byte data from the VM used to generate values for the strategy. | ||
/// | ||
/// Wrapped in a shareable container. | ||
pub type EvmFuzzState = Rc<RefCell<HashSet<[u8; 32]>>>; | ||
|
||
/// Given a function and some state, it returns a strategy which generated valid calldata for the | ||
/// given function's input types, based on state taken from the EVM. | ||
pub fn fuzz_calldata_from_state( | ||
func: Function, | ||
state: EvmFuzzState, | ||
) -> BoxedStrategy<ethers::types::Bytes> { | ||
let strats = func | ||
.inputs | ||
.iter() | ||
.map(|input| fuzz_param_from_state(&input.kind, state.clone())) | ||
.collect::<Vec<_>>(); | ||
|
||
strats | ||
.prop_map(move |tokens| { | ||
tracing::trace!(input = ?tokens); | ||
func.encode_input(&tokens).unwrap().into() | ||
}) | ||
.no_shrink() | ||
.boxed() | ||
} | ||
|
||
/// Builds the initial [EvmFuzzState] from a database. | ||
pub fn build_initial_state<DB: DatabaseRef>(db: &CacheDB<DB>) -> EvmFuzzState { | ||
let mut state: HashSet<[u8; 32]> = HashSet::new(); | ||
for (address, storage) in db.storage() { | ||
let info = db.basic(*address); | ||
|
||
// Insert basic account information | ||
state.insert(H256::from(*address).into()); | ||
state.insert(u256_to_h256(info.balance).into()); | ||
state.insert(u256_to_h256(U256::from(info.nonce)).into()); | ||
|
||
// Insert storage | ||
for (slot, value) in storage { | ||
state.insert(u256_to_h256(*slot).into()); | ||
state.insert(u256_to_h256(*value).into()); | ||
} | ||
} | ||
|
||
Rc::new(RefCell::new(state)) | ||
} | ||
|
||
/// Collects state changes from a [StateChangeset] and logs into an [EvmFuzzState]. | ||
pub fn collect_state_from_call( | ||
logs: &[RawLog], | ||
state_changeset: &StateChangeset, | ||
state: EvmFuzzState, | ||
) { | ||
let state = &mut *state.borrow_mut(); | ||
|
||
for (address, account) in state_changeset { | ||
// Insert basic account information | ||
state.insert(H256::from(*address).into()); | ||
state.insert(u256_to_h256(account.info.balance).into()); | ||
state.insert(u256_to_h256(U256::from(account.info.nonce)).into()); | ||
|
||
// Insert storage | ||
for (slot, value) in &account.storage { | ||
state.insert(u256_to_h256(*slot).into()); | ||
state.insert(u256_to_h256(*value).into()); | ||
} | ||
|
||
// Insert push bytes | ||
if let Some(code) = &account.info.code { | ||
for push_byte in collect_push_bytes(code.clone()) { | ||
state.insert(push_byte); | ||
} | ||
} | ||
|
||
// Insert log topics and data | ||
for log in logs { | ||
log.topics.iter().for_each(|topic| { | ||
state.insert(topic.0); | ||
}); | ||
log.data.chunks(32).for_each(|chunk| { | ||
let mut buffer: [u8; 32] = [0; 32]; | ||
let _ = (&mut buffer[..]) | ||
.write(chunk) | ||
.expect("log data chunk was larger than 32 bytes"); | ||
state.insert(buffer); | ||
}); | ||
} | ||
} | ||
} | ||
|
||
/// The maximum number of bytes we will look at in bytecodes to find push bytes (24 KiB). | ||
/// | ||
/// This is to limit the performance impact of fuzz tests that might deploy arbitrarily sized | ||
/// bytecode (as is the case with Solmate). | ||
const PUSH_BYTE_ANALYSIS_LIMIT: usize = 24 * 1024; | ||
|
||
/// Collects all push bytes from the given bytecode. | ||
fn collect_push_bytes(code: Bytes) -> Vec<[u8; 32]> { | ||
let mut bytes: Vec<[u8; 32]> = Vec::new(); | ||
|
||
// We use [SpecId::LATEST] since we do not really care what spec it is - we are not interested | ||
// in gas costs. | ||
let opcode_infos = spec_opcode_gas(SpecId::LATEST); | ||
|
||
let mut i = 0; | ||
while i < code.len().min(PUSH_BYTE_ANALYSIS_LIMIT) { | ||
let op = code[i]; | ||
if opcode_infos[op as usize].is_push { | ||
let push_size = (op - opcode::PUSH1 + 1) as usize; | ||
let push_start = i + 1; | ||
let push_end = push_start + push_size; | ||
|
||
// As a precaution, if a fuzz test deploys malformed bytecode (such as using `CREATE2`) | ||
// this will terminate the loop early. | ||
if push_start > code.len() || push_end > code.len() { | ||
return bytes | ||
} | ||
|
||
let mut buffer: [u8; 32] = [0; 32]; | ||
let _ = (&mut buffer[..]) | ||
.write(&code[push_start..push_end]) | ||
.expect("push was larger than 32 bytes"); | ||
bytes.push(buffer); | ||
i += push_size; | ||
} | ||
i += 1; | ||
} | ||
|
||
bytes | ||
} | ||
|
||
/// Small helper function to convert [U256] into [H256]. | ||
fn u256_to_h256(u: U256) -> H256 { | ||
let mut h = H256::default(); | ||
u.to_little_endian(h.as_mut()); | ||
h | ||
} |