Curve¶
| Field | Value |
|---|---|
| Module | almanak.connectors.curve |
| Protocol kind | LP |
| Aliases | N/A |
Supported Chains And Intents¶
| Chain | Family | Supported Intents |
|---|---|---|
| Arbitrum | EVM | LP_CLOSE, LP_OPEN, SWAP |
| Base | EVM | LP_CLOSE, LP_OPEN, SWAP |
| Ethereum | EVM | LP_CLOSE, LP_OPEN, SWAP |
| Optimism | EVM | LP_CLOSE, LP_OPEN, SWAP |
| Polygon | EVM | LP_CLOSE, LP_OPEN, SWAP |
curve
¶
Curve Finance Connector.
This module provides the Curve Finance adapter for executing swaps and managing liquidity positions on Curve pools across multiple chains.
Supported chains: - Ethereum - Arbitrum
Supported operations: - SWAP: Token swaps via Curve pools (StableSwap, CryptoSwap, Tricrypto) - LP_OPEN: Add liquidity to Curve pools - LP_CLOSE: Remove liquidity from Curve pools
Example
from almanak.connectors.curve import CurveAdapter, CurveConfig
config = CurveConfig(
chain="ethereum",
wallet_address="0x...",
)
adapter = CurveAdapter(config)
# Execute a swap
result = adapter.swap(
pool_address="0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7", # 3pool
token_in="USDC",
token_out="DAI",
amount_in=Decimal("1000"),
)
# Add liquidity
lp_result = adapter.add_liquidity(
pool_address="0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
amounts=[Decimal("1000"), Decimal("1000"), Decimal("1000")], # DAI, USDC, USDT
)
CurveAdapter
¶
Adapter for Curve Finance DEX protocol.
This adapter provides methods for: - Executing token swaps via Curve pools - Adding liquidity to pools (LP_OPEN) - Removing liquidity from pools (LP_CLOSE) - Handling ERC-20 approvals - Managing slippage protection
Example
Initialize the adapter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
CurveConfig
|
Curve adapter configuration |
required |
token_resolver
|
TokenResolver | None
|
Optional TokenResolver instance. If None, uses singleton. |
None
|
get_pool_info
¶
Get information about a pool.
The deployment binding is the cold-start value; when a gateway
or RPC is wired the registry is reconciled against live chain state
(coins / coin_addresses / decimals / virtual_price / is_ng) before being
returned — see _refresh_pool_info_from_chain (VIB-5423 / VIB-5424).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
refresh
|
bool
|
When |
True
|
Returns:
| Type | Description |
|---|---|
PoolInfo | None
|
PoolInfo if known, None otherwise |
get_pool_by_name
¶
Get pool info by name.
Same refresh-on-read reconciliation as get_pool_info (VIB-5423);
refresh=False opts the read-only quote path out of the reconcile.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Deployment-scoped pool name, when one was explicitly supplied. |
required |
refresh
|
bool
|
See |
True
|
Returns:
| Type | Description |
|---|---|
PoolInfo | None
|
PoolInfo if found, None otherwise |
swap
¶
swap(
pool_address: str,
token_in: str,
token_out: str,
amount_in: Decimal,
slippage_bps: int | None = None,
recipient: str | None = None,
price_ratio: Decimal | None = None,
oracle_guard_bps: int | None = None,
strict_oracle_guard: bool = False,
oracle_prices_real: bool = True,
) -> SwapResult
Build a swap transaction on a Curve pool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
token_in
|
str
|
Input token symbol or address |
required |
token_out
|
str
|
Output token symbol or address |
required |
amount_in
|
Decimal
|
Amount of input token (in token units, not wei) |
required |
slippage_bps
|
int | None
|
Slippage tolerance in basis points (default from config) |
None
|
recipient
|
str | None
|
Address to receive output tokens (default: wallet_address) |
None
|
price_ratio
|
Decimal | None
|
Price of input token / price of output token (e.g., if swapping USDT at $1 for WETH at $2500, price_ratio = 1/2500 = 0.0004). Required for CryptoSwap/Tricrypto pools; StableSwap pools ignore it. When None and pool is CryptoSwap, the swap fails (fail-closed) rather than executing with inaccurate slippage protection. Also the independent oracle reference for the P0-8 min-out guard below. |
None
|
oracle_guard_bps
|
int | None
|
max bps the pool quote may sit below oracle-fair
before the swap is blocked as pre-moved (VIB-5439). |
None
|
strict_oracle_guard
|
bool
|
when no oracle |
False
|
oracle_prices_real
|
bool
|
whether |
True
|
Returns:
| Type | Description |
|---|---|
SwapResult
|
SwapResult with transaction data |
add_liquidity
¶
add_liquidity(
pool_address: str,
amounts: list[Decimal],
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build an add_liquidity transaction (LP_OPEN).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
amounts
|
list[Decimal]
|
List of token amounts to deposit (in token units) |
required |
slippage_bps
|
int | None
|
Slippage tolerance for min LP tokens (default from config).
For CryptoSwap/Tricrypto (volatile) pools the min_lp floor is the
build-time on-chain quote × (1 − slippage); if the pool price drifts
between build and execution by more than |
None
|
recipient
|
str | None
|
Address to receive LP tokens (default: wallet_address) |
None
|
Returns:
| Type | Description |
|---|---|
LiquidityResult
|
LiquidityResult with transaction data |
remove_liquidity
¶
remove_liquidity(
pool_address: str,
lp_amount: Decimal,
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build a remove_liquidity transaction (LP_CLOSE, proportional).
A proportional withdrawal mirrors the pool's current reserve
composition: burning LP pays out each coin pro rata to the on-chain
reserves. A skewed pool therefore returns skewed per-coin amounts even
when the position was funded evenly. That output shape is expected.
Callers that need a specific exit shape should use
remove_liquidity_one_coin or remove_liquidity_imbalance.
Per-coin min_amounts floors are derived from the on-chain
proportional estimate after applying slippage_bps. The build fails
closed when no non-zero estimate is available.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
lp_amount
|
Decimal
|
Amount of LP tokens to burn |
required |
slippage_bps
|
int | None
|
Slippage tolerance for min output (default from config) |
None
|
recipient
|
str | None
|
Address to receive tokens (default: wallet_address) |
None
|
Returns:
| Type | Description |
|---|---|
LiquidityResult
|
LiquidityResult with transaction data. |
LiquidityResult
|
per-coin minimum-received floors in native token base units. |
remove_liquidity_one_coin
¶
remove_liquidity_one_coin(
pool_address: str,
lp_amount: Decimal,
coin_index: int,
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build a remove_liquidity_one_coin transaction (LP_CLOSE, single-sided).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
lp_amount
|
Decimal
|
Amount of LP tokens to burn |
required |
coin_index
|
int
|
Index of the coin to receive |
required |
slippage_bps
|
int | None
|
Slippage tolerance (default from config) |
None
|
recipient
|
str | None
|
Address to receive tokens (default: wallet_address) |
None
|
Returns:
| Type | Description |
|---|---|
LiquidityResult
|
LiquidityResult with transaction data |
remove_liquidity_imbalance
¶
remove_liquidity_imbalance(
pool_address: str,
amounts: list[Decimal],
lp_amount: Decimal,
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build a remove_liquidity_imbalance transaction (LP_CLOSE, imbalanced).
remove_liquidity_imbalance(uint256[N] amounts, uint256 max_burn_amount)
works the OPPOSITE way to single-sided removal: the caller names the EXACT
per-coin amounts to receive, and the pool burns however much LP is needed,
capped at max_burn_amount. So the safety floor here is a MAX-BURN
CEILING (the most LP we will spend), NOT a min-out (VIB-5438, audit P0-4).
The ceiling is derived from the pool's on-chain calc_token_amount(amounts,
is_deposit=False) LP-burn quote, padded UP by slippage_bps (the
inverse of the single-sided min-out, which pads DOWN). The slippage buffer
absorbs the imbalance fee the legacy calc_token_amount excludes; if it
is too tight the pool reverts on-chain ("Slippage screwed you") — safe.
Fail-closed: this NEVER emits max_burn_amount = MAX_UINT256 or any
unbounded cap (that would let the pool burn the entire LP balance for a
tiny withdrawal — a theft/sandwich vector). If the on-chain quote is
unavailable/reverts, or the requested withdrawal would need more LP than
the position holds, the compile fails loudly (mirrors the #3092 min-out and
3073 min_lp logic).¶
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pool_address
|
str
|
Pool contract address |
required |
amounts
|
list[Decimal]
|
EXACT per-coin amounts to withdraw, in human units, positional by pool-coin index (length MUST equal the pool's coin count). |
required |
lp_amount
|
Decimal
|
LP tokens HELD by the position (human units), the upper
bound on what may be burned. The derived |
required |
slippage_bps
|
int | None
|
Max-burn buffer over the on-chain quote (default config). |
None
|
recipient
|
str | None
|
Address to receive tokens (default: wallet_address). |
None
|
Returns:
| Type | Description |
|---|---|
LiquidityResult
|
LiquidityResult with transaction data (operation |
LiquidityResult
|
|
swap_underlying
¶
swap_underlying(
pool_address: str,
token_in: str,
token_out: str,
amount_in: Decimal,
slippage_bps: int | None = None,
recipient: str | None = None,
price_ratio: Decimal | None = None,
oracle_guard_bps: int | None = None,
strict_oracle_guard: bool = False,
oracle_prices_real: bool = True,
) -> SwapResult
Build a metapool underlying swap via exchange_underlying.
Routes a swap across the COMBINED coin space of a metapool (index 0 =
meta coin, 1..N = base-pool coins) — e.g. FRAX -> USDC through a
FRAX/3CRV metapool. exchange_underlying lives on the metapool
contract itself (NOT the zap); the metapool transparently routes the
leg through its base pool.
Stablecoin-only assumption: every coin on a 3CRV/FRAX-style metapool's
combined space is a USD stable, so the 1:1 decimal-adjusted estimate
(the same the StableSwap path uses) is the correct slippage floor, and
the on-chain get_dy_underlying quote is preferred when a gateway /
rpc is wired.
add_liquidity_underlying
¶
add_liquidity_underlying(
pool_address: str,
underlying_amounts: list[Decimal],
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build a metapool deposit over the COMBINED coin space via the zap.
underlying_amounts is indexed in COMBINED order: index 0 = meta coin,
indices 1..N = base-pool coins (DAI/USDC/USDT). The generic 3CRV
DepositZap's ABI takes the metapool as the first argument:
add_liquidity(address _pool, uint256[N+1] _deposit, uint256 _min_mint).
It deposits the base coins into the base pool (minting the base-LP), then
the base-LP plus the meta coin into the metapool — a user only has to
hold/approve the underlying coins.
remove_liquidity_underlying
¶
remove_liquidity_underlying(
pool_address: str,
lp_amount: Decimal,
slippage_bps: int | None = None,
recipient: str | None = None,
) -> LiquidityResult
Build a metapool proportional withdrawal to underlying coins via the zap.
Burns lp_amount metapool LP and returns the COMBINED underlying coins
(meta coin + base-pool coins) using the generic zap's
remove_liquidity(address _pool, uint256 _amount, uint256[N+1] _min_amounts).
The min-amounts vector is derived from the metapool's native proportional
split (meta coin + base-LP), then the base-LP leg is decomposed across the
base pool's coins by its on-chain reserves. When the on-chain reads are
unavailable, fails closed (no slippage floor) — mirrors the native
remove_liquidity guard.
quote_swap_output
¶
quote_swap_output(
*,
pool_address: str,
token_in: str,
token_out: str,
amount_in_wei: int,
) -> int
Quote a Curve exact-input swap with the pool's on-chain quote method.
set_allowance
¶
Set cached allowance (for testing).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
Token address |
required |
spender
|
str
|
Spender address |
required |
amount
|
int
|
Allowance amount |
required |
clear_planned_allowance_cache
¶
Clear optimistic approvals emitted into the current bundle.
CurveConfig
dataclass
¶
CurveConfig(
chain: str,
wallet_address: str,
default_slippage_bps: int = 50,
deadline_seconds: int = 300,
rpc_url: str | None = None,
gateway_client: GatewayClient | None = None,
permission_discovery: bool = False,
force_is_ng: bool | None = None,
permission_pool_overrides: dict[
str, dict[str, Any]
] = dict(),
)
Configuration for CurveAdapter.
Attributes:
| Name | Type | Description |
|---|---|---|
chain |
str
|
Target blockchain (ethereum, arbitrum) |
wallet_address |
str
|
Address executing transactions |
default_slippage_bps |
int
|
Default slippage tolerance in basis points (default 50 = 0.5%) |
deadline_seconds |
int
|
Transaction deadline in seconds (default 300 = 5 minutes) |
rpc_url |
str | None
|
Optional JSON-RPC URL for on-chain state queries (e.g., pool balances for accurate remove_liquidity slippage estimates). When provided, the adapter queries pool.balances(i) and lp_token.totalSupply() to compute proportional min_amounts rather than returning zeros. When absent or on RPC failure, min_amounts fall back to [0, 0, ..., 0] with a warning. |
LiquidityResult
dataclass
¶
LiquidityResult(
success: bool,
transactions: list[TransactionData] = list(),
pool_address: str = "",
operation: str = "",
amounts: list[int] = list(),
lp_amount: int = 0,
error: str | None = None,
gas_estimate: int = 0,
)
Result of a liquidity operation.
Attributes:
| Name | Type | Description |
|---|---|---|
success |
bool
|
Whether the operation was built successfully |
transactions |
list[TransactionData]
|
List of transactions to execute |
pool_address |
str
|
Pool address |
operation |
str
|
Operation type (add_liquidity, remove_liquidity, remove_liquidity_one_coin) |
amounts |
list[int]
|
Token amounts for the operation |
lp_amount |
int
|
LP token amount (minted or burned) |
error |
str | None
|
Error message if failed |
gas_estimate |
int
|
Total gas estimate |
PoolInfo
dataclass
¶
PoolInfo(
address: str,
lp_token: str,
coins: list[str],
coin_addresses: list[str],
pool_type: PoolType,
n_coins: int,
name: str = "",
virtual_price: Decimal = (lambda: Decimal("1.0"))(),
use_underlying: bool = False,
is_ng: bool = False,
coin_decimals: list[int] | None = None,
is_metapool: bool = False,
base_pool: str | None = None,
base_pool_coins: list[str] | None = None,
base_pool_coin_addresses: list[str] | None = None,
zap_address: str | None = None,
)
Information about a Curve pool.
Attributes:
| Name | Type | Description |
|---|---|---|
address |
str
|
Pool contract address |
lp_token |
str
|
LP token address |
coins |
list[str]
|
List of coin symbols |
coin_addresses |
list[str]
|
List of coin addresses |
pool_type |
PoolType
|
Type of pool (stableswap, cryptoswap, tricrypto) |
n_coins |
int
|
Number of coins in pool |
name |
str
|
Pool name |
virtual_price |
Decimal
|
Pool virtual price (LP token value relative to underlying). Mature pools accumulate fees so virtual_price > 1.0. Used to adjust LP token estimates to prevent over-estimation that causes add_liquidity reverts. |
underlying_coin_index
¶
Return the COMBINED-space index of coin for a metapool, or None.
Combined index 0 is always the meta coin (coins[0]); indices 1..N map
to base_pool_coins / base_pool_coin_addresses in order. coin
may be a symbol or an address. Returns None when this is not a
metapool or coin is neither the meta coin nor a base-pool coin — the
caller then falls back to the native 2-coin path.
get_coin_index
¶
Get the index of a coin in the pool.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coin
|
str
|
Coin symbol or address |
required |
Returns:
| Type | Description |
|---|---|
int
|
Index of the coin |
Raises:
| Type | Description |
|---|---|
ValueError
|
If coin not found in pool |
PoolType
¶
Bases: Enum
Curve pool type.
SwapResult
dataclass
¶
SwapResult(
success: bool,
transactions: list[TransactionData] = list(),
pool_address: str = "",
amount_in: int = 0,
amount_out_minimum: int = 0,
amount_out_estimate: int = 0,
token_out_decimals: int = 18,
token_in: str = "",
token_out: str = "",
error: str | None = None,
gas_estimate: int = 0,
)
Result of a swap operation.
Attributes:
| Name | Type | Description |
|---|---|---|
success |
bool
|
Whether the swap was built successfully |
transactions |
list[TransactionData]
|
List of transactions to execute |
pool_address |
str
|
Pool used for swap |
amount_in |
int
|
Input amount in wei |
amount_out_minimum |
int
|
Minimum output amount (with slippage) |
token_in |
str
|
Input token address |
token_out |
str
|
Output token address |
error |
str | None
|
Error message if failed |
gas_estimate |
int
|
Total gas estimate |
TransactionData
dataclass
¶
TransactionData(
to: str,
value: int,
data: str,
gas_estimate: int,
description: str,
tx_type: str = "swap",
)
Transaction data for execution.
Attributes:
| Name | Type | Description |
|---|---|---|
to |
str
|
Target contract address |
value |
int
|
Native token value to send |
data |
str
|
Encoded calldata |
gas_estimate |
int
|
Estimated gas |
description |
str
|
Human-readable description |
tx_type |
str
|
Type of transaction (approve, swap, add_liquidity, remove_liquidity) |
CurvePoolPermissionBinding
dataclass
¶
CurvePoolPermissionBinding(
chain: str,
pool_address: str,
coin_symbols: tuple[str, ...],
coin_addresses: tuple[str, ...],
coin_decimals: tuple[int, ...],
lp_token: str,
n_coins: int,
pool_type: PoolTypeName,
abi_families: tuple[str, ...],
is_metapool: bool,
base_pool: str | None = None,
base_pool_coin_addresses: tuple[str, ...] | None = None,
)
Immutable identity of one deployment-selected Curve pool.
from_metadata
classmethod
¶
Freeze a fully resolved CurvePoolMetadata value.
from_pool_data
classmethod
¶
Freeze the compiler's canonical resolved pool shape as a candidate.
marker_params
¶
Return connector-parameter fields for a bound synthetic intent.
pool_data
¶
Return the registry-shaped subset needed to construct vectors.
assert_matches_pool_data
¶
Fail if runtime resolution differs from the admitted identity.
AddLiquidityEventData
dataclass
¶
AddLiquidityEventData(
provider: str,
token_amounts: list[int],
fees: list[int],
invariant: int,
token_supply: int,
pool_address: str,
)
Parsed data from AddLiquidity event.
CurveEvent
dataclass
¶
CurveEvent(
event_type: CurveEventType,
event_name: str,
log_index: int,
transaction_hash: str,
block_number: int,
contract_address: str,
data: dict[str, Any],
raw_topics: list[str] = list(),
raw_data: str = "",
timestamp: datetime = (lambda: datetime.now(UTC))(),
)
Parsed Curve event.
CurveEventType
¶
Bases: Enum
Curve event types.
CurveReceiptParser
¶
CurveReceiptParser(
chain: str = "ethereum",
*,
pool_meta_lookup: PoolMetaLookup | None = None,
**kwargs: Any,
)
Bases: FailClosedExtractMixin
Parser for Curve Finance transaction receipts.
Initialize with an optional live pool-metadata lookup.
extract_swap_amounts
¶
extract_swap_amounts(
receipt: dict[str, Any],
*,
expected_out: Decimal | None = None,
) -> SwapAmounts | None
Extract swap amounts using Transfer addresses and resolved decimals.
expected_out is a human-unit pre-slippage quote used to calculate
realized basis points. Unknown decimals fail closed instead of assuming 18.
extract_position_id
¶
Return the minted fungible LP token address as the position identifier.
extract_liquidity
¶
Return minted LP tokens in human units, not raw wei.
extract_lp_tokens_received
¶
Return a zero-address mint Transfer in human LP-token units.
extract_lp_open_data
¶
Extract an AddLiquidity event for a fungible, tickless Curve position.
The emitter is the canonical pool address and position_id=0 denotes no
per-position discriminator. Absent amount slots remain unmeasured None;
emitted zero amounts remain measured zero.
extract_primitive_money_legs
¶
Declare one input leg per funded pool-ordered AddLiquidity amount.
Unknown or incomplete coin metadata, no funded coins, and extraction
failures return None for the legacy path rather than guessing identity.
extract_lp_close_data
¶
Extract pool-ordered proceeds from any supported liquidity removal.
extract_protocol_fees
¶
Report Curve receipt-level protocol fees as unavailable.
Curve NG pools encode fees arrays in AddLiquidity/RemoveLiquidity
events, but those are token-unit LP fees. The admin fee is not emitted,
and this layer has no price oracle for USD conversion.
extract_swap_amounts_result
¶
extract_swap_amounts_result(
receipt: dict[str, Any],
*,
expected_out: Decimal | None = None,
) -> ExtractResult[SwapAmounts]
Extract swap amounts, treating failure on a present swap as an error.
expected_out is forwarded for realized slippage calculation.
extract_position_id_result
¶
Extract position ID, treating failure on a present mint as an error.
extract_liquidity_result
¶
Extract liquidity, treating failure on a present mint as an error.
extract_lp_tokens_received_result
¶
Extract received LP tokens, failing closed when a mint is present.
extract_lp_open_data_result
¶
Extract LP-open data, failing closed when AddLiquidity is present.
extract_primitive_money_legs_result
¶
extract_primitive_money_legs_result(
receipt: dict[str, Any],
) -> ExtractResult[PrimitiveMoneyLegs]
Extract declared legs while preserving the intentional legacy fallback.
Unlike other fields, None with AddLiquidity present is benign when coin
metadata cannot safely bind amounts; LP-open extraction guards decode failure.
extract_lp_close_data_result
¶
Extract LP-close data, failing closed when a removal is present.
extract_protocol_fees_result
¶
Return unavailable fee metadata unless extraction itself fails.
is_curve_event
¶
Return whether a bytes or hex-string topic is a known Curve event.
get_event_type
¶
Return the Curve event type for a bytes or hex-string topic.
ParseResult
dataclass
¶
ParseResult(
success: bool,
events: list[CurveEvent] = list(),
swap_events: list[SwapEventData] = list(),
error: str | None = None,
transaction_hash: str = "",
block_number: int = 0,
transaction_success: bool = True,
)
Result of parsing a receipt.
RemoveLiquidityEventData
dataclass
¶
RemoveLiquidityEventData(
provider: str,
token_amounts: list[int],
fees: list[int],
token_supply: int,
pool_address: str,
)
Parsed data from RemoveLiquidity event.
SwapEventData
dataclass
¶
SwapEventData(
buyer: str,
sold_id: int,
tokens_sold: int,
bought_id: int,
tokens_bought: int,
pool_address: str,
)
Parsed data from TokenExchange event.