Skip to main content
Build a contract that supplies an asset, tracks its value, and withdraws on demand. Covers DeFindex strategies, SEP-56 / OpenZeppelin FungibleVault wrappers, and fee vaults. Read first: Supply and borrow, Withdraw and repay, Interest and revenue. A reference DeFindexStrategyTrait adapter maps each DeFindex vault to one controller supply account. Share accounting lives in the DeFindex vault contract; the strategy holds only a vault-to-account mapping. See DeFindex strategy for the trait API, deposit/withdraw flows, and verification checklist.

The position model

Supply positions are not tokens. The controller stores them per account (account_id: u64), keyed by asset, as scaled shares (scaled_amount = amount / supply_index, RAY = 27 decimals). Your contract is the account owner; the underlying value is scaled × supply_index, and the supply index grows as borrowers pay interest. There is no share token to hold, no emissions token to claim, and no harvest step — interest compounds into the index automatically. Three integration properties make a clean two-call loop:
  1. Views accrue to now. get_collateral_amount(account_id, hub_asset) returns the current underlying balance with interest simulated to the ledger timestamp — no keeper dependency, and the same fixed-point math the pool persists on the next mutation. What a view reports is what a deposit or withdrawal in the same transaction applies.
  2. Balance views read no oracle. get_collateral_amount, get_borrow_amount, and get_market_index stay callable through any oracle outage, so your balance() never bricks downstream accounting.
  3. Rounding always favours the pool, never you. A supply mints shares floor-rounded; a partial withdrawal burns them ceil-rounded. Both directions cost you a sub-unit rather than the pool. Do not replicate the formulas — re-read the view after the call and use what it reports.

The core loop

One strategy instance per asset; your contract is caller everywhere.

Authorize

When your contract calls supply, the controller runs exactly one nested token call: transfer(your_contract → pool, amount). Pre-authorize it with authorize_as_current_contract — no allowances, no other entries:
withdraw needs no auth entry: tokens flow from the pool toward the recipient. Your contract’s own require_auth as caller is satisfied by invoker authorization. Resolve the pool address once at deploy time from get_pool_address().

Size limits before acting

A withdrawal can revert on pool cash (#112), the market’s max-utilization cap (#127), or the account’s LTV/HF gates (only if you also borrow). A deposit can revert on the spoke supply cap (#311) or the position limit (#109).
There is no max_withdraw or max_supply view on the controller. Size an action from the underlying state and simulate the call:Indexes keep accruing after any read, so leave a margin if you act in a later transaction — or simply simulate, which is exact.

Lifecycle rules

  • Account deletion on full close. Withdrawing the last position deletes the account. Reset your stored account_id to 0; supplying with a stale id raises #24 AccountNotFound. Views degrade gracefully for missing accounts (0, empty maps, i128::MAX health factor).
  • Min borrow collateral. While the vault account carries debt, LTV-weighted collateral must stay above the instance-level MinBorrowCollateralUsd floor (get_min_borrow_collateral_usd, default $5 WAD).
  • Pause. The protocol circuit breaker blocks supply, but not withdraw — a paused protocol still lets solvent users exit. repay, renew_account, and recapitalize also stay available. A paused spoke-asset flag is stricter: it blocks that asset’s exits too.
  • Oracle posture. Every mutating flow needs a complete, valid price snapshot and fails closed otherwise — there is no lenient risk-decreasing path. Balance reporting is oracle-free.
  • Storage rent. Account keys auto-extend on every touch: when their remaining life drops below 30 days, a read or write pushes it back to 120 days. A position nobody touches gets no such renewal, so call renew_account(caller, account_id) periodically. It works while paused. Archived entries are restorable with RestoreFootprint, not lost.
  • Third parties can supply into your account, but only narrowly. A caller who is neither owner nor delegate may add only to hub assets the account already holds a supply position in, so it cannot consume your supply-position slots. Within that constraint your protocol balance is monotone-up from external action — relevant for share-price accounting; use a first-deposit inflation guard as the reference strategy does.
  • Your account is an NFT. account_id is the position-NFT token_id, and whoever holds that token is the owner the controller checks. Your contract must hold it. Transferring it transfers the whole position.

Verify

Verify your flows against the same cases the reference strategy covers: deposit/balance/withdraw round trips, third-party payout, interest accrual without index maintenance, and the account-recreation path after a full exit.

Next

Controller ABI

Exact signatures for supply, withdraw, and every view used here.

Interest and revenue

How the supply index accrues the yield your vault reports.

DeFindex strategy

The reference vault adapter: one account per vault, trait API, and test harness.

Accounts and risk

Account metadata, position limits, and spoke rules.

Errors

Every error code your integration can surface.