degenbot.balancer.stable_pools¶

Balancer V2 stable pool implementations (MetaStable, ComposableStable).

Module Contents¶

degenbot.balancer.stable_pools.INVARIANT_V1 = 1¶
degenbot.balancer.stable_pools.INVARIANT_V2 = 2¶
class degenbot.balancer.stable_pools.BalancerRateProvider¶

Bases: Protocol

Fetches per-block scaling factor rates from on-chain rate providers.

ComposableStablePools override _beforeSwapJoinExit() to refresh rate caches before reading _scalingFactors(). To match this on-chain behavior, off-chain calculations must fetch fresh rates at the block the calculation targets.

Implementations should call getRate() on each token’s rate provider contract, using eth_call with the provided block identifier so the result matches on-chain state at that block.

get_rates(block_identifier: int | str | None = None) → tuple[int, ...]¶

Return the current rate for each pool token.

The returned tuple has one entry per pool token (including BPT if present). Tokens without a rate provider should return ONE (1e18).

class degenbot.balancer.stable_pools.BalancerV2StablePool(*args: Any, **kwargs: Any)¶

Bases: degenbot.types.abstract.AbstractLiquidityPool

Balancer V2 Stable Pool (MetaStablePool or ComposableStablePool).

Supports token-to-token swaps using StableMath. For ComposableStablePools, the BPT token is automatically dropped from the invariant and swap calculations.

Swap fee application order:

GIVEN_IN: subtractFee → upscale → compute(outGivenIn) → downscaleDown GIVEN_OUT: upscale → compute(inGivenOut) → downscaleUp → addFee

Invariant versions:
V1 (INVARIANT_V1): always-roundDown, D_P accumulation. Used by older

deployed ComposableStablePools (e.g. TUSD BSP, bb-s-USD). Matches the monorepo _calculate_invariant.

V2 (INVARIANT_V2): roundUp parameter, P_D accumulation. Used by

MetaStablePools and newer deployed pools. The swap path calls with round_up=True. Matches _calculate_invariant_deployed.

Rate handling:

ComposableStablePools have time-varying rates (yield accrual in bb-a-* tokens). The deployed contract refreshes rate caches before each swap via _beforeSwapJoinExit(). For exact-integer matching, inject a BalancerRateProvider that replicates this cache-aware logic (read getTokenRateCache, check expiry, call getRate() if expired). Without a live rate provider, the pool uses construction-time scaling factors and raises StaleRateResult for ComposableStablePools to warn that rates may be stale.

MetaStablePools have no rate cache — they call getRate() directly.

variant: ClassVar[str | None] = 'balancer_stable'¶
type PoolState = BalancerV2PoolState¶
FEE_DENOMINATOR = 1000000000000000000¶
address: degenbot.types.chain.ChecksummedAddress¶
pool_id: bytes¶
pool_specialization: int¶
vault: degenbot.types.chain.ChecksummedAddress¶
scaling_factors: tuple[int, ...]¶
fee: fractions.Fraction¶
amp: int¶
bpt_idx: int | None¶
invariant_version: int¶
classmethod from_handle(py_pool: degenbot.types.Pool) → Self¶

Wrap a Rust-owned Pool handle as a Python companion.

Internal seam (ADR-005, Polars-style _from_pydf pattern). Every identity field (vault, pool_id, tokens, amp, scaling_factors, swap_fee, bpt_idx, invariant_version) is read off the handle; the rate provider is the stored I/O trait object (queried via fetch_balancer_stable_rates / balancer_stable_rate_provider_is_static); base scaling factors are derived from token decimals.

Returns:

A cls instance wrapping py_pool.

Raises:

DegenbotValueError – If the handle is not a Balancer stable pool or any token is not registered.

property balances: tuple[int, ...]¶

Balances.

Read from the Rust core via the Pool handle (ADR-005 slice 12d). Rust BotState is the single source of truth for the mutable balances slot; this getter returns the live tuple (one U256 per token, including BPT for Composable pools).

property state: PoolState¶

State.

Built from one atomic Rust snapshot (snapshot_balancer_stable() — (balances, block)) so callers see a coherent tuple (no torn read mid-external_update). Mirrors V3/V4’s snapshot_v3() / Curve’s snapshot_curve() / Weighted’s snapshot_balancer_weighted() contract.

Raises:

DegenbotValueError – If the Rust snapshot is absent (the pool is not registered in Rust as a Balancer stable pool — unreachable for a companion built over a registered handle).

property tokens: tuple[degenbot.erc20.Erc20Token, ...]¶

Tokens.

property rate_provider: BalancerRateProvider | None¶

The rate provider for per-block rate resolution, if available.

With the sealed seam (ADR-005) the provider is the stored Rust I/O trait object, queried via the handle. This property returns None when the stored provider is static (the no-I/O fallback).

property requires_io_at_calculation_time: bool¶

Whether this pool may call its rate_provider during swap calculations.

Returns True for ComposableStablePools with a live rate provider (time-varying rates from yield-bearing tokens). Returns False for MetaStablePools and for pools with only a static rate provider.

calculate_tokens_out_from_tokens_in(token_in: degenbot.erc20.Erc20Token, token_out: degenbot.erc20.Erc20Token, token_in_quantity: int, override_state: PoolState | None = None, block_identifier: int | str | None = None) → int¶

Compute the amount of token_out received for a GIVEN_IN swap.

Thin driver shell over the Rust core: token/scale resolution stays Python-side (the MetaStable block-rate lookup is driver data), the swap math is fully Rust-owned.

For ComposableStablePools without a live rate provider the result is wrapped in StaleRateResult because construction-time rates may be stale.

Returns:

The computed integer value (unwrapped from StaleRateResult semantics when rates cannot be stale).

Raises:

StaleRateResult – See function documentation.

calculate_tokens_in_from_tokens_out(token_in: degenbot.erc20.Erc20Token, token_out: degenbot.erc20.Erc20Token, token_out_quantity: int, override_state: PoolState | None = None, block_identifier: int | str | None = None) → int¶

Compute how many tokens must be sent to take token_out_quantity out.

Thin driver shell over the Rust core: the MetaStable block-rate resolution stays Python-side, the swap math is fully Rust-owned.

Returns:

The computed integer value.

Raises:

StaleRateResult – See function documentation.

external_update(update: degenbot.balancer.types.BalancerV2StablePoolExternalUpdate) → None¶

Apply an external state update with new balances.

Delegates to the Rust core (Pool.apply_balancer_stable_balance_update) which journals the prior balances (genesis-anchor V2-style discipline) and lands the new balances + update_block atomically (ADR-005 slice 12d). The _state_lock + double-check-after-acquire pattern is gone — Rust’s internal write lock handles atomicity; the registration-state precondition is enforced by the Rust core’s silent-no-op-on-older-block contract.

Raises:

DegenbotValueError – If the Rust core rejects the update (the pool is not registered as a Balancer stable pool — unreachable for a companion built over a registered handle).