degenbot Documentation

repository·main·Indexed 19 days ago

https://github.com/bowtieddevil/degenbot

A high-performance MEV-bot framework featuring a Rust core for math and state management and a Python driver for orchestration. It provides building blocks for arbitrage and liquidation bots on EVM-compatible blockchains, supporting protocols including Uniswap (V2, V3, V4), Curve V1, Solidly V2, Balancer V2, and Aave V3. The framework includes a Vyper-based cmd_executor contract for executing compact command streams of swaps, transfers, and settlements.

Tokens
261.2K
Snippets
609
Records
1.1K
Agent score
69%

What's inside degenbot

  1. Overview of Degenbot Architecture

    main

    Degenbot is a high-performance MEV-bot framework consisting of a Rust core and a Python driver shell.

    • Rust Core: Contains all performance-critical logic, including pool/token state, swap math, event decoding, solvers, and the pump loop. It is designed to be standalone; a pure-Rust developer can use it by running cargo add degenbot.
    • Python Driver: Provides a user-facing API, orchestration, and configuration management. It uses a thin PyO3 layer to translate Python calls into Rust calls without containing business logic itself.

    This split ensures that all heavy lifting (math and state machines) is handled by Rust, while Python provides an ergonomic interface for bot orchestration.

  2. Understand the Snapshot Consistency Model (Post-SnapshotStore Removal)

    main

    With the removal of SnapshotStore, the bot ensures data consistency during startup by using a single SQLite WAL (Write-Ahead Logging) read transaction.

    The Consistency Problem

    Previously, SnapshotStore froze the database state at boot. Without it, if the pool_updater process writes new data to the SQLite DB while the bot is still performing per-pool registration (build_paths), a race condition occurs:

    1. The global snapshot_seed_block (S) is set at boot.
    2. A pool registered after the updater has written new rows will read data at block S+N.
    3. The verification logic (verify_*_snapshot_seed) will compare this S+N data against the on-chain state at block S, causing a false verification failure.

    The Solution: Held Read Transaction

    To prevent this, the bot uses Approach 1: holding one BEGIN DEFERRED read transaction across the entire build_paths process.

    Because SQLite in WAL mode provides MVCC (Multi-Version Concurrency Control), a single read transaction sees the database exactly as it was when the first read in that transaction started. Even if the pool_updater commits new data, the bot's view remains frozen at the boot-time snapshot. This ensures that the global S and all per-pool liquidity maps are perfectly synchronized for verification.

  3. GHO rounding differences in Aave V4+

    main

    When working with GHO (Aave's stablecoin), be aware that GHO vToken V4+ uses different rounding logic compared to standard vTokens V4+ due to the deprecation of the discount mechanism in revision 4.

    OperationStandard vToken V4+GHO vToken V4+
    BORROW (mint)ray_div_ceil (ceiling)ray_div_floor (floor)
    REPAY (burn)ray_div_floor (floor)ray_div_floor (floor)

    Note for developers: When processing GHO BORROW events, the scaled amount must be pre-calculated from the original borrow amount using calculate_mint_scaled_amount() before being applied to the Mint event. This logic is handled in the Rust degenbot-aave-updater core.

  4. Implement the V3 `setupPool()` pattern for Tier-3 Harnesses

    main

    When building a Tier-3 harness for UniswapV3, you must avoid deploying the large UniswapV3Pool (~22 KB) directly within a constructor's new call. Doing so in revm will cause an Out-of-Gas (OOG) error due to the EIP-150 63/64 gas-forwarding rule, which starves the G_CODEDEPOSIT charge.

    The correct pattern:

    1. Constructor: The constructor should only deploy mock tokens and set the parameters().
    2. setupPool() External Call: Use an external setupPool() function to perform the actual CREATE of the pool. This ensures the CALL forwards the full transaction gas (e.g., ~16.7 M gas), providing sufficient headroom for the G_CODEDEPOSIT charge (approx. 4.4 M gas).
  5. How the I/O-free architecture works

    main

    Degenbot uses an I/O-free architecture to separate network operations from mathematical calculations.

    • Pools as Pure Calculation Objects: Once constructed, pool objects (like UniswapV2Pool or CurvePool) have no network dependencies. They do not import ProviderAdapter or perform async calls. This makes them highly testable, performant, and easy to snapshot or pickle.
    • Builders for I/O: All network I/O (DB lookups, RPC fetches, decoding) is encapsulated within Builder classes (e.g., V2PoolBuilder, V3PoolBuilder). Builders handle the choreography of fetching data and then injecting it into the pool.
    • State Updates: State changes in pools are performed via the external_update() method, which accepts a typed update object (e.g., UniswapV2PoolExternalUpdate). This method contains pure logic to validate the update and transition the pool's state without performing any I/O.
    • Curve Calculators: Specifically for Curve, calculators use a DyCalculationInputs frozen dataclass to receive pre-resolved data, ensuring no private member access or hidden I/O occurs during calculation.
  6. Work with Balancer V2 Weighted Pools

    main

    Balancer V2 weighted pools use a weighted product invariant with configurable token weights and a singleton Vault architecture.

    Key implementation details:

    • PowVersion detection: The library detects the specific FixedPoint library version (V1 vs V2) from the contract bytecode at construction time.
    • Rounding: GIVEN_IN rounds down (seller gets less); GIVEN_OUT rounds up (buyer pays more).
    • Fee ordering: GIVEN_OUT applies downscale-up first, then adds the swap fee, matching Solidity's exact order.
    • Scaling: Non-18 decimal tokens are normalized using scaling factors: ONE * 10**(18 - decimals).
  7. Optimization Strategy: f64-narrowed Brent vs Native U256

    main

    Degenbot uses different mathematical strategies for solving swap paths depending on the liquidity pool family to balance latency and precision. The core decision is whether to use a fast f64 (floating point) 'narrowing' pass to find a candidate optimum before performing a high-precision U256 verification sweep.

    Strategy by Pool Family

    FamilyStrategyRationale
    Balancer weightedBrent over U256 directlyNative U256 calls are fast (~138 ns). The f64 narrow saves <4 µs/solve, which is not worth the added precision-loss complexity.
    Curve stableswapf64-narrowed Brent + U256 verifyNative U256 calls are expensive (~1.32 µs). f64 narrowing reduces solve latency from ~53 µs to ~4.4 µs (12x speedup).
    Balancer stablef64-narrowed Brent + U256 verifySimilar to Curve, the iterative Newton process is expensive (~1.2 µs/call). f64 narrowing provides significant latency savings.

    Precision and Verification

    When using f64 narrowing, precision loss is expected because f64 (52-bit mantissa) cannot exactly represent large U256 values (e.g., $1M 18dp pools).

    • Bracketing: f64 is safe for finding the general vicinity of the optimum.
    • Verification: A U256 integer verify sweep (a ±3 integer scan around the f64-derived optimum) is mandatory to catch the true maximum and ensure exactness.
  8. Calculate Interest Accrual (Liquidity and Borrow Indices)

    main

    Aave updates liquidity and borrow indices to reflect interest accrual over time.

    Liquidity Index Update (Linear Interest)

    Used for liquidity rates. It uses a linear interest calculation.

    • Formula: cumulatedInterest = 1 + (rate * timeDelta / SECONDS_PER_YEAR)
    • Update: newIndex = oldIndex.rayMul(cumulatedInterest)

    Borrow Index Update (Compounded Interest)

    Used for variable borrow rates. It uses a Taylor series approximation for compounded interest.

    • Formula: newIndex = oldIndex.rayMul(cumulatedInterest)
    • Implementation: Uses MathUtils.calculateCompoundedInterest which accounts for the rate per second and the time elapsed since the last update.
    // Linear interest calculation
    function calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp)
        internal view returns (uint256)
    {
        uint256 timeDifference = block.timestamp - uint256(lastUpdateTimestamp);
        return (rate * timeDifference) / SECONDS_PER_YEAR + WadRayMath.RAY;
    }
  9. Understand the Rust/Python structural alignment for Concentrated Liquidity (CL) state

    main

    Degenbot maintains a consistent structural pattern between its Rust core and Python implementation for managing Concentrated Liquidity (CL) pool states (like Uniswap V3/V4).

    Key architectural principles:

    • Flat State Structs: Both Rust and Python avoid splitting state into nested sub-structs (like Slot0Head or TickBookkeepingMap) within the main pool state. Instead, V3PoolState and V4PoolState (Rust) or UniswapV3PoolState (Python) keep fields like liquidity, sqrt_price_x96, and tick_data as flat, sibling fields.
    • Trait/Protocol-based Narrowing: To solve the problem of functions needing only a subset of the state (e.g., a verifier that only needs tick data), the project uses traits in Rust and protocols in Python.
      • In Rust, the TickMap trait is used to provide a typed, narrow view of the tick-related data to consumers like the CL verifier or liquidity application paths.
      • In Python, the _HasTickData and _HasPoolLiquidityMap protocols serve the same purpose.
    • Mutation Separation: Mutation paths are split at the method level. For example, apply_v3_swap handles slot0 scalars, while apply_v3_liquidity_update handles the tick map. This mirrors the Python pattern where external_update() handles scalars and update_liquidity_map() handles the tick map.
  10. Understand Aave Repay Amount Transformations

    main

    Aave handles debt repayment differently depending on whether the debt is Variable Rate or Stable Rate. Understanding these transformations is critical for calculating exact repayment amounts.

    Variable Rate Repay

    Variable debt uses a scaled balance approach where interest is factored into an index.

    1. Validation: currentDebt must be greater than 0.
    2. Calculation: The payback amount is the minimum of the requested amount or the currentDebt.
    3. Scaling: The scaledAmountToBurn is calculated as payback.rayDiv(index) (using rayDivFloor in v4+).
    4. Update: The _scaledBalance is reduced by the scaledAmountToBurn.

    Stable Rate Repay

    Stable debt tracks a principal amount and uses timestamps to calculate interest.

    1. Calculation: currentDebt is derived from principal + accruedInterest (where interest is principal * compoundedInterest - principal).
    2. Validation: currentDebt must be greater than 0.
    3. Calculation: The payback amount is the minimum of the requested amount or the currentDebt.
    4. Update: The new principal becomes currentDebt - payback, and the lastUpdateTimestamp is updated to block.timestamp.

    Key Takeaways

    • Repayment Methods: You can repay using the underlying tokens OR by using aTokens.
    • Full Repayment: Setting the amount to MAX will clear all debt in that specific mode.
  11. Understand Curve StableSwap Variant Enums

    main

    Mainnet Curve pools use different calculation formulas depending on the pool contract version. These are determined by the pool address at construction and stored as enums. There are three primary variant types:

    DVariant

    Identifies the calc_d / calc_dp formula pair used in _get_d().

    • STANDARD: Standard formula for both.
    • VARIANT_ALPHA: Variant alpha for calc_d, Standard for calc_dp.
    • VARIANT_ALPHA_DP_ALPHA: Variant alpha for both.
    • VARIANT_DP_ALPHA: Standard for calc_d, Variant alpha for calc_dp.
    • VARIANT_BETA_DP: Standard for calc_d, Variant beta for calc_dp.
    • VARIANT_GAMMA_DP: Standard for calc_d, Variant gamma for calc_dp.

    YVariant

    Identifies the Y-calculation formula used in _get_y(), controlling the amp divisor and c/b formula.

    • STANDARD: Uses A_PRECISION for both amp divisor and c/b formula.
    • VARIANT_0: Does not use A_PRECISION for either.
    • VARIANT_1: Uses A_PRECISION for amp divisor, but not for c/b formula.

    YDVariant

    Identifies the Y_D-calculation formula used in _get_y_d(), controlling A_PRECISION in b/c formulas.

    • STANDARD: Without A_PRECISION.
    • VARIANT_0: With A_PRECISION.

    Resolution is handled by resolve_d_variant(), resolve_y_variant(), and resolve_yd_variant() in _variant_groups.py during CurvePoolBuilder.build().

  12. Permission requirements for eliminateReserveDeficit

    main

    The eliminateReserveDeficit function has restricted access. It is only callable by the Umbrella contract, which serves as a protocol-level safety mechanism.

    To maintain protocol solvency, the Umbrella contract performs the following lifecycle:

    1. Accumulates protocol revenue and fees over time.
    2. Uses accumulated funds to cover deficits via the deficit elimination process.
    3. Acts as a backstop for protocol solvency.

    When the Umbrella contract calls eliminateReserveDeficit, a specified amount of the Umbrella's aTokens are burned to reduce the reserve's deficit.