PR: #41 (feat/executor: transaction builder + eth_call simulator)
File: crates/charon-executor/src/simulation.rs, crates/charon-executor/src/builder.rs
Refs #41
Problem
Simulator::new(sender, liquidator) and TxBuilder::new(signer, chain_id, liquidator) are constructed independently. Nothing prevents a caller from passing a different sender to the simulator than the signer address used in build_tx.
CharonLiquidator.executeLiquidation is onlyOwner. If Simulator::sender is any address other than the deployed contract's owner (e.g., zero address, wrong address, stale address after wallet rotation), every simulate() call will revert on the onlyOwner modifier and return Err. The pipeline drops every opportunity — not because the liquidation would fail, but because the simulation called from the wrong address.
The inverse failure: sender coincidentally passes onlyOwner but does not match the signer key used in build_tx. Simulation passes, real broadcast fails onlyOwner at submission.
Impact
Either all opportunities are silently dropped (simulation always fails) or simulation gives a false-pass signal (wrong sender passes onlyOwner). Both break the CLAUDE.md invariant that the eth_call gate must faithfully gate the real broadcast.
Fix
Expose a factory method that derives Simulator directly from TxBuilder to eliminate the wiring gap:
impl Simulator {
pub fn from_builder(builder: &TxBuilder, liquidator: Address) -> Self {
Self {
sender: builder.signer_address(),
liquidator,
}
}
}
Add a debug-mode assertion that sender != Address::ZERO. Document in rustdoc that sender must equal the CharonLiquidator owner.
PR: #41 (feat/executor: transaction builder + eth_call simulator)
File: crates/charon-executor/src/simulation.rs, crates/charon-executor/src/builder.rs
Refs #41
Problem
Simulator::new(sender, liquidator) and TxBuilder::new(signer, chain_id, liquidator) are constructed independently. Nothing prevents a caller from passing a different sender to the simulator than the signer address used in build_tx.
CharonLiquidator.executeLiquidation is onlyOwner. If Simulator::sender is any address other than the deployed contract's owner (e.g., zero address, wrong address, stale address after wallet rotation), every simulate() call will revert on the onlyOwner modifier and return Err. The pipeline drops every opportunity — not because the liquidation would fail, but because the simulation called from the wrong address.
The inverse failure: sender coincidentally passes onlyOwner but does not match the signer key used in build_tx. Simulation passes, real broadcast fails onlyOwner at submission.
Impact
Either all opportunities are silently dropped (simulation always fails) or simulation gives a false-pass signal (wrong sender passes onlyOwner). Both break the CLAUDE.md invariant that the eth_call gate must faithfully gate the real broadcast.
Fix
Expose a factory method that derives Simulator directly from TxBuilder to eliminate the wiring gap:
Add a debug-mode assertion that sender != Address::ZERO. Document in rustdoc that sender must equal the CharonLiquidator owner.