Skip to main content

Contract Map

Vanna Protocol is composed of 14 Soroban smart contracts divided into five functional groups.
GroupContractsRole
EntryAccountManager, LendingPoolXLM, LendingPoolUSDCUser-facing entry points
Per-User StateSmartAccount (one per user)Holds collateral, debt, and external positions
RiskRiskEngine, RateModelHealth checks and interest rate computation
InfrastructureRegistry, Oracle, TrackingToken, vXLM, vUSDCAddress resolution, price feeds, position tokens
DeploymentDeployerContract, DeployerLPoolContractFactory contracts for new SmartAccount instances

Layer Diagram

                         ┌──────────────────────────────┐
                         │           USER                │
                         └──────────────┬───────────────┘

              ┌─────────────────────────┼──────────────────────┐
              │                         │                       │
    ┌─────────▼──────────┐   ┌──────────▼────────┐   ┌────────▼─────────┐
    │   AccountManager   │   │  LendingPool XLM  │   │ LendingPool USDC │
    │ (margin lifecycle) │   │  (LP supply/earn) │   │ (LP supply/earn) │
    └──────┬─────────────┘   └──────────┬────────┘   └────────┬─────────┘
           │                            │                      │
           │  ┌─────────────────────────┤                      │
           │  │                         ▼                      │
    ┌──────▼──▼──────────┐   ┌──────────────────┐             │
    │    RiskEngine       │   │    RateModel     │             │
    │ (health factor      │   │ (borrow rate     │◄────────────┘
    │  borrow/withdraw    │   │  per second)     │
    │  liquidation gates) │   └──────────────────┘
    └──────┬─────────────┘

    ┌──────▼─────────────┐
    │    SmartAccount     │
    │ (per-user contract) │
    │  collateral balances│
    │  borrowed tokens    │
    │  external positions │
    └──────┬─────────────┘

    ┌──────┼──────────────────────────┐
    │      │                          │
    ▼      ▼                          ▼
  Blend  Aquarius               Soroswap
  Pool   AMM Pool               DEX Pair

Contract Descriptions

AccountManager

The single entry point for all margin operations. Users never call SmartAccount directly — every operation passes through AccountManager, which:
  1. Authenticates the caller
  2. Asks the RiskEngine whether the operation is allowed
  3. Calls the appropriate LendingPool to issue or collect debt
  4. Calls the SmartAccount to update balances and execute external protocol calls
  5. Emits events
Key functions: create_account, borrow, repay, deposit_collateral_tokens, withdraw_collateral_balance, liquidate, close_account

SmartAccount (Per-User)

Each user who opens a margin account gets their own deployed SmartAccount contract — a separate on-chain instance with its own storage. This isolates risk: a bug in one account cannot affect another. The AccountManager deploys SmartAccounts via the DeployerContract using a deterministic salt. Closed accounts are recycled into an inactive pool and reused on next open to save deployment cost. SmartAccount stores:
  • CollateralTokensList and CollateralBalanceWAD(symbol) — raw collateral
  • BorrowedTokensList — symbols with outstanding debt
  • IsAccountActive, HasDebt — account state flags
  • External positions are tracked via TrackingTokens (not stored here)
External protocol routing is handled by execute(target, action, ...) which routes to Blend, Aquarius, or Soroswap based on action type.

LendingPools (XLM · USDC)

Two separate pool contracts, one per asset. Each pool:
  • Accepts deposits and mints vTokens (vXLM, vUSDC) proportional to share of pool
  • Tracks outstanding borrows using a borrow shares model
  • Calls update_state() before every borrow/repay to accrue interest
  • Has two privileged functions only callable by AccountManager: lend_to() and collect_from()
Pool assets are fully isolated — a problem in one pool cannot affect another.

RiskEngine

A stateless contract that reads position data from SmartAccount and LendingPools, prices everything via Oracle, and returns a boolean (allowed / not allowed) for every gated operation. Three guard functions:
  • is_borrow_allowed(symbol, amount, account) — gross-asset model
  • is_withdraw_allowed(symbol, amount, account) — net-asset model
  • is_account_healthy(balance, debt) — raw comparator (1.1× threshold)
The RiskEngine caches oracle prices within a single call to avoid duplicate cross-contract calls.

RateModel

Computes the per-second borrow rate given current pool utilization. Uses a smooth polynomial curve (not a kinked two-slope model):
util = borrows / (liquidity + borrows)
rate_per_sec = 3.5 × (util×0.1 + util³²×0.1 + util⁶⁴×0.3) / SECS_PER_YEAR
Called by each LendingPool during update_state().

Registry

An on-chain address book. Every protocol contract resolves peer addresses through the Registry at runtime — no hardcoded addresses in business logic. This makes upgrades safe: update the Registry entry, not every contract.

Oracle

A thin passthrough to the Reflector oracle. Exposes get_price_latest(symbol) returning (price: u128, decimals: u32). The RiskEngine converts these to WAD format for collateral valuation.

TrackingToken

A single contract that tracks balances for synthetic position tokens — one per tracked position type:
SymbolRepresents
BLEND_XLMXLM deposited into Blend pool
BLEND_USDCUSDC deposited into Blend pool
AQ_XLM_USDCAquarius XLM/USDC LP position
SS_XLM_USDCSoroswap XLM/USDC LP position
When a SmartAccount deploys capital to Blend or Aquarius, the AccountManager mints TrackingTokens on the SmartAccount. The RiskEngine reads these to value the positions.

vTokens (vXLM · vUSDC)

Standard fungible tokens representing LP share in a lending pool. Exchange rate grows over time as borrowers repay interest. Holders earn yield passively by holding vTokens.

Key Data Flows

Deposit Flow (LP Supply)

User.deposit_xlm(lender, amount)
  → LendingPool.update_state()       [accrue pending interest]
  → LendingPool.convert_xlm_to_vtoken(amount)  [compute share]
  → NativeXLM.transfer(lender → pool)
  → vXLM.mint(lender, vtoken_amount)
  → emit deposit_event

Borrow Flow (Margin Account)

User.borrow(smart_account, amount, symbol)
  → AccountManager.borrow()
      → RiskEngine.is_borrow_allowed()
          → SmartAccount.get_all_collateral_tokens()
          → SmartAccount.get_collateral_token_balance() × n
          → Oracle.get_price_latest() × n   [cached]
          → LendingPool.get_borrow_balance() × n
          → returns true / panics
      → LendingPool.lend_to(smart_account, amount)
          → update_state()
          → compute borrow shares
          → transfer origination_fee → treasury
          → transfer amount → smart_account
      → SmartAccount.add_borrowed_token(symbol)
      → SmartAccount.set_has_debt(true)
      → emit TraderBorrowEvent

Liquidation Flow

Bot.liquidate(smart_account)
  → AccountManager.liquidate()
      → RiskEngine.is_account_healthy(collateral_usd, debt_usd)
          → returns false (account is unhealthy)
      → for each borrowed_token:
          → LendingPool.get_borrow_balance()
          → SmartAccount.remove_borrowed_token_balance()
              → LendingPool.collect_from()  [repay all debt]
      → SmartAccount.sweep_to(trader_address)  [return all collateral]
      → emit TraderLiquidateEvent

External Protocol Deployment (Blend)

User executes AccountManager.execute(smart_account, Deposit, XLM, [1000])
  → AccountManager.execute()
      → SmartAccount.execute(blend_pool, Deposit, trader, [XLM], [1000])
          → get positions_before from Blend
          → auth XLM transfer to Blend
          → BlendPool.submit([deposit_request])
          → get positions_after from Blend
          → b_tokens_minted = positions_after - positions_before
          → returns (true, b_tokens_minted)
      → TrackingToken.mint(BLEND_XLM, smart_account, b_tokens_minted)

Authorization Model

CallerCan call
User walletAccountManager, LendingPool (deposit/redeem)
AccountManagerSmartAccount (all write functions), LendingPool (lend_to, collect_from)
SmartAccountTrackingToken (mint/burn), external protocols (Blend, Aquarius, Soroswap)
AnyoneAccountManager.liquidate() — no auth required, anyone can liquidate
SmartAccount rejects all calls that don’t originate from its registered AccountManager. Users cannot call SmartAccount functions directly.

Fixed-Point Arithmetic

All balances, prices, and rates are stored in WAD format (10¹⁸ = 1.0). Asset amounts are converted to WAD at protocol boundaries:
WAD ← native:  wad = native_amount × 10¹⁸ / 10^decimals
native ← WAD:  native = wad × 10^decimals / 10¹⁸
All intermediate math uses mul_wad_down and div_wad_down (floor division) to prevent rounding in the protocol’s favor. For the full formula reference, see Math Reference.