diff --git a/modules/twap-monitor/src/keeper.rs b/modules/twap-monitor/src/keeper.rs index c56b7979..2b99141d 100644 --- a/modules/twap-monitor/src/keeper.rs +++ b/modules/twap-monitor/src/keeper.rs @@ -1195,6 +1195,64 @@ mod tests { }); } + #[test] + fn removal_before_its_create_self_heals_via_the_first_poll() { + // The exact race module.toml's own comment warns about: the two + // log streams merge in arrival order, not chain order, so a + // removal can reach `on_chain_logs` before the create it + // cancels. `removal_of_an_unindexed_watch_is_a_no_op` already + // confirms the removal alone is silently dropped in that case - + // this chains the rest of the story: the create that follows + // persists the watch anyway, with no memory of the earlier + // removal, and the very first poll must still tear it down. + // + // The revert used here is `SingleOrderNotAuthed()` - the actual + // selector the deployed `ComposableCow._auth` reverts with for a + // removed order (nullislabs/composable-cow/src/ComposableCow.sol), + // not one of the five legacy `IConditionalOrder` reasons + // `poll_invalid_drops_watch_and_gates` already covers. + // `LegacyRevertAdapter::classify`'s unrecognised-selector + // fallback is what has to catch this one. + let host = MockHost::new(); + let venue = MockVenue::default(); + let owner = address!("0011223344556677889900AABBCCDDEEFF001122"); + let params = sample_params(); + let hash = keccak256(params.abi_encode()); + let key = watch_key(&owner, &hash); + + on_chain_logs(&host, &[make_removed_log(owner, hash, at(2, 0))]).unwrap(); + assert_eq!( + host.store.len(), + 0, + "removal of an unindexed watch is a no-op" + ); + + on_chain_logs(&host, &[make_log(owner, ¶ms, at(1, 0))]).unwrap(); + assert!( + host.store.snapshot().contains_key(&key), + "the create persists the watch with no memory of the earlier removal", + ); + + let selector = alloy_primitives::keccak256(b"SingleOrderNotAuthed()")[..4].to_vec(); + host.chain.respond_to( + "eth_call", + programmed_eth_call_params(owner, ¶ms), + Err(ChainError::Rpc(nexum_sdk::host::RpcError { + code: 3, + message: "execution reverted".into(), + data: Some(selector.into()), + })), + ); + + dispatch(&host, &venue, sample_block(1_000)).unwrap(); + + assert!( + !host.store.snapshot().contains_key(&key), + "the first poll against a chain-dead order must self-heal the stale watch", + ); + assert_eq!(venue.submit_count(), 0, "a dead order is never submitted"); + } + /// The supervisor builds log filters from this manifest's chain-log /// `event_signature` pins, so a drift from a decoder topic-0 /// subscribes to one topic and decodes another. Compares the two