Skip to content

Commit bd3fa41

Browse files
committed
fix(swap): stage discovery against registered contracts, unbreak override seam
Pre-push went red three ways at HEAD; this restores every gate. Behavior fixes (each pinned by an existing-but-RED test): 1. Tracked-pool CL sim beyond full-range liquidity: the staged discovery walk (swap_missing_words) built its transient with Sparse coverage unconditionally, so an empty-range word crossing fabricated missing words a Tracked pool never misses; with no stored fetcher the staged pass failed and the seam returned None (LiquidityPoolError on the V4 ETH/USDC golden parity test at amount 1e30, boundary 654751007996983783903/904 wei). The discovery transients now mirror the REGISTERED pool's coverage AND its checked-word set (T1 3WTDFK: a caller-checked empty word is never a fetch target), factored into v3/v4_discovery_transient helpers. Fixes test_uniswap_v4_onchain_parity + test_sparse_checked_zero_word_prevents_refetch. 2. Override seam: sim_override_inner aborted on a None stored fetcher BEFORE inspecting the missing-word list, so any pool without a fetcher could never sim an override at all. A complete hypothetical now sims; an absent fetcher only aborts when the walk found real misses (same shape as the disarmed FetchExhausted arm). Fixes test_calculate_tokens_out_with_override. 3. clippy --deny warnings: ~40 violations left by the RATR5A/CXRHW3 refactor (stale #[expect]s, doc-markdown backticks, clone-on-Copy, missing # Errors/must_use) plus a file-level ![allow(dead_code)] in cpu_budget.rs tripping the inner-allow guard. Verified: just pre-push green end to end - clippy --deny warnings, 2574 pytest passed / 20 skipped (incl. golden V4 quoter parity and the checked-word FFI invariants).
1 parent aeb26dd commit bd3fa41

8 files changed

Lines changed: 144 additions & 99 deletions

File tree

rust/crates/degenbot-bot/src/bot_core/cl_orchestration.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ impl StagedWordFetch {
5252
/// re-enters Python (`Python::attach` + the companion's web3 RPC), so
5353
/// this call is the multi-second window the fetch-under-write defect
5454
/// parked the whole pump inside (RATR5A).
55+
///
56+
/// # Errors
57+
/// Propagates the stored fetcher's [`FetchTickWordError`] (RPC/transport
58+
/// or tick-word decode); the caller owns retry semantics, nothing panics.
5559
pub fn fetch(
5660
&self,
5761
) -> Result<
@@ -1096,7 +1100,7 @@ impl BotState {
10961100

10971101
/// RATR5A stage half of the word backfill: clone the stored fetcher +
10981102
/// capture the pool's tick fingerprint UNDER a short write, and release
1099-
/// the caller's guard before the (multi-second, Python::attach + web3
1103+
/// the caller's guard before the (multi-second, `Python::attach` + web3
11001104
/// RPC) fetch runs. The old single-hold path fetched while the write
11011105
/// guard was alive, parking the pump’s apply/solve pipeline behind the
11021106
/// RPC. Pair with [`Self::install_word_fetch`].

rust/crates/degenbot-bot/src/bot_core/cpu_budget.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
//! Python, the pump, and the `OTel` exporter share the quota and must not
1717
//! starve during bursts), overridable with `DEGENBOT_SOLVE_CPUS`.
1818
19-
#![allow(dead_code)] // v1 helpers are exercised by tests; some hosts never hit them
20-
2119
use std::path::{Path, PathBuf};
2220
use std::sync::OnceLock;
2321

rust/crates/degenbot-bot/src/bot_core/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5214,7 +5214,7 @@ mod tests {
52145214
// --- T2 (FBJTUM): write-path sparse backfill — ensure_word_known ---
52155215

52165216
#[test]
5217-
#[expect(clippy::expect_used, clippy::indexing_slicing)]
5217+
#[expect(clippy::expect_used, clippy::too_many_lines)]
52185218
fn staged_word_fetch_install_races_on_interleaved_pool_write() {
52195219
use crate::bot_core::InstallWordOutcome;
52205220
use ::degenbot_pools::tick_fetch::{FetchedTickWord, TickWordFetcher};

rust/crates/degenbot-bot/src/bot_core/sim_anchor.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,29 @@ mod tests {
109109
anchor
110110
.anchor_words
111111
.get(key)
112-
.map(|p| (p.kind.clone(), p.engine_word.into(), p.update_block))
112+
.map(|p| (p.kind, p.engine_word.into(), p.update_block))
113113
}
114114

115115
const V4_TEST_PM: Address = Address::new([0x44; 20]);
116116

117+
#[expect(clippy::cast_possible_truncation)] // fixture: i < 2 pools
117118
fn v3_addr(i: usize) -> Address {
118119
let mut a = [0x33u8; 20];
119120
a[0] = (i as u8) + 1;
120121
Address::new(a)
121122
}
122123

124+
#[expect(clippy::cast_possible_truncation)] // fixture: i, j < 2 pools
123125
fn v4_id(i: usize) -> [u8; 32] {
124126
std::array::from_fn(|j| (i as u8).wrapping_add(j as u8))
125127
}
126128

129+
#[expect(
130+
clippy::expect_used,
131+
clippy::unwrap_used,
132+
clippy::cast_possible_truncation,
133+
clippy::cast_possible_wrap
134+
)]
127135
fn heavy_state(v3_pools: usize, v3_ticks: usize, v4_pools: usize) -> BotState {
128136
let mut core = BotState::new();
129137
core.register_v2_pool(&crate::bot_core::RegisterV2PoolParams {
@@ -200,7 +208,7 @@ mod tests {
200208
}
201209

202210
#[test]
203-
#[expect(clippy::expect_used, clippy::too_many_lines)]
211+
#[expect(clippy::unwrap_used, clippy::panic, clippy::type_complexity)]
204212
fn snapshot_parity_with_query_semantics() {
205213
// Fixture: one pool of each family (Aerodrome/Curve/Balancer families
206214
// contribute no anchor words by probe semantics — pass-through None).
@@ -219,10 +227,7 @@ mod tests {
219227
(V4_TEST_PM, s_state.checked_add(U256::from(3u64)).unwrap()),
220228
] {
221229
if let Some(p) = core.probe_tracked_storage_slot(addr, idx) {
222-
expected.push((
223-
(addr, idx),
224-
(p.kind.clone(), p.engine_word.into(), p.update_block),
225-
));
230+
expected.push(((addr, idx), (p.kind, p.engine_word.into(), p.update_block)));
226231
}
227232
}
228233
assert!(
@@ -264,7 +269,6 @@ mod tests {
264269
}
265270

266271
#[test]
267-
#[expect(clippy::too_many_lines)]
268272
fn snapshot_is_enumerated_not_a_scan() {
269273
// The ADR-039 perf gate: the projection enumerates per-family scalars
270274
// and NEVER iterates tick maps (V3 arbitrary-index fallthrough) or the

rust/crates/degenbot-bot/src/bot_core/swap_simulation.rs

Lines changed: 94 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ use ::degenbot_pools::registry::PoolEntry;
3434
use ::degenbot_pools::simulate_swap::simulate_swap;
3535
use ::degenbot_pools::tick_fetch::{FetchedTickWord, TickWordFetcher};
3636
use ::degenbot_pools::v3_state::{
37-
v3_simulate_swap, PoolTickCoverage, RegisterV3PoolParams, SimulateSwapError, V3PoolState,
38-
V3SwapOutcome,
37+
v3_simulate_swap, PoolTickCoverage, RegisterV3PoolParams, SimulateSwapError, V3PoolIdentity,
38+
V3PoolState, V3SwapOutcome,
39+
};
40+
use ::degenbot_pools::v4_state::{
41+
v4_simulate_swap, RegisterV4PoolParams, V4PoolIdentity, V4PoolState,
3942
};
40-
use ::degenbot_pools::v4_state::{v4_simulate_swap, RegisterV4PoolParams, V4PoolState};
4143

4244
use super::BotState;
4345

@@ -394,7 +396,7 @@ struct OverrideSim<'a> {
394396
fetcher: Option<Arc<dyn TickWordFetcher>>,
395397
pool_id: u64,
396398
/// RATR5A/CXRHW3: miss recovery DISARMED - a fresh missing word
397-
/// surfaces as FetchExhausted (typed contract) instead of an inline
399+
/// surfaces as `FetchExhausted` (typed contract) instead of an inline
398400
/// fetch under the caller read guard.
399401
disarm_fetch: bool,
400402
}
@@ -583,33 +585,7 @@ impl BotState {
583585
| PoolEntry::AerodromeV2(..) => Some(Vec::new()),
584586
PoolEntry::V3(identity, st) => {
585587
let mut missing = Vec::new();
586-
let mut transient = TransientCl {
587-
family: TransientFamily::V3(identity.fee, identity.tick_spacing),
588-
inner: TransientInner::V3(Box::new(
589-
V3PoolState::from_params(
590-
RegisterV3PoolParams {
591-
address: identity.address,
592-
token0: identity.token0,
593-
token1: identity.token1,
594-
fee: identity.fee,
595-
tick_spacing: identity.tick_spacing,
596-
factory: identity.factory,
597-
deployer: identity.deployer,
598-
init_hash: identity.init_hash,
599-
sqrt_price_x96: st.sqrt_price_x96,
600-
liquidity: st.liquidity,
601-
tick: st.tick,
602-
tick_data: st.tick_data.clone(),
603-
update_block: st.update_block,
604-
coverage: PoolTickCoverage::Sparse,
605-
fetcher: None,
606-
..Default::default()
607-
},
608-
self.journal_depth,
609-
)
610-
.1,
611-
)),
612-
};
588+
let mut transient = self.v3_discovery_transient(identity, st);
613589
let spec =
614590
engine_amount_specified(request.amount_specified, EngineFamily::V3Engine);
615591
let limit = request
@@ -626,33 +602,7 @@ impl BotState {
626602
}
627603
PoolEntry::V4(identity, st) => {
628604
let mut missing = Vec::new();
629-
let mut transient = TransientCl {
630-
family: TransientFamily::V4(
631-
identity.pool_key.fee,
632-
identity.pool_key.tick_spacing,
633-
),
634-
inner: TransientInner::V4(Box::new(
635-
V4PoolState::from_params(
636-
RegisterV4PoolParams {
637-
pool_manager: identity.pool_manager,
638-
pool_id: identity.pool_id,
639-
pool_key: identity.pool_key.clone(),
640-
hook_flags: 0,
641-
protocol_fee: 0,
642-
sqrt_price_x96: st.sqrt_price_x96,
643-
liquidity: st.liquidity,
644-
tick: st.tick,
645-
tick_data: st.tick_data.clone(),
646-
update_block: st.update_block,
647-
tick_data_block: None,
648-
coverage: PoolTickCoverage::Sparse,
649-
fetcher: None,
650-
},
651-
self.journal_depth,
652-
)
653-
.1,
654-
)),
655-
};
605+
let mut transient = self.v4_discovery_transient(identity, st);
656606
let spec =
657607
engine_amount_specified(request.amount_specified, EngineFamily::V4Engine);
658608
let limit = request
@@ -671,6 +621,75 @@ impl BotState {
671621
}
672622
}
673623

624+
/// Build the V3 discovery transient for a registered pool: it mirrors
625+
/// the REGISTERED pool's coverage (a Tracked pool never raises
626+
/// `MissingTickWord`, so the staging walk must not invent fetch work
627+
/// for one) AND its checked-word set (T1 3WTDFK: `from_params` seeds
628+
/// known words from tick ROWS only — a caller-CHECKED empty word has no
629+
/// rows yet must never become a fetch target).
630+
fn v3_discovery_transient(&self, identity: &V3PoolIdentity, st: &V3PoolState) -> TransientCl {
631+
let mut v3_transient = V3PoolState::from_params(
632+
RegisterV3PoolParams {
633+
address: identity.address,
634+
token0: identity.token0,
635+
token1: identity.token1,
636+
fee: identity.fee,
637+
tick_spacing: identity.tick_spacing,
638+
factory: identity.factory,
639+
deployer: identity.deployer,
640+
init_hash: identity.init_hash,
641+
sqrt_price_x96: st.sqrt_price_x96,
642+
liquidity: st.liquidity,
643+
tick: st.tick,
644+
tick_data: st.tick_data.clone(),
645+
update_block: st.update_block,
646+
coverage: st.coverage,
647+
fetcher: None,
648+
..Default::default()
649+
},
650+
self.journal_depth,
651+
)
652+
.1;
653+
v3_transient
654+
.known_bitmap_words
655+
.extend(st.known_bitmap_words.iter().copied());
656+
TransientCl {
657+
family: TransientFamily::V3(identity.fee, identity.tick_spacing),
658+
inner: TransientInner::V3(Box::new(v3_transient)),
659+
}
660+
}
661+
662+
/// V4 twin of [`Self::v3_discovery_transient`] — the same coverage +
663+
/// checked-word mirroring discipline (see that doc).
664+
fn v4_discovery_transient(&self, identity: &V4PoolIdentity, st: &V4PoolState) -> TransientCl {
665+
let mut v4_transient = V4PoolState::from_params(
666+
RegisterV4PoolParams {
667+
pool_manager: identity.pool_manager,
668+
pool_id: identity.pool_id,
669+
pool_key: identity.pool_key.clone(),
670+
hook_flags: 0,
671+
protocol_fee: 0,
672+
sqrt_price_x96: st.sqrt_price_x96,
673+
liquidity: st.liquidity,
674+
tick: st.tick,
675+
tick_data: st.tick_data.clone(),
676+
update_block: st.update_block,
677+
tick_data_block: None,
678+
coverage: st.coverage,
679+
fetcher: None,
680+
},
681+
self.journal_depth,
682+
)
683+
.1;
684+
v4_transient
685+
.known_bitmap_words
686+
.extend(st.known_bitmap_words.iter().copied());
687+
TransientCl {
688+
family: TransientFamily::V4(identity.pool_key.fee, identity.pool_key.tick_spacing),
689+
inner: TransientInner::V4(Box::new(v4_transient)),
690+
}
691+
}
692+
674693
/// The stored word fetcher for a registered pool, if any — the staged
675694
/// pre-pass fetches through it OUTSIDE any state lock.
676695
#[must_use]
@@ -731,7 +750,7 @@ impl BotState {
731750
/// RATR5A/CXRHW3: the override sim with miss recovery DISARMED - a
732751
/// missing word surfaces as None (the legacy Option contract) instead of
733752
/// an inline fetch under the caller read guard. The pooled caller
734-
/// pre-stages through [Self::override_missing_words] + the lock-free
753+
/// pre-stages through [`Self::override_missing_words`] + the lock-free
735754
/// fetch choreography before entering.
736755
pub fn simulate_override_disarmed(
737756
&self,
@@ -745,7 +764,7 @@ impl BotState {
745764
/// listed by a collect-only walk over a transient built from the
746765
/// override scalars + the caller tick data (no fetch, no registered
747766
/// mutation). Pair with the lock-free fetch choreography in pool.rs
748-
/// (ensure_override_missing_staged).
767+
/// (`ensure_override_missing_staged`).
749768
#[must_use]
750769
pub fn override_missing_words(&self, over: &OverrideSwap) -> Option<Vec<i32>> {
751770
if over.request.amount_specified.is_zero() {
@@ -843,6 +862,7 @@ impl BotState {
843862
}
844863
}
845864

865+
#[must_use]
846866
pub fn simulate_override(&self, over: &OverrideSwap, block: u64) -> Option<V3SwapOutcome> {
847867
self.simulate_override_ext(over, block, false)
848868
}
@@ -960,17 +980,13 @@ impl BotState {
960980
///
961981
/// `block` is the fetch context threaded into the tick-word fetcher on
962982
/// sparse-miss recovery; it does not affect pure computation.
963-
// TODO(X4EU3J follow-up): extract the per-family arms once the hook
964-
// caveat plumbing settles; the body is 101 lines against a 100-line
965-
// clippy::too_many_lines budget.
966-
#[expect(clippy::too_many_lines)]
967983
pub fn swap_simulation(&mut self, block: u64, pool_id: u64, request: SwapRequest) -> SwapRead {
968984
self.swap_simulation_ext(block, pool_id, &request, false)
969985
}
970986

971987
/// RATR5A/CXRHW3: the swap with miss recovery DISARMED — a residual
972988
/// missing word after the staged pre-pass surfaces as the typed
973-
/// FetchExhausted contract (additive, ADR-037) instead of fetching under
989+
/// `FetchExhausted` contract (additive, ADR-037) instead of fetching under
974990
/// the caller write guard. Identical arithmetic otherwise (the caller
975991
/// must have staged the missing words via the lock-free pre-pass).
976992
pub fn swap_simulation_disarmed(
@@ -982,6 +998,10 @@ impl BotState {
982998
self.swap_simulation_ext(block, pool_id, request, true)
983999
}
9841000

1001+
// TODO(X4EU3J follow-up): extract the per-family arms once the hook
1002+
// caveat plumbing settles; the body is 114 lines against a 100-line
1003+
// clippy::too_many_lines budget.
1004+
#[expect(clippy::too_many_lines)]
9851005
fn swap_simulation_ext(
9861006
&mut self,
9871007
block: u64,
@@ -1078,7 +1098,7 @@ impl BotState {
10781098
block,
10791099
SwapOutcomeFamily::V3,
10801100
coverage,
1081-
request.clone(),
1101+
*request,
10821102
Caveats::default(),
10831103
)
10841104
}
@@ -1108,7 +1128,7 @@ impl BotState {
11081128
block,
11091129
SwapOutcomeFamily::V4,
11101130
coverage,
1111-
request.clone(),
1131+
*request,
11121132
extra_caveats,
11131133
)
11141134
}
@@ -1148,7 +1168,12 @@ fn finish_cl(
11481168

11491169
#[cfg(test)]
11501170
mod tests {
1151-
#![expect(clippy::panic, clippy::unwrap_used, clippy::used_underscore_binding)]
1171+
#![expect(
1172+
clippy::panic,
1173+
clippy::unwrap_used,
1174+
clippy::expect_used,
1175+
clippy::used_underscore_binding
1176+
)]
11521177

11531178
use super::*;
11541179
use hashbrown::HashMap;
@@ -1160,7 +1185,6 @@ mod tests {
11601185

11611186
/// Registered sparse pool whose stored fetcher counts every call.
11621187
fn counting_fetcher_setup(calls: Arc<AtomicUsize>) -> (BotState, u64) {
1163-
let mut core = BotState::new();
11641188
#[derive(Debug)]
11651189
struct CountingFetcher(Arc<AtomicUsize>);
11661190
impl ::degenbot_pools::tick_fetch::TickWordFetcher for CountingFetcher {
@@ -1180,6 +1204,7 @@ mod tests {
11801204
})
11811205
}
11821206
}
1207+
let mut core = BotState::new();
11831208
let pool_id = core
11841209
.register_v3_pool(&RegisterV3PoolParams {
11851210
address: alloy::primitives::Address::ZERO,

rust/crates/degenbot-bot/src/instruments.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub struct PipelineInstruments {
150150
/// the registry, and a container OOM-kill presents as an overnight
151151
/// availability failure, not a solver symptom).
152152
///
153-
/// PROMETHEUS NAME COUPLING (pair-review flag, 2026-09-04): the OTel
153+
/// PROMETHEUS NAME COUPLING (pair-review flag, 2026-09-04): the `OTel`
154154
/// name-mapping renders this gauge (dots->underscores + `By` unit
155155
/// suffix) as `degenbot_process_rss_bytes`, and the Grafana panel
156156
/// "Process RSS - registry drift watch" queries that string verbatim —
@@ -688,7 +688,7 @@ impl PipelineInstruments {
688688

689689
/// Read the process resident set size in bytes from `/proc/self/statm`
690690
/// (second field: resident pages; page size classically 4096 — the runs
691-
/// this gauges are Linux x86_64). `None` when statm is unavailable or
691+
/// this gauges are Linux `x86_64`). `None` when statm is unavailable or
692692
/// unexpected (Windows/CI sandboxes) — callers must no-op on None.
693693
#[must_use]
694694
pub(crate) fn read_process_rss_bytes() -> Option<u64> {
@@ -709,6 +709,7 @@ mod tests {
709709
use super::parse_statm_resident;
710710

711711
#[test]
712+
#[expect(clippy::expect_used)]
712713
fn statm_parser_reads_resident_pages() {
713714
// /proc/self/statm: size resident shared text lib data dt (pages)
714715
let rss = parse_statm_resident("54321 21000 1234 100 0 7000 0", 4096)

0 commit comments

Comments
 (0)