degenbot.balancer.stable_pools ============================== .. py:module:: degenbot.balancer.stable_pools .. autoapi-nested-parse:: Balancer V2 stable pool implementations (MetaStable, ComposableStable). Module Contents --------------- .. py:data:: INVARIANT_V1 :value: 1 .. py:data:: INVARIANT_V2 :value: 2 .. py:class:: BalancerRateProvider Bases: :py:obj:`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. .. py:method:: 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). .. py:class:: BalancerV2StablePool(*args: Any, **kwargs: Any) Bases: :py:obj:`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. .. py:attribute:: variant :type: ClassVar[str | None] :value: 'balancer_stable' .. py:type:: PoolState :canonical: BalancerV2PoolState .. py:attribute:: FEE_DENOMINATOR :value: 1000000000000000000 .. py:attribute:: address :type: degenbot.types.chain.ChecksummedAddress .. py:attribute:: pool_id :type: bytes .. py:attribute:: pool_specialization :type: int .. py:attribute:: vault :type: degenbot.types.chain.ChecksummedAddress .. py:attribute:: scaling_factors :type: tuple[int, ...] .. py:attribute:: fee :type: fractions.Fraction .. py:attribute:: amp :type: int .. py:attribute:: bpt_idx :type: int | None .. py:attribute:: invariant_version :type: int .. py:method:: from_handle(py_pool: degenbot.types.Pool) -> Self :classmethod: 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. .. py:property:: balances :type: 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). .. py:property:: state :type: 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). .. py:property:: tokens :type: tuple[degenbot.erc20.Erc20Token, ...] Tokens. .. py:property:: rate_provider :type: 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). .. py:property:: requires_io_at_calculation_time :type: 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. .. py:method:: 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. .. py:method:: 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. .. py:method:: 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).