Refs #46
PR: feat/21-mempool-monitor
File: crates/charon-scanner/src/mempool.rs
Function: PendingCache::drain()
Problem
PendingCache::drain() removes and returns every non-stale entry on each call. It is called by the block-listener task on each ChainEvent::NewBlock. The drain logic does NOT verify that PreSignedLiquidation::trigger_tx was actually included in the just-confirmed block.
Scenario:
- Block N-1: oracle update tx seen in mempool, pre-sign created for borrower X.
- Block N: oracle update tx is NOT included (validator skipped it or it was replaced).
- Block N fires NewBlock, drain() returns the pre-sign for borrower X.
- Caller broadcasts against state where price has NOT changed.
- Borrower X may still be healthy; liquidation tx reverts or worse succeeds incorrectly.
The 30-second TTL only drops entries older than 30s. It does not enforce the semantic "only broadcast if trigger_tx confirmed in the block that caused drain()."
Required fix
Before returning a drained entry for broadcast, verify either:
(a) eth_getTransactionReceipt(trigger_tx).status == 1 for the block that just arrived, or
(b) re-read the current oracle price and confirm it matches OracleUpdate.new_price.
A drain_for_block(block_hash) variant that checks trigger_tx receipt before including entries in the returned vec is one concrete approach. Entries whose trigger_tx did not confirm are re-inserted (if within TTL) rather than returned for broadcast.
Refs #46
PR: feat/21-mempool-monitor
File: crates/charon-scanner/src/mempool.rs
Function: PendingCache::drain()
Problem
PendingCache::drain() removes and returns every non-stale entry on each call. It is called by the block-listener task on each ChainEvent::NewBlock. The drain logic does NOT verify that PreSignedLiquidation::trigger_tx was actually included in the just-confirmed block.
Scenario:
The 30-second TTL only drops entries older than 30s. It does not enforce the semantic "only broadcast if trigger_tx confirmed in the block that caused drain()."
Required fix
Before returning a drained entry for broadcast, verify either:
(a) eth_getTransactionReceipt(trigger_tx).status == 1 for the block that just arrived, or
(b) re-read the current oracle price and confirm it matches OracleUpdate.new_price.
A drain_for_block(block_hash) variant that checks trigger_tx receipt before including entries in the returned vec is one concrete approach. Entries whose trigger_tx did not confirm are re-inserted (if within TTL) rather than returned for broadcast.