Summary
NonceManager::resync() re-fetches the on-chain pending transaction count and resets the local atomic counter to that value. This is incorrect when a tx was broadcast successfully (reached at least one BSC node) but the originating RPC call returned a transport error — common with BSC's high-redundancy public RPC pools where a request times out but the tx has already propagated.
Sequence of failure:
next() issues nonce N, tx is signed and sent
- RPC returns a timeout error on the broadcast call
resync() fires, fetches pending_count = N (tx not yet reflected in pending state)
next() issues nonce N again for the next opportunity
- Two signed liquidation txs with nonce N are now in the mempool — whichever has higher
max_fee wins, the other is silently dropped
The fix is a high-water mark: the maximum nonce ever issued by next(). resync should set the counter to max(on_chain_pending, high_water + 1).
File
crates/charon-executor/src/nonce.rs — NonceManager::resync()
Fix
pub struct NonceManager {
nonce: Arc<AtomicU64>,
high_water: Arc<AtomicU64>, // highest nonce ever issued by next()
}
pub fn next(&self) -> u64 {
let n = self.nonce.fetch_add(1, Ordering::SeqCst);
self.high_water.fetch_max(n, Ordering::SeqCst);
n
}
pub async fn resync(&self, provider: &impl Provider) -> Result<(), NonceError> {
let on_chain = /* pending count */;
let hw = self.high_water.load(Ordering::SeqCst);
let safe = on_chain.max(hw + 1);
self.nonce.store(safe, Ordering::SeqCst);
Ok(())
}
Refs #43
Summary
NonceManager::resync()re-fetches the on-chain pending transaction count and resets the local atomic counter to that value. This is incorrect when a tx was broadcast successfully (reached at least one BSC node) but the originating RPC call returned a transport error — common with BSC's high-redundancy public RPC pools where a request times out but the tx has already propagated.Sequence of failure:
next()issues nonce N, tx is signed and sentresync()fires, fetchespending_count = N(tx not yet reflected in pending state)next()issues nonce N again for the next opportunitymax_feewins, the other is silently droppedThe fix is a high-water mark: the maximum nonce ever issued by
next().resyncshould set the counter tomax(on_chain_pending, high_water + 1).File
crates/charon-executor/src/nonce.rs—NonceManager::resync()Fix
Refs #43