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 2 commits
925ef66
56106c7
40506a5
c4f9c74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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( | ||
|
|
@@ -1575,9 +1576,20 @@ impl SymbolicExecutor { | |
| SymBoolExpr::eq(&mut self.cx, target.clone(), address) | ||
| }) | ||
| .collect::<Vec<_>>(); | ||
| // The cheatcode addresses have code, so an unconstrained target may alias them. They are | ||
| // not call candidates: Forge reverts a call to them with an unknown selector. | ||
| let cheatcode_constraints = | ||
| [CHEATCODE_ADDRESS, SYMBOLIC_VM_COMPAT_ADDRESS].map(|address| { | ||
| let address = SymExpr::constant(&mut self.cx, address_word(address)); | ||
| SymBoolExpr::eq(&mut self.cx, target.clone(), 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_constraints) | ||
| .cloned() | ||
| .map(|condition| condition.not(&mut self.cx)), | ||
| ); | ||
| let outside_sat = self.is_sat_with_state(state, &outside_constraints)?; | ||
|
|
||
|
|
@@ -1674,6 +1686,38 @@ 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 |
||
| let is_cheatcode = SymBoolExpr::or(&mut self.cx, cheatcode_constraints.to_vec()); | ||
| cheatcode_branch.constraints.push(is_cheatcode); | ||
| if self.is_sat_with_state(&cheatcode_branch, &cheatcode_branch.constraints)? { | ||
| let selector = call_input | ||
| .slice_concrete(&mut self.cx, 0, 4) | ||
| .concrete_bytes(&mut self.cx, "symbolic cheatcode selector")?; | ||
| if let Ok(selector) = <[u8; 4]>::try_from(selector) | ||
| && VmCalls::signature_by_selector(selector).is_some() | ||
| { | ||
| return Err(SymbolicError::Unsupported("symbolic cheatcode CALL target")); | ||
| } | ||
| let return_data = SymReturnData::empty(&mut self.cx); | ||
|
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. Preserve the unknown-selector revert payload Concrete Forge returns ABI-encoded |
||
| match self.finish_codeless_call( | ||
|
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. Keep revert expectations pending across cheatcode calls
|
||
| executor, | ||
| &mut cheatcode_branch, | ||
| kind, | ||
| CallStatus::Revert, | ||
| return_data, | ||
| CHEATCODE_ADDRESS, | ||
| out_offset.clone(), | ||
| &out_size, | ||
| )? { | ||
| StepOutcome::Continue => parents.push_back(cheatcode_branch), | ||
| StepOutcome::AssumeRejected => {} | ||
| outcome => { | ||
| *state = cheatcode_branch; | ||
| return Ok(outcome); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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.
Separate compatibility aliases from Forge cheatcode decoding
SYMBOLIC_VM_COMPAT_ADDRESSis not intercepted by the concrete Forge cheatcode inspector: in an ordinary unforked test it has no code, so a low-levelping()call succeeds. Grouping it withCHEATCODE_ADDRESSsends it throughdecode_cheatcode_calland forces a revert instead. For example, constraintargetto the compatibility address or a deployed contract with a reverting fallback, calltarget.call(abi.encodeWithSignature("ping()")), and assert!success: both branches now revert symbolically, while the compatibility-address input concretely succeeds and violates the assertion. The existing engine with--symbolic-call-targetsfinds and replays exactly that counterexample. Please split the compatibility-address handling and report incomplete if its semantics cannot be modeled, rather than fabricating a Forge decoder revert.