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.
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
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
Shared objects and caps
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
compoundadds 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.burnreads no price and has no pause check. - Royalties. Every kiosk sale pays 5%.
sweep_royaltiessends 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_suisells all of the vault's SUI to the caller for USDC at the oracle price less 0.3%, andcompoundadds 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
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
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.
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
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
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:
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
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.
Minted.list_price and backing_per_nft are the values before that mint, and paid is the larger of the two.
Error codes
Examples
These use @mysten/sui with the Transaction builder.
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
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
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
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.
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.
