Automated concentrated liquidity management protocol for Ekubo AMM, combining dynamic position management with fee auto-compounding and STRK reward harvesting.
The vault lets users deposit token pairs into optimized liquidity positions on Ekubo. Using similar mechanics as ERC-4626 token issuance, it automatically reinvests earned fees back into the position and handles complex operations like reward harvesting. Governance-controlled rebalancing maintains optimal price bounds while role-based security restricts critical operations. The system tracks positions via NFT ownership and enforces strict precision checks for capital efficiency.
Multi-Pool Architecture: The vault supports managing multiple liquidity pools simultaneously. All pools must use the same token pair (token0/token1) but can have different price bounds. This allows for more flexible liquidity distribution across different price ranges.
To define convention, total assets is the total liquidity held by the vault across all managed pools. Total supply is ERC-20 tokens (shares) minted by the vault. Every time when earned fees are added to liquidity or STRK rewards harvested to add liquidity, it increases the total liquidity (i.e. total assets) to increase the ERC-20 (share) value.
- Deposits token0 and token1 amounts into the vault
- Automatically collects fees from all pools before processing deposit
- Distributes deposits proportionally across all managed pools based on current vault positions
- Mints ERC-20 shares proportional to liquidity created
- For first deposit (total_supply == 0), uses initial ratio values to calculate shares
- Event:
Deposit- emitted with sender, owner (receiver), shares minted, and amounts deposited
- Burns shares and redeems proportional liquidity across all pools
- Collects fees from each pool before withdrawal
- Withdraws tokens from Ekubo positions proportionally
- Transfers assets directly to receiver
- Updates NFT state if a position empties (sets nft_id to 0)
- Returns
MyPositionsstruct with detailed position breakdown - Event:
Withdraw- emitted with sender, receiver, owner, shares burned, and amounts withdrawn
(Relayer-only function)
Rebalances liquidity across all managed pools:
- Withdraws specified liquidity amounts from each pool (if
liquidity_burn > 0) - Performs token swaps to optimize asset distribution (if
swap_params.token_from_amount > 0) - Updates price bounds for each pool (if
new_boundsprovided) - Deposits liquidity with new bounds into each pool (if
liquidity_mint > 0) - Validates sufficient token balances before minting
- Each pool can be independently configured with different bounds and liquidity amounts
- Unused Token Validation: After all operations complete, checks contract token balances against the configured
max_unused_balances_on_rebalancethresholds (set by governor). If balances exceed the configured maximums, the transaction reverts with an error showing expected max and found balance.
- Event:
Rebalance- emitted with array ofRangeInstructionactions performed - Events:
EkuboPositionUpdated- emitted for each pool during withdrawal and deposit operations
(Permissionless, pausable function)
- Collects accumulated fees from the specified Ekubo position
- Calculates and transfers strategy fees to fee collector (based on
fee_bps) - Deposits remaining fees back into the position as liquidity
- May leave some unused token balances in the contract (handled during rebalance)
- Event:
HandleFees- emitted with token addresses, original balances, deposited fee amounts, and pool info - Event:
EkuboPositionUpdated- emitted when fees are deposited as liquidity
(Relayer-only function)
- Claims STRK rewards from Ekubo distributor contract using merkle proof
- Collects strategy fees (based on
fee_bps) and transfers to fee collector - Swaps remaining rewards into token0 and token1 using Avnu multi-route swaps
- Deposits swapped tokens as liquidity (handled separately via deposit/rebalance)
- Event:
HarvestEvent- emitted with reward token, reward amount, and resulting token0/token1 amounts after swaps
(Governor-only function)
- Adds a new managed pool to the vault
- Validates that the pool uses the same token pair as existing pools
- Ensures the pool doesn't already exist (same pool_key and bounds)
- Calculates and stores sqrt values for the new bounds
- Event:
PoolUpdated- emitted with pool_key, bounds, pool_index, andis_add: true
(Governor-only function)
- Removes a managed pool from the vault
- Requires that the pool has zero liquidity
- Swaps the removed pool with the last pool in the array (if not already last)
- Events:
PoolUpdated- emitted for pool removal and any swaps that occur
(Governor-only function)
- Updates fee settings (fee_bps and fee_collector address)
- Validates fee_bps <= 10000 (100%)
- Event:
FeeSettings- emitted with new fee settings
(Governor-only function)
- Sets the maximum allowed unused token balances after rebalance operations
max_unused_balances.token0: Maximum allowed unused token0 balance (set to0to disable check)max_unused_balances.token1: Maximum allowed unused token1 balance (set to0to disable check)- This configuration helps enforce standards - relayer must ensure rebalance operations don't leave excessive unused tokens
- Event:
MaxUnusedBalances- emitted with new configuration
convert_to_shares(amount0, amount1) -> SharesInfo: Converts asset amounts to shares (doesn't execute deposit)convert_to_assets(shares) -> MyPositions: Converts shares to asset amounts across all pools
get_position(pool_index) -> MyPosition: Returns current position details for a specific poolget_positions() -> MyPositions: Returns all positions across all managed poolstotal_liquidity_per_pool(pool_index) -> u256: Returns total liquidity for a specific pool
get_pool_settings(pool_index) -> ClSettings: Returns complete settings for a poolget_managed_pools() -> Array<ManagedPool>: Returns all managed poolsget_managed_pools_len() -> u64: Returns number of managed poolsget_managed_pool(index) -> ManagedPool: Returns specific pool by index
get_amount_delta(pool_index, liquidity) -> (u256, u256): Calculates token amounts for given liquidityget_liquidity_delta(pool_index, amount0, amount1) -> u128: Calculates liquidity for given token amountsget_fee_settings() -> FeeSettings: Returns current fee settingsget_max_unused_balances_on_rebalance() -> MaxUnusedBalances: Returns current max unused balance configuration
Emitted when a user deposits tokens into the vault.
sender: Address that initiated the depositowner: Address that receives the shares (can differ from sender)shares: Number of ERC-20 shares mintedamount0: Amount of token0 depositedamount1: Amount of token1 deposited
Emitted when a user withdraws tokens from the vault.
sender: Address that initiated the withdrawalreceiver: Address that receives the tokensowner: Address that owns the shares (usually same as receiver)shares: Number of ERC-20 shares burnedamount0: Total amount of token0 withdrawnamount1: Total amount of token1 withdrawn
Emitted after a rebalance operation completes.
actions: Array ofRangeInstructioncontaining:liquidity_mint: Amount of liquidity to mintliquidity_burn: Amount of liquidity to burnpool_key: Pool identifiernew_bounds: New price bounds for the pool
Emitted when fees are collected and reinvested for a specific pool.
token0_addr: Address of token0token0_origin_bal: Original token0 balance before fee collectiontoken0_deposited: Amount of token0 fees deposited as liquiditytoken1_addr: Address of token1token1_origin_bal: Original token1 balance before fee collectiontoken1_deposited: Amount of token1 fees deposited as liquiditypool_info: CompleteManagedPoolinformation
Emitted when STRK rewards are harvested and swapped.
rewardToken: Address of the reward token (typically STRK)rewardAmount: Total reward amount claimed (after fees)token0: Address of token0token0Amount: Amount of token0 received after swaptoken1: Address of token1token1Amount: Amount of token1 received after swap
Emitted when fee settings are updated.
fee_bps: Fee basis points (0-10000, where 10000 = 100%)fee_collector: Address that receives strategy fees
Emitted when maximum unused balance thresholds are updated.
token0: Maximum allowed unused token0 balance after rebalance (0 = disabled)token1: Maximum allowed unused token1 balance after rebalance (0 = disabled)
Emitted when a pool is added or removed from the vault.
pool_key: Pool identifier (token0, token1, fee tier)bounds: Price bounds (lower and upper ticks)pool_index: Index of the pool in the managed_pools arrayis_add:trueif pool was added,falseif removed
Emitted whenever an Ekubo position is modified (deposit, withdraw, fee reinvestment).
nft_id: NFT identifier for the Ekubo positionpool_key: Pool identifierbounds: Price bounds for the positionamount0_delta: Change in token0 amount (i129 with sign: false = increase, true = decrease)amount1_delta: Change in token1 amount (i129 with sign)liquidity_delta: Change in liquidity (i129 with sign)
| Role | Privileges | Methods |
|---|---|---|
| Governor | Update settings, manage pools | set_settings()set_max_unused_balances_on_rebalance()add_pool()remove_pool() |
| Emergency Actor | Pause/unpause | pause()unpause() |
| Relayer | Execute rebalances, harvest rewards | rebalance_pool()harvest() |
| Super admin | Upgrade contract | upgrade() |
| Public | Deposit, withdraw, handle fees | deposit()withdraw()handle_fees() |
- ReentrancyGuard protection on all state-changing functions
- Pausable functionality for emergency stops
- Role-based access control for critical operations
- Precision checks to ensure share calculations remain consistent across pools
- Validation of sufficient balances before operations
- NFT-based position tracking with automatic cleanup when positions empty