Repository navigation
fix(symbolic): include the cheatcode address in EXTCODE* candidates #18330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mablr/evm2-migration
Are you sure you want to change the base?
Changes from all commits
925ef66
56106c7
40506a5
c4f9c74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| use super::*; | ||
| use foundry_cheatcodes_spec::Vm::VmCalls; | ||
|
|
||
| impl SymbolicExecutor { | ||
| pub(super) fn call( | ||
|
|
@@ -824,7 +825,27 @@ impl SymbolicExecutor { | |
| out_offset: SymExpr, | ||
| out_size: BoundedCopySize, | ||
| ) -> Result<StepOutcome, SymbolicError> { | ||
| if to == CHEATCODE_ADDRESS || to == SYMBOLIC_VM_COMPAT_ADDRESS { | ||
| let compatibility_cheatcode = if to == SYMBOLIC_VM_COMPAT_ADDRESS { | ||
| let (_, max_input_size, _) = in_size.parts(&mut self.cx); | ||
| if max_input_size < 4 { | ||
| false | ||
| } else if let Some(offset) = in_offset.as_const() { | ||
| let offset = usize::try_from(offset).map_err(|_| { | ||
| SymbolicError::Unsupported("symbolic compatibility CALL input offset") | ||
| })?; | ||
| let selector = state.memory.read_concrete(&mut self.cx, offset, 4)?; | ||
| let selector: [u8; 4] = selector | ||
| .try_into() | ||
| .map_err(|_| SymbolicError::Unsupported("symbolic compatibility selector"))?; | ||
| SymbolicVmCheatcode::from_selector(selector).is_some() | ||
| } else { | ||
| // The existing symbolic-offset path checks whether the selector is pinned. | ||
| true | ||
| } | ||
| } else { | ||
| false | ||
| }; | ||
| if to == CHEATCODE_ADDRESS || compatibility_cheatcode { | ||
| if !state.constrained_word(&mut self.cx, &value).is_some_and(|value| value.is_zero()) { | ||
| return Err(SymbolicError::Unsupported("value-bearing cheatcode CALL")); | ||
| } | ||
|
|
@@ -1559,14 +1580,10 @@ impl SymbolicExecutor { | |
| out_size: BoundedCopySize, | ||
| ) -> Result<StepOutcome, SymbolicError> { | ||
| let mut candidates = state.world.symbolic_call_targets(&mut self.cx, executor)?; | ||
| candidates.push(SYMBOLIC_VM_COMPAT_ADDRESS); | ||
| candidates.extend(active_precompiles(executor.spec_id).as_map().addresses()); | ||
| candidates.sort(); | ||
| candidates.dedup(); | ||
| if candidates.is_empty() { | ||
| return Err(SymbolicError::Unsupported( | ||
| "symbolic CALL target has no known contract candidates", | ||
| )); | ||
| } | ||
|
|
||
| let candidate_constraints = candidates | ||
| .iter() | ||
|
|
@@ -1575,9 +1592,17 @@ impl SymbolicExecutor { | |
| SymBoolExpr::eq(&mut self.cx, target.clone(), address) | ||
| }) | ||
| .collect::<Vec<_>>(); | ||
| // Only the real cheatcode address uses Forge's decoder. The compatibility address is an | ||
| // ordinary call target unless its selector names a symbolic-VM operation. | ||
| let cheatcode_address = SymExpr::constant(&mut self.cx, address_word(CHEATCODE_ADDRESS)); | ||
| let cheatcode_condition = SymBoolExpr::eq(&mut self.cx, target.clone(), cheatcode_address); | ||
| let mut outside_constraints = state.constraints.clone(); | ||
| outside_constraints.extend( | ||
| candidate_constraints.iter().cloned().map(|condition| condition.not(&mut self.cx)), | ||
| candidate_constraints | ||
| .iter() | ||
| .chain([&cheatcode_condition]) | ||
| .cloned() | ||
| .map(|condition| condition.not(&mut self.cx)), | ||
| ); | ||
| let outside_sat = self.is_sat_with_state(state, &outside_constraints)?; | ||
|
|
||
|
|
@@ -1674,6 +1699,37 @@ impl SymbolicExecutor { | |
| } | ||
|
|
||
| let call_input = in_size.read_from_memory(&mut self.cx, &state.memory, in_offset.clone()); | ||
| let mut cheatcode_branch = state.clone(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cheatcode branch doesn't check the call kind. Forge only intercepts when |
||
| cheatcode_branch.constraints.push(cheatcode_condition); | ||
| if self.is_sat_with_state(&cheatcode_branch, &cheatcode_branch.constraints)? { | ||
| // The decoder error only depends on the selector, except for shorter calldata. | ||
| let input = if call_input.len() < 4 { | ||
| call_input.clone() | ||
| } else { | ||
| call_input.slice_concrete(&mut self.cx, 0, 4) | ||
| }; | ||
| let input = input.concrete_bytes(&mut self.cx, "symbolic cheatcode selector")?; | ||
| if let Ok(selector) = <[u8; 4]>::try_from(input.as_slice()) | ||
| && VmCalls::signature_by_selector(selector).is_some() | ||
| { | ||
| return Err(SymbolicError::Unsupported("symbolic cheatcode CALL target")); | ||
| } | ||
| let Err(error) = decode_cheatcode_call(&input) else { | ||
| return Err(SymbolicError::Unsupported("symbolic cheatcode CALL target")); | ||
| }; | ||
| // Like Forge, a cheatcode call leaves `vm.expectRevert` and `vm.assumeNoRevert` | ||
| // pending. | ||
| let return_data = error_string_return_data(&mut self.cx, &error.to_string()); | ||
| revert_cheatcode_call( | ||
| &mut self.cx, | ||
| &mut cheatcode_branch, | ||
| out_offset.clone(), | ||
| &out_size, | ||
| return_data, | ||
| )?; | ||
| parents.push_back(cheatcode_branch); | ||
| } | ||
|
|
||
| for (to, constraint) in candidates.into_iter().zip(candidate_constraints) { | ||
| let mut branch = state.clone(); | ||
| branch.constraints.push(constraint); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2184,9 +2184,7 @@ impl SymbolicWorld { | |
| let expr = address_expr; | ||
| let representative = expr.representative_symbolic_address(); | ||
| let mut result = self.balance_word_for_address(cx, executor, representative); | ||
| // Cheatcode accounts can hold balances even though they are not code candidates. | ||
| let cheatcode_addresses = [CHEATCODE_ADDRESS, SYMBOLIC_VM_COMPAT_ADDRESS]; | ||
| for address in self.known_addresses(executor).into_iter().chain(cheatcode_addresses) { | ||
| for address in self.known_addresses(executor) { | ||
| let balance = self.balance_word_for_address(cx, executor, address); | ||
| let address = SymExpr::constant(cx, address_word(address)); | ||
| let condition = SymBoolExpr::eq(cx, expr.clone(), address); | ||
|
|
@@ -2538,7 +2536,10 @@ impl SymbolicWorld { | |
| ) -> Result<Vec<Address>, SymbolicError> { | ||
| let mut targets = Vec::new(); | ||
| for address in self.known_addresses(executor) { | ||
| if is_precompile_for_spec(address, executor.spec_id) { | ||
| if is_precompile_for_spec(address, executor.spec_id) | ||
| || address == CHEATCODE_ADDRESS | ||
| || address == SYMBOLIC_VM_COMPAT_ADDRESS | ||
| { | ||
| continue; | ||
| } | ||
| if !self.extcode(cx, executor, address)?.is_empty() { | ||
|
|
@@ -2551,7 +2552,8 @@ impl SymbolicWorld { | |
| /// Returns the concrete accounts a symbolic address may alias, in address order. | ||
| /// | ||
| /// Includes accounts that exist only in the backend, such as contracts deployed in `setUp`, | ||
| /// which the world caches only once touched. Cheatcode addresses are excluded. | ||
| /// which the world caches only once touched, and the cheatcode addresses, which have code | ||
| /// and can hold balances. | ||
| fn known_addresses(&self, executor: &ExecutorView<'_>) -> Vec<Address> { | ||
| let mut addresses = self | ||
| .code_cache | ||
|
|
@@ -2560,9 +2562,7 @@ impl SymbolicWorld { | |
| .chain(&self.existing_accounts) | ||
| .chain(&executor.account_addresses) | ||
| .copied() | ||
| .filter(|address| { | ||
| *address != CHEATCODE_ADDRESS && *address != SYMBOLIC_VM_COMPAT_ADDRESS | ||
| }) | ||
| .chain([CHEATCODE_ADDRESS, SYMBOLIC_VM_COMPAT_ADDRESS]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the high-level CALL regression for the new candidates Including these addresses makes Solidity's |
||
| .collect::<Vec<_>>(); | ||
| addresses.sort_unstable(); | ||
| addresses.dedup(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Execute the installed code for ordinary compatibility calls
Unknown selectors now fall through to normal CALL execution, but that path loads its child code through
SymbolicWorld::extcode, which still unconditionally returns a synthetic STOP byte for the compatibility address before consultingcode_cacheor the backend. For example,vm.etch(COMPAT, hex"60006000fd"); (bool ok,) = COMPAT.call(hex"5c36b186"); assert(ok);now executes STOP symbolically and can pass, although concrete Forge executes the installed REVERT and fails the assertion. Previously this unknown compatibility selector reported incomplete. Please make this ordinary-call path honor the account's installed code, or fail closed when that code cannot be modeled.