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:
oblivionowns the reserve treasury token account (idle funds)- the keeper invokes an
oblivioninstruction (deposit/withdraw/refresh) oblivionCPI-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 oblivionhandlers: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
- CPI instruction data builders:
- TS SDK payload builders (off-chain):
packages/client/src/adapters/types.tsandpackages/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<()>(returnsu64via 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:
depositdiscriminator bytes:[242, 35, 198, 137, 82, 225, 242, 182]withdrawdiscriminator bytes:[183, 18, 70, 156, 148, 109, 161, 34]refresh_balancediscriminator 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_yieldandwithdraw_reserve_from_yieldrequireadapter_payload.len() > 0refresh_reserve_yield_balancerequiresadapter_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_programreturn_data.len() == 8
Then it updates the reserve accounting:
global_asset_reserve.protocol_balance = returned_u64global_asset_reserve.total_assets_native = reserve_treasury.amount + protocol_balanceglobal_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/mainnetallow 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:
YieldProtocolAdapterprovidesbuildDepositPayload,buildWithdrawPayload, andbuildRefreshBalancePayload- 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:
depositReserveToYieldwithdrawReserveFromYieldrefreshReserveYieldBalance
Each accepts:
adapterProgram(must matchGlobalAssetReserve.protocol_adapter)adapterPayload(opaque bytes, must be non-empty)- optional
remainingAccounts(protocol-specific account list forwarded to the adapter)