Skip to content

Developer Quickstart

This guide walks developers through integrating with the Alula lending protocol on Stellar/Soroban. It covers reading market state, executing core operations, composing advanced strategies, and building liquidation bots.

Overview

Alula exposes a single Market contract per lending market. Each market contains multiple asset pools and manages all user positions (obligations). You interact with the contract through the Soroban SDK or the Stellar CLI.

Key concepts before you start:

  • ObligationKey — Every user function takes an ObligationKey { user: Address, seed: Option<BytesN<32>> } instead of a plain address. The seed lets one address hold multiple isolated positions. Use seed: None for a standard obligation. See Concepts.
  • Pool addresses — Each asset pool has a contract address. Use get_all_pools() to discover them.
  • Batch operations — The submit_requests_batch function lets you compose multiple operations atomically. This is how leveraged positions are built.

Reading Market State

Start by querying the market to understand its pools and current state.

Get global configuration

rust
// Returns GlobalState with admin, oracle, status, max_positions, etc.
let state: GlobalState = market_client.get_global_state();

List all pools

rust
// Returns Vec<Address> of all registered pool addresses
let pools: Vec<Address> = market_client.get_all_pools();

Get pool details

rust
// Returns Pool struct with balances, rates, config
let pool: Pool = market_client.get_pool(&pool_address);

// Returns PoolData with computed APYs and oracle price (simulation only)
let pool_data: PoolData = market_client.get_pool_data(&pool_address);

Get all market data at once

rust
// Returns MarketData with all pools, APYs, global state, oracle decimals
let market_data: MarketData = market_client.get_market_data();

Read a user's obligation

rust
let obligation_key = ObligationKey {
    user: user_address.clone(),
    seed: None, // standard obligation
};
let obligation: Obligation = market_client.get_user_obligation(&obligation_key);

For full function signatures, see the Query Operations API page.

Depositing and Withdrawing

Deposit assets into a pool

Depositing supplies tokens to a pool and issues jTokens (supply shares) to the user's obligation. Deposited assets earn interest and count as collateral.

rust
let obligation_key = ObligationKey {
    user: user_address.clone(),
    seed: None,
};

// Deposit 1000 units of the pool's token
market_client.deposit(
    &obligation_key,    // user
    &pool_address,      // pool_address
    &1000_0000000_i128, // amount (7 decimals for XLM)
    &None,              // referrer (optional)
);

Withdraw assets from a pool

Withdrawals redeem jTokens for underlying tokens. The actual amount withdrawn may be capped to maintain the obligation's health (Open LTV).

rust
// Withdraw up to 500 units; use i128::MAX to withdraw maximum available
market_client.withdraw(
    &obligation_key,
    &pool_address,
    &500_0000000_i128,
    &None,
);

INFO

Use simulate_withdraw to preview fees and the actual withdrawal amount before committing to the transaction. See User Operations.

Borrowing and Repaying

Borrow against collateral

You must have collateral (deposits) in your obligation before borrowing. The amount you can borrow is limited by your borrowing capacity (determined by collateral value, open LTV, and liability factors).

rust
// Borrow 200 USDC against existing collateral
market_client.borrow(
    &obligation_key,
    &usdc_pool_address,
    &200_0000000_i128,
    &None,
);

Repay a loan

rust
// Repay 100 USDC; use i128::MAX to repay entire debt
market_client.repay(
    &obligation_key,
    &usdc_pool_address,
    &100_0000000_i128,
    &None,
);

WARNING

You cannot hold a deposit and a borrow in the same pool within the same obligation. If you need to earn yield on an asset you're also borrowing, use separate obligations (different seeds).

Composing a Leveraged Position

The submit_requests_batch function lets you build a leveraged position in a single atomic transaction by combining a flash borrow, swap, deposit, and borrow.

Example: 3× leveraged XLM position funded by USDC

rust
use Request::*;

let requests = vec![
    // 1. Flash borrow USDC (temporary, no collateral needed)
    FlashBorrow(StandardRequest {
        amount: 2000_0000000,
        pool_address: usdc_pool.clone(),
    }),
    // 2. Swap USDC → XLM on the DEX
    SwapExactTokens(SwapExactTokensRequest {
        swap_provider: dex_address.clone(),
        token_in: usdc_token.clone(),
        token_out: xlm_token.clone(),
        amount_in: 2000_0000000,
        min_amount_out: 1900_0000000, // slippage tolerance
    }),
    // 3. Deposit XLM as collateral (earning yield)
    Deposit(StandardRequest {
        amount: i128::MAX, // deposit everything received from the swap
        pool_address: xlm_pool.clone(),
    }),
    // 4. Borrow USDC against the XLM collateral to repay the flash loan
    Borrow(StandardRequest {
        amount: 2000_0000000,
        pool_address: usdc_pool.clone(),
    }),
    // Flash loan is auto-repaid at the end of the batch
];

market_client.submit_requests_batch(
    &obligation_key,
    &requests,
    &None, // referrer
);

The batch executes atomically — if any step fails (e.g., insufficient collateral for the borrow, swap slippage too high), the entire transaction reverts.

For the full Request enum and struct definitions, see Request & Response Types.

Building a Liquidation Bot

Liquidation bots monitor obligations for unhealthy positions and profit by repaying debt in exchange for discounted collateral.

Step 1: Discover all obligations

rust
let all_keys: Vec<ObligationKey> = market_client.get_all_obligations();

WARNING

get_all_obligations is intended for simulations. For production, read the AllObligations storage entry directly from the ledger.

Step 2: Check health for each obligation

For each obligation, fetch the obligation data and the relevant pool data, then compute the liquidation health factor (LHF):

LHF = (Σ Vc_i × cLTV_i) / (Σ Vb_j × LF_j)

If LHF < 1, the obligation is liquidatable. See Health Factor for the complete formula.

Step 3: Execute a liquidation

rust
market_client.liquidate(
    &liquidator_address,       // your address
    &borrower_obligation_key,  // target obligation
    &borrow_pool_address,      // pool of debt to repay
    &collateral_pool_address,  // pool of collateral to seize
    &repay_amount,             // amount of debt to repay
    &min_collateral_amount,    // minimum collateral you'll accept
);

Key constraints:

  • You cannot self-liquidate (liquidator ≠ borrower)
  • Each call can repay up to the pool's liquidation_close_factor_bps (e.g., 40% of the debt)
  • Collateral discount is bounded by max_liquidation_incentive_bps (e.g., 5%)
  • You can also liquidate within a batch via LiquidateRequest

For more on liquidation mechanics, see Liquidations.

Error Handling

When a transaction fails, the market contract returns an MCError with a numeric code. Common codes:

CodeNameWhat it means
103NotEnoughPoolFundsPool doesn't have enough liquidity for your borrow/withdraw
110OperationForbiddenOnPoolPool status flags prohibit this operation
207UnhealthyOperationYour borrow/withdraw would make the position unhealthy (HF < 1)
203WithdrawScarcityOverLimitWithdrawal exceeds the scarcity limit during high utilization
204ScarcityCooldownPeriodMust wait before next withdrawal (throttle active)
601ObligationIsHealthyCannot liquidate — the position is healthy
702FlashBorrowAlreadyRegisteredOnly one flash borrow is allowed per batch

For the complete error code reference, see Error Codes.