Refs #46
PR: feat/21-mempool-monitor
File: crates/charon-scanner/src/mempool.rs
Types: PreSignedLiquidation, PendingCache::drain()
CLAUDE.md safety invariant (hard rule):
"Every liquidation transaction passes an eth_call simulation gate before broadcast."
Problem
PreSignedLiquidation.raw_tx is a fully-signed EIP-2718 envelope produced by TxBuilder::sign and stored ready for eth_sendRawTransaction. The PR's stated flow is:
- See pending oracle tx in mempool.
- Predict post-update state off-chain.
- Pre-sign liquidation against predicted state.
- Store in PendingCache.
- On NewBlock: drain cache then broadcast.
Step 5 contains no eth_call simulation. The signed tx was built against a predicted price that may never materialise: the oracle tx could revert, be replaced via EIP-1559, or simply not be included in the next block. The existing simulation gate in charon-executor runs eth_call against current confirmed state before eth_sendRawTransaction. That gate is structurally bypassed here: the raw bytes are signed before the triggering oracle tx confirms, and drained and broadcast without re-simulation.
Risk
Broadcasting a liquidation tx against a state that never materialised causes gas loss on revert, possible liquidation of a still-healthy position, and direct violation of the CLAUDE.md hard invariant.
Required fix
Option A: drain_candidates() returns entries; the wiring code must run eth_call simulation against the now-confirmed block state before calling eth_sendRawTransaction. Enforce via type wrapper that requires a SimResult before unwrapping raw_tx.
Option B: PendingCache stores an unsigned LiquidationOpportunity plus signer reference. Signing and simulation happen together in the drainer after block confirmation, in a single eth_call then sign then send sequence.
This PR must not be merged while the simulation invariant is violated.
Refs #46
PR: feat/21-mempool-monitor
File: crates/charon-scanner/src/mempool.rs
Types: PreSignedLiquidation, PendingCache::drain()
CLAUDE.md safety invariant (hard rule):
"Every liquidation transaction passes an eth_call simulation gate before broadcast."
Problem
PreSignedLiquidation.raw_tx is a fully-signed EIP-2718 envelope produced by TxBuilder::sign and stored ready for eth_sendRawTransaction. The PR's stated flow is:
Step 5 contains no eth_call simulation. The signed tx was built against a predicted price that may never materialise: the oracle tx could revert, be replaced via EIP-1559, or simply not be included in the next block. The existing simulation gate in charon-executor runs eth_call against current confirmed state before eth_sendRawTransaction. That gate is structurally bypassed here: the raw bytes are signed before the triggering oracle tx confirms, and drained and broadcast without re-simulation.
Risk
Broadcasting a liquidation tx against a state that never materialised causes gas loss on revert, possible liquidation of a still-healthy position, and direct violation of the CLAUDE.md hard invariant.
Required fix
Option A: drain_candidates() returns entries; the wiring code must run eth_call simulation against the now-confirmed block state before calling eth_sendRawTransaction. Enforce via type wrapper that requires a SimResult before unwrapping raw_tx.
Option B: PendingCache stores an unsigned LiquidationOpportunity plus signer reference. Signing and simulation happen together in the drainer after block confirmation, in a single eth_call then sign then send sequence.
This PR must not be merged while the simulation invariant is violated.