Skip to main content

Yield Adapter Interface

Oblivion integrates external yield protocols (currently Jupiter Lend and Kamino) through adapter programs. An adapter is a standalone Anchor program that implements a small, stable instruction surface area and interprets an opaque payload byte array to perform protocol-specific logic.

At a high level:

  • oblivion owns the reserve treasury token account (idle funds)
  • the keeper invokes an oblivion instruction (deposit/withdraw/refresh)
  • oblivion CPI-invokes the configured adapter program
  • the adapter CPI-invokes the underlying yield protocol (if applicable)

Where the adapter lives

  • On-chain interface crate (shared args/traits used by adapter programs): programs/yield_adapter_interface/src/lib.rs
  • Adapter program examples:
    • programs/mock_yield_adapter/src/lib.rs (used by tests)
    • programs/kamino_yield_adapter/src/lib.rs
  • Core program integration points:
    • CPI instruction data builders: programs/oblivion/src/instructions/adapter_helpers.rs
    • oblivion handlers: programs/oblivion/src/instructions/deposit_reserve_to_yield.rs, programs/oblivion/src/instructions/withdraw_reserve_from_yield.rs, programs/oblivion/src/instructions/refresh_reserve_yield_balance.rs
  • TS SDK payload builders (off-chain): packages/client/src/adapters/types.ts and packages/client/src/adapters/*.ts

Adapter program instructions

Adapters are expected to expose the following Anchor instructions (see programs/mock_yield_adapter/src/lib.rs for the minimal shape):

  • deposit(amount: u64, payload: Vec<u8>) -> Result<()>
  • withdraw(amount: u64, payload: Vec<u8>) -> Result<()>
  • refresh_balance(payload: Vec<u8>) -> Result<()> (returns u64 via Solana return data; see below)
  • run_action(payload: Vec<u8>) -> Result<()> (optional extension point)

How Oblivion calls the adapter

The oblivion program does not use an Anchor CPI client for adapters. Instead it constructs the CPI instruction data manually in programs/oblivion/src/instructions/adapter_helpers.rs:

  • deposit discriminator bytes: [242, 35, 198, 137, 82, 225, 242, 182]
  • withdraw discriminator bytes: [183, 18, 70, 156, 148, 109, 161, 34]
  • refresh_balance discriminator bytes: [103, 52, 1, 244, 153, 67, 71, 133]

Each discriminator is followed by Borsh/Anchor-serialized arguments:

  • deposit / withdraw: { amount: u64, payload: Vec<u8> }
  • refresh_balance: { payload: Vec<u8> }

oblivion enforces non-empty payload bytes on all three entrypoints:

  • deposit_reserve_to_yield and withdraw_reserve_from_yield require adapter_payload.len() > 0
  • refresh_reserve_yield_balance requires adapter_payload.len() > 0

If your adapter truly needs no parameters, pass a sentinel value (for example a single 0x00 byte) rather than an empty buffer.

Return-data contract (refresh balance)

refresh_balance returns the protocol-deployed balance by setting Solana return data to an 8-byte little-endian u64.

Example (mock adapter):

set_return_data(&balance.to_le_bytes());

oblivion reads the return data via get_return_data() and validates:

  • return_program_id == adapter_program
  • return_data.len() == 8

Then it updates the reserve accounting:

  • global_asset_reserve.protocol_balance = returned_u64
  • global_asset_reserve.total_assets_native = reserve_treasury.amount + protocol_balance
  • global_asset_reserve.last_refresh_ts = now

See programs/oblivion/src/instructions/refresh_reserve_yield_balance.rs.

Remaining accounts (adapter-specific)

oblivion forwards only ctx.remaining_accounts into the CPI call to the adapter. The adapter is responsible for interpreting these accounts in a fixed order.

This makes the adapter interface stable while allowing each protocol to define whatever CPI accounts it needs.

Examples:

If you are writing a new adapter, document your exact remaining-account layout next to the adapter instruction handlers.

Reserve accounting semantics (deposit/withdraw vs refresh)

deposit_reserve_to_yield and withdraw_reserve_from_yield update GlobalAssetReserve.protocol_balance optimistically (± amount) after the adapter CPI succeeds. They intentionally do not change total_assets_native because funds are moving between idle (treasury) and deployed (protocol), not entering/leaving the reserve.

refresh_reserve_yield_balance is the authoritative sync point. It overwrites protocol_balance from adapter return data and recomputes total_assets_native.

Allowlisting and configuration

The adapter program ID is stored on the GlobalAssetReserve as protocol_adapter and is set when the reserve is created via init_global_asset_reserve.

init_global_asset_reserve enforces that:

  • Pubkey::default() is allowed (meaning “no adapter” / idle-only reserve)
  • otherwise, the adapter program must be executable and present in ALLOWED_ADAPTER_PROGRAMS

ALLOWED_ADAPTER_PROGRAMS is compile-time-feature gated in programs/oblivion/src/lib.rs:

  • test/local builds allow mock_yield_adapter
  • devnet / staging / mainnet allow the real adapters

Off-chain payload building (TS SDK)

Off-chain, the keeper (or any caller) is responsible for producing the adapterPayload bytes passed to oblivion.

The TS client defines a small helper interface for this in packages/client/src/adapters/types.ts:

  • YieldProtocolAdapter provides buildDepositPayload, buildWithdrawPayload, and buildRefreshBalancePayload
  • payloads are adapter-defined bytes; JSON encoding is allowed but not required

packages/client/src/instructions.ts exposes typed builders for the reserve-level instructions:

  • depositReserveToYield
  • withdrawReserveFromYield
  • refreshReserveYieldBalance

Each accepts:

  • adapterProgram (must match GlobalAssetReserve.protocol_adapter)
  • adapterPayload (opaque bytes, must be non-empty)
  • optional remainingAccounts (protocol-specific account list forwarded to the adapter)