PR: #41 (feat/executor: transaction builder + eth_call simulator)
File: crates/charon-executor/src/simulation.rs — simulate()
Refs #41
Problem
The eth_call request in simulate() has no gas field:
let req = TransactionRequest::default()
.from(self.sender)
.to(self.liquidator)
.input(calldata.into());
Without gas, the BSC node defaults to the block gas limit (~30M) for the call. The simulation succeeds even if the gas_limit set in build_tx is insufficient to complete the flash-loan callback (Venus liquidateBorrow + vToken redemption + PancakeSwap V3 swap costs roughly 600k–900k gas). A liquidation passes simulation, gets signed, then reverts on-chain with OOG in the Aave callback. Aave reverts the entire transaction. No funds are lost but the opportunity is wasted.
CLAUDE.md safety invariant
"Every liquidation transaction passes an eth_call simulation gate before broadcast." A gate that ignores gas does not faithfully simulate what broadcast will execute.
Fix
Accept gas_limit in simulate() and forward it to the request:
pub async fn simulate<P>(
&self,
provider: &P,
calldata: Bytes,
gas_limit: u64,
) -> Result<()>
where
P: Provider,
{
let req = TransactionRequest::default()
.from(self.sender)
.to(self.liquidator)
.input(calldata.into())
.with_gas_limit(gas_limit);
// ...
}
PR: #41 (feat/executor: transaction builder + eth_call simulator)
File: crates/charon-executor/src/simulation.rs — simulate()
Refs #41
Problem
The eth_call request in simulate() has no gas field:
Without gas, the BSC node defaults to the block gas limit (~30M) for the call. The simulation succeeds even if the gas_limit set in build_tx is insufficient to complete the flash-loan callback (Venus liquidateBorrow + vToken redemption + PancakeSwap V3 swap costs roughly 600k–900k gas). A liquidation passes simulation, gets signed, then reverts on-chain with OOG in the Aave callback. Aave reverts the entire transaction. No funds are lost but the opportunity is wasted.
CLAUDE.md safety invariant
"Every liquidation transaction passes an eth_call simulation gate before broadcast." A gate that ignores gas does not faithfully simulate what broadcast will execute.
Fix
Accept gas_limit in simulate() and forward it to the request: