Skip to main content

Positions are NFTs

A position is a bounded range of bins — at most 100, i.e. one bin array’s width — owned by a Token-2022 NFT. open_position mints the NFT directly into the caller’s wallet; every downstream action (deposit, withdraw, claim fee, close) is gated to whoever currently holds that NFT, not necessarily the original opener. Selling or transferring the NFT transfers the position along with it. Ownership is checked declaratively against the position mint’s direct owner — there’s no delegate support.

Depositing by weight

A single instruction, deposit_by_weight, places a deposit across a position’s bins. The caller supplies:
  • a total (amount_x, amount_y),
  • a weight vector — one u16 weight per bin in the range,
  • a distribution_mode (below),
  • the client’s believed active_bin_id (checked against the pool’s real one, within a caller-set tolerance).
The program then allocates capital bin by bin, proportional to each bin’s weight, adjusted for that bin’s price (a higher-priced bin needs fewer raw tokens to represent the same weight). The partition is strict:
  • bins below the active bin only ever receive Y (bid liquidity),
  • bins above the active bin only ever receive X (ask liquidity),
  • the active bin may receive both.
Liquidity shapes (Spot / Curve / Bid-Ask) are not a program or SDK concept. The on-chain instruction and the SDK both only accept a raw bin_weights: number[] array — generating a flat, triangular, or U-shaped weight curve from a shape name is entirely client-side. Picon’s own web app does this in a plain TypeScript helper (generateBinWeights), not inside @picon-finance/dlmm-sdk. If you want shape presets, you’ll write that helper yourself — it’s a few lines: Spot is Array(n).fill(1); Curve peaks at the active bin and tapers outward; Bid-Ask is the inverse, troughing at the active bin.

Distribution modes

The active bin is the only bin that can receive both tokens, and how its weight splits between X and Y depends on DistributionMode: If the active bin currently holds zero liquidity, the composition ratio defaults to 50/50 — there’s no existing composition to match yet.
A position entirely on one side of the active bin can only ever absorb the corresponding token — any amount typed for the other side is silently unused (the CPI transfer for that side is a no-op, not an error). You can derive this yourself from data the SDK already exposes: pool.activeBinId < position.data.lowerBinId means the position is entirely above the active bin (X only, so a Y input should be disabled); pool.activeBinId > position.data.upperBinId means the mirror case (Y only). Worth checking before letting a user type into the side that will silently be ignored.

Withdrawing and closing

withdraw removes a proportional share (bps_to_remove, out of 10,000) of a position’s bins, rounding down. It’s bps-proportional, not price-exposed — there’s no min_amount_out-style slippage parameter, by design, since a withdrawal takes back a fixed fraction of whatever the position currently holds rather than trading against price. close_position burns the NFT and closes the Position account. It requires the position to be fully empty first — zero shares in every bin, and zero unclaimed fees. A position that still has anything in it (principal or fees) must be withdrawn and claimed before it can be closed.

Fees accrue independently of principal

LP swap fees accrue directly into the bins a position holds shares in, tracked per-bin on the position’s own FeeState slots via a lazily-materialized, checkpoint-based accumulator (the same pattern as a masterchef-style farm). claim_fee sweeps accrued fees for a caller-specified sub-range of a position’s bins, independent of withdrawing principal — you can claim fees without touching your deposited liquidity at all.

Fee state and bin-array lifecycle

A position’s unclaimed fee_owed lives on the Position account itself, not on the BinArray — so a bin array can, in principle, be deleted (delete_bin_array, admin-only, requires the array to be fully empty of shares) while a position still has fees owed against one of its bins. If that happens, claim_fee for that bin fails until the array is recreated (create_bin_array is idempotent) — the already-earned fee amount is untouched by the deletion, since it was already materialized onto the Position account before the array went away.