Skip to content
⌘K

Contracts

Rarelings contract

The rarelings Move package: 999 NFTs, each one share of a contract-held liquidity vault. Mint, burn, royalties, events and PTB examples.

The rarelings package is a collection of 999 Rareling NFTs. Each Rareling is exactly one share of a vault of sSPCX/USDC liquidity held by the contract in a concentrated-liquidity pool (Perpsplexity CLMM). Minting adds USDC to the vault, burning a Rareling pays out its share, and royalties from kiosk sales flow back into the vault.

Not yet live on mainnet The package id appears here once the package is published.

On this page, PACKAGE_ID is the latest rarelings package id. The package takes two coin types: Q is USDC and T is sSPCX. Prices come from Aftermath's on-chain price feeds. There is no keeper and no admin access to vault funds.

The package id appears above once it is published. The collection only opens after a separate launch step binds it to the sSPCX/USDC pool. Read the collection, pool and policy ids from the Launched event.

Coins and feeds

NameValue
Q (USDC, 6 decimals)0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC
T (sSPCX, 8 decimals)0x4ce80a04ebc8b54c0af24f54ddf3e256cf0a3b628118a72ae756cb6467198400::sspcx::SSPCX
SPCXUSD feed (token_feed)0x02d426b09be9ab1d10e4f8c3709be68588002174608f61485173718ba1d39db9
USDCUSD feed (usdc_feed)0x96ea92a33842446845c38992f5de53971471156e26b269c19a1f5864109ada03
SUIUSD feed (sui_feed)0xa963d9abc71d27918befc697d0b584aa7906fd8fa698e8c8d0e982acc288d97c

launch refuses any other coin types, and every price read refuses any other feed object (201). The feeds are Aftermath PriceFeedStorage objects. A feed older than 60 seconds (203) or further than 5% from its own 5 minute TWAP (204) is refused.

Modules

ModuleWhat it does
zapThe integrator entry points: launch, mint, burn, burn_to_usdc, sell_sui, compound, and the backing view.
collectionThe shared Collection<Q, T>: sale state, the vault, royalties, events and admin settings.
rarelingThe Rareling NFT, its Display and the AdminCap.
oracleFeed reads and checks, fair price maths.
paramsConstants fixed at publish.
merkleAllowlist proofs.

Shared objects and caps

ObjectOwnerWhat it is
collection::Collection<Q, T>sharedThe sale, the vault (its full-range position is wrapped here), the royalty split, the Display and a rule-less burn policy that only burn can use.
TransferPolicy<Rareling>sharedOne royalty_rule at 5% (min 0) and kiosk_lock_rule. Its cap is wrapped in the collection.
rareling::Rarelingin a kiosknumber (1-based, shown as "Rareling #N"). Always locked in a kiosk.
rareling::AdminCappublisherPhases, list prices, wallet caps, allowlist root, mint pause, one-time reveal, team reserve. It cannot touch vault funds.
UpgradeCappublisherHanded to a multisig after publishing.

Since the Publisher and the policy cap are wrapped in the collection, nobody (the owner included) can create a royalty-free policy, withdraw royalties outside sweep_royalties, or edit the Display after the reveal.

Package publishing creates a second, rule-less TransferPolicy<Rareling> used only for burns. It is wrapped in the collection, not shared, but its TransferPolicyCreated event exists. Indexers and marketplaces should use the shared policy from the Launched event.

Economics

  • Mint price is max(list price, backing per NFT) in USDC. Backing per NFT is the whole vault valued at the oracle price, rounded up, divided by the number of live Rarelings. A late minter never takes value from holders, and pushing the pool price cannot lower the mint price. The default list price is 10 USDC.
  • Zap. When the pool is within 3% of the fair price (and the CLMM is not paused), the USDC is swapped to the full-range ratio and added to the position. Otherwise it is parked in the vault and a later compound adds it. Either way the minter gets one share.
  • Burn. The holder gets floor(x * 9000 / (10000 * alive)) of the position's liquidity and of each idle balance (USDC, sSPCX, SUI). Swap fees are harvested first. The other 10% stays in the vault. The last holder gets everything. burn reads no price and has no pause check.
  • Royalties. Every kiosk sale pays 5%. sweep_royalties sends the platform's 1% to the rare.market treasury and puts the other 4% into the vault's SUI. The split is computed on the running total, so it stays exact.
  • Compounding is permissionless. sell_sui sells all of the vault's SUI to the caller for USDC at the oracle price less 0.3%, and compound adds every idle USDC and sSPCX to the position.
  • Sandwich safety. Every vault swap requires the pool within 3% of the oracle price and uses the band edge as its price limit, so a vault swap never trades more than 3% away from fair.

Entry points

mint

Move
public fun mint<Q, T>(
    c: &mut Collection<Q, T>,
    config: &Config,                      // CLMM Config
    pool: &mut Pool<T, Q>,                // the sSPCX/USDC pool
    policy: &TransferPolicy<Rareling>,
    kiosk: &mut Kiosk,
    kiosk_cap: &KioskOwnerCap,
    payment: &mut Coin<Q>,
    max_price: u64,
    proof: vector<vector<u8>>,            // [] in the public phase
    token_feed: &PriceFeedStorage,
    usdc_feed: &PriceFeedStorage,
    sui_feed: &PriceFeedStorage,
    clock: &Clock,
    ctx: &mut TxContext,
)

Mints the next Rareling, takes the mint price from payment and locks the NFT in kiosk. Each call mints one. Aborts: 1 minting paused, 2 phase closed, 3 sold out, 4 wallet cap reached, 5 not on the allowlist, 6 payment below the price, 12 price above max_price, 102 wrong pool, 106 wrong policy, and 201 to 204 for feed problems.

The price can move between reading it and minting. Read the current mint price from the backing view and pass max_price with a small margin.

burn

Move
public fun burn<Q, T>(
    c: &mut Collection<Q, T>,
    config: &Config,
    pool: &mut Pool<T, Q>,
    nft: Rareling,
    request: TransferRequest<Rareling>,
    ctx: &mut TxContext,
): (Coin<Q>, Coin<T>, Coin<SUI>)

Burns a Rareling for its share of the vault and returns USDC, sSPCX and SUI. Get nft and request out of your kiosk with 0x2::kiosk::list at price 0 and 0x2::kiosk::purchase with a zero SUI coin. The request is confirmed against the collection's internal burn policy, so no royalty is charged. Burns work while minting is paused and while the CLMM is paused, and they never read a price. Aborts: 11 the request is not for this NFT, 102 wrong pool.

Move
public fun burn_to_usdc<Q, T>(c: &mut Collection<Q, T>, config: &Config, pool: &mut Pool<T, Q>, nft: Rareling, request: TransferRequest<Rareling>, min_usdc_out: u64, ctx: &mut TxContext): (Coin<Q>, Coin<T>, Coin<SUI>)

Same as burn, then sells the sSPCX part for USDC in the same pool. Aborts with 105 unless you end up with at least min_usdc_out.

Royalties and compounding

Move
public fun sweep_royalties<Q, T>(c: &mut Collection<Q, T>, policy: &mut TransferPolicy<Rareling>, ctx: &mut TxContext)
public fun sell_sui<Q, T>(c: &mut Collection<Q, T>, payment: &mut Coin<Q>, usdc_feed: &PriceFeedStorage, sui_feed: &PriceFeedStorage, clock: &Clock, ctx: &mut TxContext): Coin<SUI>
public fun compound<Q, T>(c: &mut Collection<Q, T>, config: &Config, pool: &mut Pool<T, Q>, token_feed: &PriceFeedStorage, usdc_feed: &PriceFeedStorage, clock: &Clock, ctx: &mut TxContext)

All three can be called by anyone. sweep_royalties is in collection, the other two in zap. sell_sui charges SUIUSD / USDCUSD * (1 - 0.3%), rounded up, from payment (107 if short), so the caller earns the 0.3% discount and can source the USDC from any route. compound harvests the position's fees and adds every idle balance to the position. It needs fresh feeds and the pool within the band (104).

Views

Move
public fun backing<Q, T>(c: &Collection<Q, T>, pool: &Pool<T, Q>, token_feed: &PriceFeedStorage, usdc_feed: &PriceFeedStorage, sui_feed: &PriceFeedStorage, clock: &Clock): Backing
public fun pending_fees<Q, T>(c: &Collection<Q, T>, pool: &Pool<T, Q>): (u64, u64)   // unharvested (usdc, sspcx)

Read backing with a simulated transaction. Backing fields, in BCS order:

FieldTypeMeaning
aliveu64Live Rarelings
liquidityu128Position liquidity
liquidity_per_nftu128
usdc_per_nft, token_per_nft, sui_per_nftu64What one share is made of at the pool price
value_per_nftu64Everything at the oracle price, in USDC base units, rounded up
list_priceu64Current phase's list price
mint_priceu64What a mint costs now
fair_sqrt_priceu128From the oracle
pool_sqrt_priceu128From the pool

Views on collection: minted, alive, burned, reserve_minted, phase (0 closed, 1 allowlist, 2 public), paused, sale(phase) (list price and wallet cap), minted_by(phase, who), revealed, base_uri, liquidity, idle (usdc, sspcx, sui), royalties (total swept, platform paid), pool_id, policy_id. On rareling: number.

Admin

AdminCap calls in collection: set_phase, set_sale, set_allowlist_root, set_paused (mints only), reveal (once) and mint_reserve (team reserve, 0 by default). None of them can move vault funds.

Allowlist

Leaf: blake2b256(bcs(address)). Each level hashes the pair in ascending byte order: blake2b256(min(a, b) || max(a, b)), with no prefix bytes. The proof is the sibling hashes, bottom up. Pass an empty proof in the public phase.

Display

Before the reveal: name (Rareling #{number}), number, description, image_url and thumbnail_url (a placeholder at https://rare.market/rarelings/unrevealed.png), project_url, link and creator. reveal(base_uri) runs once. It sets image_url and thumbnail_url to <base_uri>/{number}.png and adds metadata_url <base_uri>/{number}.json, which holds the attributes.

Fees and royalties

WhatRateGoes to
Kiosk sale royalty5% (one royalty_rule)1% rare.market treasury, 4% the vault, split by sweep_royalties
Burn exit fee10% of the burner's shareStays in the vault for remaining holders
SUI sale discount0.3%The caller of sell_sui

These are constants compiled into the package. There is no mint fee beyond the mint price, which goes entirely into the vault.

Events

All events are in PACKAGE_ID::collection.

EventFields
Launchedcollection, pool, policy
Mintedcollection, number, minter, phase: u8, list_price, backing_per_nft, paid, zapped: bool, liquidity_added: u128, alive, minted
Burnedcollection, number, burner, alive_before, liquidity_before: u128, burner_liquidity: u128, project_liquidity: u128, usdc_out, token_out, sui_out, project_usdc, project_token, project_sui
RoyaltiesSweptcollection, amount, to_vault, to_platform, total_swept, total_platform
SuiSoldcollection, buyer, sui, usdc, sui_usd: u128, usdc_usd: u128
Compoundedcollection, usdc_in, token_in, liquidity_added: u128, usdc_left, token_left
FeesHarvestedcollection, usdc, token
SaleSetcollection, phase: u8, price, wallet_cap
PhaseSetcollection, phase: u8
PausedSetcollection, paused
AllowlistSetcollection, root: vector<u8>
Revealedcollection, base_uri

Minted.list_price and backing_per_nft are the values before that mint, and paid is the larger of the two.

Error codes

ModuleCodes
collection1 EPaused, 2 EPhaseClosed, 3 ESoldOut, 4 EWalletCap, 5 ENotAllowlisted, 6 EPayment, 7 EAlreadyRevealed, 8 EBadPhase, 9 EReserveExhausted, 10 EWrongPolicy, 11 EWrongItem, 12 EAboveMaxPrice, 14 EEmptyUri
zap101 EWrongType, 102 EWrongPool, 103 ETreasuryUnset, 104 EOutOfBand, 105 ESlippage, 106 EWrongPolicy, 107 EPayment
oracle201 EWrongFeed, 202 EBadPrice, 203 EStale, 204 ETwapDeviation

Examples

These use @mysten/sui with the Transaction builder.

TypeScript
import { Transaction } from "@mysten/sui/transactions";

const PACKAGE_ID = "0x...";      // latest rarelings package id
const COLLECTION_ID = "0x...";   // `collection` from the Launched event
const POOL_ID = "0x...";         // `pool` from the Launched event
const POLICY_ID = "0x...";       // `policy` from the Launched event
const CLMM_CONFIG_ID = "0x...";  // the CLMM Config object the pool belongs to

const USDC = "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC";
const SSPCX = "0x4ce80a04ebc8b54c0af24f54ddf3e256cf0a3b628118a72ae756cb6467198400::sspcx::SSPCX";
const RARELING = `${PACKAGE_ID}::rareling::Rareling`;
const FEED_SPCX = "0x02d426b09be9ab1d10e4f8c3709be68588002174608f61485173718ba1d39db9";
const FEED_USDC = "0x96ea92a33842446845c38992f5de53971471156e26b269c19a1f5864109ada03";
const FEED_SUI = "0xa963d9abc71d27918befc697d0b584aa7906fd8fa698e8c8d0e982acc288d97c";

Mint into a new kiosk

TypeScript
export function mintRareling(
  tx: Transaction,
  sender: string,
  usdcCoinId: string,  // a USDC coin with at least maxPrice
  maxPrice: bigint,    // backing().mint_price plus a small margin
  proof: number[][] = [],
) {
  const [kiosk, kioskCap] = tx.moveCall({ target: "0x2::kiosk::new" });
  tx.moveCall({
    target: `${PACKAGE_ID}::zap::mint`,
    typeArguments: [USDC, SSPCX],
    arguments: [
      tx.object(COLLECTION_ID),
      tx.object(CLMM_CONFIG_ID),
      tx.object(POOL_ID),
      tx.object(POLICY_ID),
      kiosk,
      kioskCap,
      tx.object(usdcCoinId), // &mut: the mint price is split off, the rest stays
      tx.pure.u64(maxPrice),
      tx.pure.vector("vector<u8>", proof),
      tx.object(FEED_SPCX),
      tx.object(FEED_USDC),
      tx.object(FEED_SUI),
      tx.object("0x6"),
    ],
  });
  tx.moveCall({
    target: "0x2::transfer::public_share_object",
    typeArguments: ["0x2::kiosk::Kiosk"],
    arguments: [kiosk],
  });
  tx.transferObjects([kioskCap], sender);
}

With an existing kiosk, pass it and its cap instead. Repeat the zap::mint call to mint several Rarelings in one PTB.

Burn for USDC

TypeScript
export function burnRareling(
  tx: Transaction,
  sender: string,
  kioskId: string,
  kioskCapId: string,
  rarelingId: string,
  minUsdcOut: bigint,
) {
  tx.moveCall({
    target: "0x2::kiosk::list",
    typeArguments: [RARELING],
    arguments: [tx.object(kioskId), tx.object(kioskCapId), tx.pure.id(rarelingId), tx.pure.u64(0)],
  });
  const [zero] = tx.splitCoins(tx.gas, [0]);
  const [nft, request] = tx.moveCall({
    target: "0x2::kiosk::purchase",
    typeArguments: [RARELING],
    arguments: [tx.object(kioskId), tx.pure.id(rarelingId), zero],
  });
  const [usdc, sspcx, sui] = tx.moveCall({
    target: `${PACKAGE_ID}::zap::burn_to_usdc`,
    typeArguments: [USDC, SSPCX],
    arguments: [tx.object(COLLECTION_ID), tx.object(CLMM_CONFIG_ID), tx.object(POOL_ID), nft, request, tx.pure.u64(minUsdcOut)],
  });
  tx.transferObjects([usdc, sspcx, sui], sender);
}

Use zap::burn without min_usdc_out to receive the sSPCX part as is.

Read the mint price

TypeScript
const tx = new Transaction();
tx.moveCall({
  target: `${PACKAGE_ID}::zap::backing`,
  typeArguments: [USDC, SSPCX],
  arguments: [
    tx.object(COLLECTION_ID),
    tx.object(POOL_ID),
    tx.object(FEED_SPCX),
    tx.object(FEED_USDC),
    tx.object(FEED_SUI),
    tx.object("0x6"),
  ],
});
// Simulate the transaction (devInspect or dry run) and decode the return value
// with the Backing layout above. `mint_price` is what a mint costs right now.

Sweep, sell SUI and compound

Anyone can run this whenever royalties build up. The caller earns the 0.3% discount on the SUI.

TypeScript
tx.moveCall({
  target: `${PACKAGE_ID}::collection::sweep_royalties`,
  typeArguments: [USDC, SSPCX],
  arguments: [tx.object(COLLECTION_ID), tx.object(POLICY_ID)],
});
const [sui] = tx.moveCall({
  target: `${PACKAGE_ID}::zap::sell_sui`,
  typeArguments: [USDC, SSPCX],
  arguments: [tx.object(COLLECTION_ID), tx.object(usdcCoinId), tx.object(FEED_USDC), tx.object(FEED_SUI), tx.object("0x6")],
});
tx.transferObjects([sui], sender);
tx.moveCall({
  target: `${PACKAGE_ID}::zap::compound`,
  typeArguments: [USDC, SSPCX],
  arguments: [tx.object(COLLECTION_ID), tx.object(CLMM_CONFIG_ID), tx.object(POOL_ID), tx.object(FEED_SPCX), tx.object(FEED_USDC), tx.object("0x6")],
});

Rarelings trade on the marketplace like any kiosk collection: the 5% royalty is paid and the item is locked into the buyer's kiosk.

--
TPS:--