Skip to content

feat(core): gas-aware profit calculator + profit-ordered OpportunityQueue#40

Merged
obchain merged 4 commits intomainfrom
feat/15-profit-calc-and-queue
Apr 24, 2026
Merged

feat(core): gas-aware profit calculator + profit-ordered OpportunityQueue#40
obchain merged 4 commits intomainfrom
feat/15-profit-calc-and-queue

Conversation

@obchain
Copy link
Copy Markdown
Owner

@obchain obchain commented Apr 21, 2026

Two bundled changes that sit between router and executor.

Profit calculator (profit.rs):

  • ProfitInputs / NetProfit structs — integer USD cents throughout, no float drift
  • calculate_profit(inputs, min_profit_usd) formula:
    gross    = repay × liquidation_bonus_bps / 10_000
    slippage = gross × slippage_bps           / 10_000
    net      = gross − flash_fee − gas − slippage
    
  • Overflow-safe via checked_mul/checked_add; bogus bps (> 10_000) rejected
  • Error messages carry itemised cost breakdown for log clarity
  • Five unit tests: healthy path, below-threshold rejection, cost-eats-gross, bogus-bps validation, zero-threshold

OpportunityQueue (queue.rs):

  • BinaryHeap<QueueEntry> keyed on net_profit_usd_cents (max-heap root = most profitable)
  • Private QueueEntry wrapper keeps Ord off public LiquidationOpportunity
  • TTL in blocks (default 2 ≈ 6 s on BSC); stale entries silently dropped on pop, sweepable via prune_stale
  • Five unit tests: ordering, TTL boundary, stale drop, prune count, default TTL

Depends on #14 (feat/14-flashloan-router).

obchain added 2 commits April 21, 2026 18:17
Shared decision point for the scanner → router → executor handoff:
given a candidate liquidation priced entirely in USD cents, decide
whether its net beats the configured `min_profit_usd` threshold.

Formula:
    gross    = repay × liquidation_bonus_bps / 10_000
    slippage = gross × slippage_bps           / 10_000
    net      = gross − flash_fee − gas − slippage

Design notes:
- Integer USD cents throughout. Caller converts once at the boundary
  using the Chainlink price cache; no float drift on the hot path
- `ProfitInputs` / `NetProfit` structs make the interface auditable
- Overflow-safe: `checked_mul` + `checked_add` on every arithmetic
  step; bogus bps values (> 10_000) are rejected up front
- Errors carry the itemised cost breakdown so logs show exactly why a
  candidate was dropped

Five unit tests cover: healthy-path numbers, below-threshold
rejection, cost-eats-gross rejection, bogus bps validation, zero
threshold acceptance.
Final Day-3 piece: a priority queue the executor drains one per block,
always taking the highest-net-profit liquidation that hasn't aged out.

- `BinaryHeap<QueueEntry>` under the hood; ordering keyed on
  `net_profit_usd_cents` so the heap root is the richest opportunity
- Private `QueueEntry` wrapper keeps `Ord` off the public
  `LiquidationOpportunity` — the type stays free of ordering
  semantics in unrelated contexts
- TTL in blocks (default 2, ≈ 6 s on BSC). Stale entries get
  silently dropped on `pop` and can be swept via `prune_stale`
- `push` / `pop` / `prune_stale` / `len` / `is_empty` surface;
  callers track the current block themselves so the queue has no
  concept of time beyond what the caller feeds it
- Five unit tests: ordering, TTL boundary, stale drop, prune count,
  default TTL
This was referenced Apr 22, 2026
Rebuild the profit path and opportunity queue end-to-end around
integer units and typed errors so the executor can reason about
failure modes without anyhow string matching.

profit.rs:
- replace anyhow::Result with thiserror ProfitError enum covering
  InvalidBps, InvalidPrice, Overflow, UnsupportedDecimals,
  Unprofitable, BelowMinThreshold
- drop all f64 from the profit path; min_profit threshold is now
  u64 micro-USD matching BotConfig
- add Price (1e8) and from_opportunity constructor that converts
  wei amounts through U256 into USD cents via a per-token Chainlink
  price; collateral and debt priced independently so the formula
  works for non-stable/non-matching markets
- charge slippage against expected_swap_output_cents, not the gross
  bonus, so the budget reflects the swap the bot actually performs
- ProfitInputs, NetProfit, ProfitError all #[non_exhaustive]
- field-level rustdoc documents every unit + conversion path
- tests: u64::MAX overflow on slippage mul, u64::MAX overflow on
  total-cost add, 10_000 bps boundary valid, 10_001 rejected,
  cost > gross, min_profit == 0, zero price rejected, decimals > 18
  rejected, realistic BSC case (1 BNB @ $600 with 10% bonus)

queue.rs:
- wrap BinaryHeap in Arc<tokio::sync::Mutex<_>>; OpportunityQueue
  is Clone + Send + Sync; push/pop/prune/len/is_empty are async
- QueueEntry Ord uses (net_profit_cents, inserted_at_block)
  lexicographic with fresher-first tie-break; manual PartialEq
  mirrors Ord exactly
- saturating_sub on the staleness check keeps entries alive across
  a reorg rewind instead of wrapping to a massive age
- QueueEntry gains #[non_exhaustive]
- tests: tie-break (fresher wins), reorg 105->104 rewind keeps
  entry, pruned entry stays dropped across rewind, 16 concurrent
  producers / 1 consumer with ordering invariant asserted

types.rs:
- LiquidationOpportunity::with_profit(position, .., net_profit)
  constructor that copies net_profit.net_usd_cents into the
  opportunity, eliminating dual representation drift

config.rs + config/default.toml:
- BotConfig.min_profit_usd (f64) -> min_profit_usd_1e6 (u64)
  stored in micro-USD so TOML stays integer-only; executor
  converts to cents internally (/ 10_000)

Closes #146 #147 #148 #149 #150 #151 #152 #153 #154 #155 #156 #157
@obchain obchain changed the base branch from feat/14-flashloan-router to main April 24, 2026 11:08
…nd-queue

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	config/default.toml
#	crates/charon-core/src/config.rs
#	crates/charon-core/src/lib.rs
#	crates/charon-core/src/types.rs
@obchain obchain merged commit f6874a5 into main Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant