Appearance
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. Useseed: Nonefor 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_batchfunction 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:
| Code | Name | What it means |
|---|---|---|
| 103 | NotEnoughPoolFunds | Pool doesn't have enough liquidity for your borrow/withdraw |
| 110 | OperationForbiddenOnPool | Pool status flags prohibit this operation |
| 207 | UnhealthyOperation | Your borrow/withdraw would make the position unhealthy (HF < 1) |
| 203 | WithdrawScarcityOverLimit | Withdrawal exceeds the scarcity limit during high utilization |
| 204 | ScarcityCooldownPeriod | Must wait before next withdrawal (throttle active) |
| 601 | ObligationIsHealthy | Cannot liquidate — the position is healthy |
| 702 | FlashBorrowAlreadyRegistered | Only one flash borrow is allowed per batch |
For the complete error code reference, see Error Codes.