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 implementation ships in the protocol repo at contracts/defindex-strategy: a complete DeFindexStrategyTrait adapter that 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 integration tests.

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_ray = 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, including admin-funded rewards (add_rewards). Three guarantees make the integration a clean two-call loop:
  1. Views accrue to now. collateral_amount_for_token(account_id, 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. collateral_amount_for_token, borrow_amount_for_token, and get_market_index stay callable through any oracle outage, so your balance() never bricks downstream accounting.
  3. Rounding is deterministic and conservative. Credits round down (floor), debits round up (ceil), neutral conversions half-up, at RAY precision. Measure your exact credited value by re-reading the view after the call instead of replicating formulas.

The core loop

One strategy instance per asset; your contract is caller everywhere.
// Deposit: pull tokens in, then supply. account_id = 0 opens an account;
// persist the returned id.
let id = controller.supply(&strategy, &stored_id_or_0, &0u32,
    &vec![&env, (asset.clone(), amount)]);

// Balance: one call, underlying units, interest accrued to now.
let value = controller.collateral_amount_for_token(&id, &asset);

// Withdraw: pay any recipient directly; returns actual amounts paid.
// amount = 0 closes the position; over-asking clamps to a full close.
let paid = controller.withdraw(&strategy, &id,
    &vec![&env, (asset.clone(), amount)], &Some(recipient));

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:
env.authorize_as_current_contract(vec![&env,
    InvokerContractAuthEntry::Contract(SubContractInvocation {
        context: ContractContext {
            contract: asset.clone(),
            fn_name: Symbol::new(&env, "transfer"),
            args: (env.current_contract_address(), pool.clone(), amount).into_val(&env),
        },
        sub_invocations: Vec::new(&env),
    }),
]);
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_all_markets_detailed.

Preview limits before acting

A withdrawal can revert on pool cash, the market’s max-utilization cap, or the account’s LTV/HF gates (only if you also borrow). A deposit can revert on the supply cap. Two views model the exact enforcement so you can clamp instead of revert:
ViewReturns
max_withdraw(account_id, asset)Largest withdrawal the next transaction can execute. Equals the full balance whenever a full close is feasible; 0 while paused.
max_supply(asset)Remaining supply-cap headroom; i128::MAX when uncapped; 0 while paused or the market is not active.
Both are previews against live state: indexes keep accruing after the read, so leave a margin if you act in a later transaction.

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 owner circuit breaker blocks supply and withdraw (previews return 0). repay and renew_account stay available.
  • Oracle posture. Mutating flows need a resolvable price; a debt-free account withdraws under the lenient risk-decreasing policy (stale or deviating prices tolerated, total feed loss not). Balance reporting is oracle-free.
  • Storage rent. Account keys auto-extend on every touch; a dormant position should call renew_account(owner, account_id) periodically (it works even while paused). Archived entries are restorable via RestoreFootprint, not lost.
  • Anyone can supply into your account. Supply is not owner-gated (account_id is the routing key), so treat your protocol balance as monotone-up from external action — relevant for share-price accounting; use a first-deposit inflation guard as the reference strategy does.

Verify

Run your flows against the reference strategy’s integration tests (contracts/defindex-strategy/tests): 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.