Coffer Docs
Technical

Single-token Deposit

Quote, prepare accounts, and deposit one pool token

The helper swaps part of one input token, joins proportionally, then forwards LP tokens and dust. These movements are atomic; prior account setup is separate.

Supported instruction

deposit_single_token uses helper program 7BpdUH1tzTSXLuQNo6YpjJ8Eagw8AkrS6cnkxiJdCFS2. Confirm cluster/ABI compatibility.

Quote and prepare a deposit

Amounts use BN raw units. client.singleTokenDeposit is a getter exposing quote, buildTx, and buildTxs. This example batches setup:

import { CubicPoolClient, applySlippage, type BuiltTx } from "@coffer_so/sdk";
import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js";
import BN from "bn.js";

export async function prepareSingleTokenDeposit(
  client: CubicPoolClient,
  user: PublicKey,
  tokenInIndex: number,
  amountIn: BN,
  helperBalances?: BN[],
) {
  const synced = await client.sync();
  if (!synced.ok) throw new Error(synced.error.humanMessage);

  const slippageHbps = 5_000; // 0.5%; application-selected tolerance
  const quote = client.singleTokenDeposit.quote(
    tokenInIndex,
    amountIn,
    slippageHbps,
    undefined, // use the Solana Clock timestamp from sync()
    helperBalances,
  );
  if (!quote.ok) throw new Error(quote.error.humanMessage);

  const minimumBptAmount = new BN(applySlippage(
    BigInt(quote.data.estimatedBpt.toString()),
    slippageHbps,
  ).toString());
  if (minimumBptAmount.isZero()) throw new Error("Deposit is too small");

  const built = client.singleTokenDeposit.buildTxs({
    user,
    tokenInIndex,
    amountIn,
    minimumBptAmount,
  });
  if (!built.ok) throw new Error(built.error.humanMessage);

  // Replace the setup budget and batch idempotent ATA creation.
  const creates = (built.data.setup?.instructions ?? []).filter(
    ix => !ix.programId.equals(ComputeBudgetProgram.programId),
  );
  const setup: BuiltTx[] = [];
  for (let i = 0; i < creates.length; i += 3) {
    const batch = creates.slice(i, i + 3);
    const units = 50_000 * batch.length;
    setup.push({
      instructions: [
        ComputeBudgetProgram.setComputeUnitLimit({ units }),
        ...batch,
      ],
      suggestedCuLimit: units,
    });
  }

  return {
    pool: synced.data,
    quote: quote.data,
    plan: { setup, deposit: built.data.deposit },
  };
}

helperBalances contains existing helper balances before input, in pool order; omission assumes zero. sync() does not fetch them. Read/refresh these accounts to include donations or dust.

Confirm setup first, refresh the quote, and rebuild the deposit with its final LP floor. Compile using a usable ALT and simulate with wallet-added instructions. compileBuiltTx neither creates nor extends ALTs. Wrap SOL in application code. buildTx combines setup/deposit; buildTxs separates them, but its setup may still need batching. Neither guarantees transaction fit.

How the helper allocates the input

Input must have positive LP-owned actual balance. Zero-balance tokens are sidelined. For live tokens:

W_i = weight_i × min(actualBalance_i, virtualBalance_i) / virtualBalance_i
allocation_i ≈ amountIn × W_i / sum(W)

Two fixed-point divisions precede whole-unit allocation; the remainder stays in the input allocation. Actual balances already exclude protocol fees.

Keep the input allocation; swap others in ascending token-index order. Each leg uses updated reserves/windows, pays base/surge fees, and advances the input sell-off window. Independent quotes against starting state are wrong.

Reload pool/helper holdings, take the smallest proportional ratio, then apply join rounding. Forward LP tokens and leftovers. Quote fields estimatedBpt, depositedAmounts, refundAmounts estimate that sequence.

Slippage and failure conditions

deposit_single_token(amount_in: u64, token_in_index: u8, minimum_bpt_amount: u64)

Positive minimum_bpt_amount protects the whole operation. Internal swaps use minimum_amount_out = 0; quote minOuts and the slippage parameter add no on-chain guards. Explicitly build/show the final LP floor.

Disabled pool/swaps, inactive input, insufficient inventory/window capacity, dust, or an unmet floor can reject the deposit. Token movements roll back; network fees and confirmed setup do not. The builder checks extensions on every mint, including sidelined tokens touched by refunds.

Account layout

Helper PDA: ["stld_helper", pool] under the helper program. Fixed accounts: pool, helper, LP mint, helper LP account, user LP account, user signer, pool program, LP token program. Remaining accounts are five per token, in pool order:

[mint_i, user_ata_i, helper_ata_i, vault_i, token_program_i]

Derive ATAs with each mint's token program; preserve builder account order.

On this page