diff --git a/src/examples/genesis/add_launch_pool_bucket_v2/index.js b/src/examples/genesis/add_launch_pool_bucket_v2/index.js index f6193f296..ea162acb4 100644 --- a/src/examples/genesis/add_launch_pool_bucket_v2/index.js +++ b/src/examples/genesis/add_launch_pool_bucket_v2/index.js @@ -10,9 +10,9 @@ const umiSections = { "imports": "import {\n addLaunchPoolBucketV2,\n findLaunchPoolBucketV2Pda,\n findUnlockedBucketV2Pda,\n genesis,\n} from '@metaplex-foundation/genesis'\nimport { mplToolbox } from '@metaplex-foundation/mpl-toolbox'\nimport { publicKey } from '@metaplex-foundation/umi'\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults'", "setup": "const umi = createUmi('https://api.mainnet-beta.solana.com')\n .use(mplToolbox())\n .use(genesis())\n\n// umi.use(keypairIdentity(yourKeypair));\n\n// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.", - "main": "const [launchPoolBucket] = findLaunchPoolBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\nconst [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\n\nconst now = BigInt(Math.floor(Date.now() / 1000))\nconst depositStart = now\nconst depositEnd = now + 86400n // 24 hours\nconst claimStart = depositEnd + 1n\nconst claimEnd = claimStart + 604800n // 1 week\n\nawait addLaunchPoolBucketV2(umi, {\n genesisAccount,\n baseMint: baseMint.publicKey,\n baseTokenAllocation: TOTAL_SUPPLY,\n\n // Timing\n depositStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositStart,\n triggeredTimestamp: null,\n },\n depositEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositEnd,\n triggeredTimestamp: null,\n },\n claimStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimStart,\n triggeredTimestamp: null,\n },\n claimEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimEnd,\n triggeredTimestamp: null,\n },\n\n // Optional: Minimum deposit\n minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }\n\n // Where collected SOL goes after transition\n endBehaviors: [\n {\n __kind: 'SendQuoteTokenPercentage',\n padding: Array(4).fill(0),\n destinationBucket: publicKey(unlockedBucket),\n percentageBps: 10000, // 100%\n processed: false,\n },\n ],\n}).sendAndConfirm(umi)", + "main": "const [launchPoolBucket] = findLaunchPoolBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\nconst [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\n\nconst now = BigInt(Math.floor(Date.now() / 1000))\nconst depositStart = now\nconst depositEnd = now + 86400n // 24 hours\nconst claimStart = depositEnd + 1n\nconst claimEnd = claimStart + 604800n // 1 week\n\nawait addLaunchPoolBucketV2(umi, {\n genesisAccount,\n baseMint: baseMint.publicKey,\n baseTokenAllocation: TOTAL_SUPPLY,\n\n // Timing\n depositStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositStart,\n triggeredTimestamp: null,\n },\n depositEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositEnd,\n triggeredTimestamp: null,\n },\n claimStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimStart,\n triggeredTimestamp: null,\n },\n claimEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimEnd,\n triggeredTimestamp: null,\n },\n\n // Optional: Minimum deposit\n minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }\n\n // Required since @metaplex-foundation/genesis 0.42.0: pass null for no soft cap,\n // or { amount: sol(100).basisPoints } to cap the quote tokens kept\n softCap: null,\n\n // Where collected SOL goes after transition\n endBehaviors: [\n {\n __kind: 'SendQuoteTokenPercentage',\n padding: Array(4).fill(0),\n destinationBucket: publicKey(unlockedBucket),\n percentageBps: 10000, // 100%\n processed: false,\n },\n ],\n}).sendAndConfirm(umi)", "output": "", - "full": "// [IMPORTS]\nimport {\n addLaunchPoolBucketV2,\n findLaunchPoolBucketV2Pda,\n findUnlockedBucketV2Pda,\n genesis,\n} from '@metaplex-foundation/genesis'\nimport { mplToolbox } from '@metaplex-foundation/mpl-toolbox'\nimport { publicKey } from '@metaplex-foundation/umi'\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults'\n// [/IMPORTS]\n\n// [SETUP]\nconst umi = createUmi('https://api.mainnet-beta.solana.com')\n .use(mplToolbox())\n .use(genesis())\n\n// umi.use(keypairIdentity(yourKeypair));\n\n// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.\n// [/SETUP]\n\n// [MAIN]\nconst [launchPoolBucket] = findLaunchPoolBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\nconst [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\n\nconst now = BigInt(Math.floor(Date.now() / 1000))\nconst depositStart = now\nconst depositEnd = now + 86400n // 24 hours\nconst claimStart = depositEnd + 1n\nconst claimEnd = claimStart + 604800n // 1 week\n\nawait addLaunchPoolBucketV2(umi, {\n genesisAccount,\n baseMint: baseMint.publicKey,\n baseTokenAllocation: TOTAL_SUPPLY,\n\n // Timing\n depositStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositStart,\n triggeredTimestamp: null,\n },\n depositEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositEnd,\n triggeredTimestamp: null,\n },\n claimStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimStart,\n triggeredTimestamp: null,\n },\n claimEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimEnd,\n triggeredTimestamp: null,\n },\n\n // Optional: Minimum deposit\n minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }\n\n // Where collected SOL goes after transition\n endBehaviors: [\n {\n __kind: 'SendQuoteTokenPercentage',\n padding: Array(4).fill(0),\n destinationBucket: publicKey(unlockedBucket),\n percentageBps: 10000, // 100%\n processed: false,\n },\n ],\n}).sendAndConfirm(umi)\n// [/MAIN]\n\n// [OUTPUT]\n// [/OUTPUT]\n" + "full": "// [IMPORTS]\nimport {\n addLaunchPoolBucketV2,\n findLaunchPoolBucketV2Pda,\n findUnlockedBucketV2Pda,\n genesis,\n} from '@metaplex-foundation/genesis'\nimport { mplToolbox } from '@metaplex-foundation/mpl-toolbox'\nimport { publicKey } from '@metaplex-foundation/umi'\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults'\n// [/IMPORTS]\n\n// [SETUP]\nconst umi = createUmi('https://api.mainnet-beta.solana.com')\n .use(mplToolbox())\n .use(genesis())\n\n// umi.use(keypairIdentity(yourKeypair));\n\n// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.\n// [/SETUP]\n\n// [MAIN]\nconst [launchPoolBucket] = findLaunchPoolBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\nconst [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })\n\nconst now = BigInt(Math.floor(Date.now() / 1000))\nconst depositStart = now\nconst depositEnd = now + 86400n // 24 hours\nconst claimStart = depositEnd + 1n\nconst claimEnd = claimStart + 604800n // 1 week\n\nawait addLaunchPoolBucketV2(umi, {\n genesisAccount,\n baseMint: baseMint.publicKey,\n baseTokenAllocation: TOTAL_SUPPLY,\n\n // Timing\n depositStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositStart,\n triggeredTimestamp: null,\n },\n depositEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: depositEnd,\n triggeredTimestamp: null,\n },\n claimStartCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimStart,\n triggeredTimestamp: null,\n },\n claimEndCondition: {\n __kind: 'TimeAbsolute',\n padding: Array(47).fill(0),\n time: claimEnd,\n triggeredTimestamp: null,\n },\n\n // Optional: Minimum deposit\n minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }\n\n // Required since @metaplex-foundation/genesis 0.42.0: pass null for no soft cap,\n // or { amount: sol(100).basisPoints } to cap the quote tokens kept\n softCap: null,\n\n // Where collected SOL goes after transition\n endBehaviors: [\n {\n __kind: 'SendQuoteTokenPercentage',\n padding: Array(4).fill(0),\n destinationBucket: publicKey(unlockedBucket),\n percentageBps: 10000, // 100%\n processed: false,\n },\n ],\n}).sendAndConfirm(umi)\n// [/MAIN]\n\n// [OUTPUT]\n// [/OUTPUT]\n" } export const metadata = { diff --git a/src/examples/genesis/add_launch_pool_bucket_v2/umi.js b/src/examples/genesis/add_launch_pool_bucket_v2/umi.js index 18115e48f..54d668adf 100644 --- a/src/examples/genesis/add_launch_pool_bucket_v2/umi.js +++ b/src/examples/genesis/add_launch_pool_bucket_v2/umi.js @@ -64,6 +64,10 @@ await addLaunchPoolBucketV2(umi, { // Optional: Minimum deposit minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints } + // Required since @metaplex-foundation/genesis 0.42.0: pass null for no soft cap, + // or { amount: sol(100).basisPoints } to cap the quote tokens kept + softCap: null, + // Where collected SOL goes after transition endBehaviors: [ { diff --git a/src/examples/genesis/refund_launch_pool_v2/index.js b/src/examples/genesis/refund_launch_pool_v2/index.js new file mode 100644 index 000000000..30e0f2b32 --- /dev/null +++ b/src/examples/genesis/refund_launch_pool_v2/index.js @@ -0,0 +1,32 @@ +/** + * Example: refund_launch_pool_v2 + * + * + * + * This file is auto-generated by scripts/build-examples.js + * Edit the native .js/.ts and .rs files, then run: node scripts/build-examples.js + */ + +const umiSections = { + "imports": "import {\n genesis,\n refundLaunchPoolV2,\n} from '@metaplex-foundation/genesis'\nimport { mplToolbox } from '@metaplex-foundation/mpl-toolbox'\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults'", + "setup": "const umi = createUmi('https://api.mainnet-beta.solana.com')\n .use(mplToolbox())\n .use(genesis())\n\n// umi.use(keypairIdentity(yourKeypair));\n\n// Assumes genesisAccount, launchPoolBucket, and baseMint from previous steps.\n// Only valid after the deposit window closed AND either the\n// minimumQuoteTokenThreshold was missed or the softCap was exceeded.", + "main": "// The program computes the refundable amount, so there is no amount argument.\n// A missed threshold refunds the full deposit; an exceeded soft cap refunds\n// only the excess, leaving the depositor's token allocation intact.\nawait refundLaunchPoolV2(umi, {\n genesisAccount,\n bucket: launchPoolBucket,\n baseMint: baseMint.publicKey,\n recipient: umi.identity.publicKey,\n}).sendAndConfirm(umi)", + "output": "", + "full": "// [IMPORTS]\nimport {\n genesis,\n refundLaunchPoolV2,\n} from '@metaplex-foundation/genesis'\nimport { mplToolbox } from '@metaplex-foundation/mpl-toolbox'\nimport { createUmi } from '@metaplex-foundation/umi-bundle-defaults'\n// [/IMPORTS]\n\n// [SETUP]\nconst umi = createUmi('https://api.mainnet-beta.solana.com')\n .use(mplToolbox())\n .use(genesis())\n\n// umi.use(keypairIdentity(yourKeypair));\n\n// Assumes genesisAccount, launchPoolBucket, and baseMint from previous steps.\n// Only valid after the deposit window closed AND either the\n// minimumQuoteTokenThreshold was missed or the softCap was exceeded.\n// [/SETUP]\n\n// [MAIN]\n// The program computes the refundable amount, so there is no amount argument.\n// A missed threshold refunds the full deposit; an exceeded soft cap refunds\n// only the excess, leaving the depositor's token allocation intact.\nawait refundLaunchPoolV2(umi, {\n genesisAccount,\n bucket: launchPoolBucket,\n baseMint: baseMint.publicKey,\n recipient: umi.identity.publicKey,\n}).sendAndConfirm(umi)\n// [/MAIN]\n\n// [OUTPUT]\n// [/OUTPUT]\n" +} + +export const metadata = { + title: "refund_launch_pool_v2", + description: "", + tags: [], +} + +export const examples = { + umi: { + framework: 'Umi', + language: 'javascript', + code: umiSections.full, + sections: umiSections, + }, + +} diff --git a/src/examples/genesis/refund_launch_pool_v2/umi.js b/src/examples/genesis/refund_launch_pool_v2/umi.js new file mode 100644 index 000000000..09515ad1c --- /dev/null +++ b/src/examples/genesis/refund_launch_pool_v2/umi.js @@ -0,0 +1,35 @@ +// [IMPORTS] +import { + genesis, + refundLaunchPoolV2, +} from '@metaplex-foundation/genesis' +import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' +import { createUmi } from '@metaplex-foundation/umi-bundle-defaults' +// [/IMPORTS] + +// [SETUP] +const umi = createUmi('https://api.mainnet-beta.solana.com') + .use(mplToolbox()) + .use(genesis()) + +// umi.use(keypairIdentity(yourKeypair)); + +// Assumes genesisAccount, launchPoolBucket, and baseMint from previous steps. +// Only valid after the deposit window closed AND either the +// minimumQuoteTokenThreshold was missed or the softCap was exceeded. +// [/SETUP] + +// [MAIN] +// The program computes the refundable amount, so there is no amount argument. +// A missed threshold refunds the full deposit; an exceeded soft cap refunds +// only the excess, leaving the depositor's token allocation intact. +await refundLaunchPoolV2(umi, { + genesisAccount, + bucket: launchPoolBucket, + baseMint: baseMint.publicKey, + recipient: umi.identity.publicKey, +}).sendAndConfirm(umi) +// [/MAIN] + +// [OUTPUT] +// [/OUTPUT] diff --git a/src/pages/en/smart-contracts/genesis/launch-pool.md b/src/pages/en/smart-contracts/genesis/launch-pool.md index a4db9a6b7..e2a0cf975 100644 --- a/src/pages/en/smart-contracts/genesis/launch-pool.md +++ b/src/pages/en/smart-contracts/genesis/launch-pool.md @@ -3,7 +3,7 @@ title: Launch Pool metaTitle: Genesis Launch Pool | Fair Launch & Token Distribution on Solana | Metaplex description: Fair launch token distribution on Solana. Users deposit SOL and receive SPL tokens proportionally — an on-chain crowdsale with organic price discovery on the Genesis token launchpad. created: '01-15-2025' -updated: '01-31-2026' +updated: '09-18-2026' keywords: - launch pool - token distribution @@ -17,10 +17,15 @@ keywords: - token launchpad alternative - SPL token launch - on-chain token launch + - soft cap + - oversubscription + - pro-rata refund + - minimum quote token threshold about: - Launch pools - Price discovery - Token distribution + - Soft caps proficiencyLevel: Intermediate programmingLanguage: - JavaScript @@ -45,6 +50,14 @@ faqs: a: After the deposit period ends and the claim window opens (defined by claimStartCondition). triggerBehaviorsV2 must be executed first to process end behaviors. - q: What's the difference between Launch Pool and Presale? a: Launch Pool discovers price organically based on deposits with proportional distribution. Presale has a fixed price set upfront with first-come-first-served allocation up to the cap. + - q: What is a Launch Pool soft cap? + a: A soft cap is a ceiling on the quote tokens a Launch Pool keeps, set with the softCap extension. Deposits above the cap are still accepted, the launch still succeeds, and the excess is refunded pro-rata after the deposit window closes. + - q: What is the difference between a soft cap and a minimum quote token threshold? + a: A soft cap is a ceiling on capital raised and never causes a launch to fail. A minimum quote token threshold is a floor — if total deposits fall below it, the launch fails and every depositor can take a full refund. They are separate extensions and can be used together. + - q: Do depositors receive fewer tokens when a Launch Pool is oversubscribed? + a: No. The full base token allocation is still distributed pro-rata across all deposits. Oversubscription refunds excess quote tokens instead of cutting token allocations, so the effective price is capped at softCap divided by baseTokenAllocation. + - q: Does a soft cap change the Raydium graduation start price? + a: Yes. When a Launch Pool is oversubscribed, the graduation start price is derived from the capped proceeds rather than the raw deposit total, so it matches the amount actually forwarded by SendQuoteTokenPercentage. --- **Launch Pools** provide organic price discovery for fair token launches on Solana. Users deposit SOL during a window and receive SPL tokens proportional to their share of total deposits. No sniping, no front-running, fair distribution for everyone. {% .lead %} @@ -64,11 +77,12 @@ Launch Pools are a crowdsale-style token launch mechanism that accepts deposits - Users deposit SOL during the deposit window ({% fee product="genesis" config="launchPool" fee="deposit" /%} fee applies) - Withdrawals allowed during deposit period ({% fee product="genesis" config="launchPool" fee="withdraw" /%} fee) - Token distribution is proportional to deposit share +- An optional [soft cap](#launch-pool-soft-cap) bounds how much the launch keeps, refunding the excess pro-rata - End behaviors route collected SOL to treasury buckets -## Out of Scope - -Fixed-price sales (see [Presale](/smart-contracts/genesis/presale)), bid-based auctions (see [Uniform Price Auction](/smart-contracts/genesis/uniform-price-auction)), and liquidity pool creation (use Raydium/Orca). +{% callout type="note" %} +Launch Pools discover price from deposits. For a fixed price set upfront, use [Presale](/smart-contracts/genesis/presale); for bid-based clearing, use [Uniform Price Auction](/smart-contracts/genesis/uniform-price-auction). Liquidity pool creation is handled by the Raydium graduation buckets, not by the Launch Pool bucket itself. +{% /callout %} ## Quick Start @@ -223,11 +237,130 @@ userTokens = (userDeposit / totalDeposits) * tokenAllocation **Example:** 1,000,000 tokens allocated, 100 SOL total deposits = 0.0001 SOL per token +Price discovery is unbounded by default — the more the pool is subscribed, the higher the implied price. Set a [soft cap](#launch-pool-soft-cap) to put a ceiling on it. + ### Lifecycle 1. **Deposit Period** - Users deposit SOL during a defined window 2. **`triggerBehaviorsV2`** - End behaviors execute (e.g., send collected SOL to another bucket) 3. **Claim Period** - Users claim tokens proportional to their deposit weight +4. **Refund Period** (conditional) - If a [soft cap](#launch-pool-soft-cap) was exceeded or a [minimum quote token threshold](#soft-cap-and-minimum-quote-token-threshold-together) was missed, depositors call `refundLaunchPoolV2` + +## Launch Pool Soft Cap + +A **soft cap** is a ceiling on the quote tokens a Launch Pool keeps, configured with the `softCap` extension on `addLaunchPoolBucketV2`. Deposits above the cap are still accepted during the deposit window, the launch still succeeds, and the excess quote tokens are refunded pro-rata once the window closes. + +Without a soft cap, price discovery is unbounded: every deposit is kept and the implied token price rises with subscription. A soft cap fixes the maximum the launch raises, so the effective price is `softCap / baseTokenAllocation` no matter how far deposits overshoot. + +| Property | Behaviour when a soft cap is configured | +|----------|------------------------------------------| +| Deposits above the cap | Accepted during the deposit window — deposits are never rejected at the cap | +| Launch outcome | Succeeds — a soft cap is a ceiling, never a failure condition | +| Base token distribution | Full `baseTokenAllocation` distributed pro-rata across all deposits | +| Excess quote tokens | Refunded pro-rata via `refundLaunchPoolV2` after the deposit window ends | +| Proceeds seen by end behaviors | Clamped to the soft cap, so `SendQuoteTokenPercentage` forwards at most `softCap` | +| Raydium graduation start price | Derived from the capped proceeds, not the raw deposit total | + +{% callout type="note" %} +A soft cap is a ceiling on capital raised, not a floor. The floor is a separate extension, `minimumQuoteTokenThreshold` — see [Soft Cap and Minimum Quote Token Threshold Together](#soft-cap-and-minimum-quote-token-threshold-together). +{% /callout %} + +### Configuring a Soft Cap on a Launch Pool Bucket + +Pass a `softCap` value to `addLaunchPoolBucketV2` when you add the bucket. The amount is denominated in quote token quantum units (lamports for wSOL). + +{% totem %} + +```typescript {% title="Add a Launch Pool bucket with a 100 SOL soft cap" %} +import { sol } from '@metaplex-foundation/umi'; + +await addLaunchPoolBucketV2(umi, { + genesisAccount, + baseMint: baseMint.publicKey, + baseTokenAllocation: TOTAL_SUPPLY, + // ...time conditions and end behaviors... + + // Floor: the launch fails below this and everyone can take a full refund. + minimumQuoteTokenThreshold: { amount: sol(10).basisPoints }, + + // Ceiling: the launch keeps at most this much; the rest is refunded pro-rata. + softCap: { amount: sol(100).basisPoints }, +}).sendAndConfirm(umi); +``` + +{% /totem %} + +{% callout type="warning" %} +`softCap` is a **required** argument on `addLaunchPoolBucketV2` in `@metaplex-foundation/genesis` 0.42.0. Unlike the other Launch Pool extensions it has no default, so pass `softCap: null` explicitly when you do not want a cap. +{% /callout %} + +Soft caps are validated when the extension is set and again at `finalizeV2`: + +| Rule | Error if violated | +|------|-------------------| +| `softCap.amount` must be greater than zero | `InvalidSoftCap` (221) | +| `softCap.amount` must be greater than or equal to `minimumQuoteTokenThreshold.amount` | `SoftCapBelowThreshold` (222) | +| Extensions can only be added or removed before `finalizeV2` | Account is rejected as already finalized | + +A soft cap can also be set or cleared on an existing bucket with `addLaunchPoolBucketV2Extensions` and `removeLaunchPoolBucketV2Extensions` using the `SoftCap` member of `LaunchPoolV2ExtensionType` — but only while the Genesis Account is unfinalized. + +### Oversubscription and Pro-Rata Refund Math + +A Launch Pool is **oversubscribed** when `quoteTokenDepositTotal` is strictly greater than `softCap.amount`. Each deposit is then split into a *filled* portion that counts toward the cap and an *excess* portion that is refundable: + +{% totem %} + +```text {% title="Per-depositor split in an oversubscribed Launch Pool" %} +filled_i = ceil(deposit_i * softCap / totalDeposits) +excess_i = deposit_i - filled_i +tokens_i = (weighted_i / weightedQuoteTokenTotal) * baseTokenAllocation +``` + +{% /totem %} + +`filled` rounds **up** so that the sum of all filled portions is always at least the soft cap. This keeps the bucket solvent against the capped graduation transfer regardless of whether refunds or graduation are cranked first; each depositor's rounding adds less than one quantum unit, so the dust left in the bucket is at most `depositCount - 1` quantum units in total. + +Token allocation is unaffected by the cap. Refunding excess does not remove a depositor's weighted contribution, so everyone still receives tokens proportional to their **full** deposit. + +**Worked example** — 1,000,000 tokens allocated, a 100 SOL soft cap, and 150 SOL deposited: + +| Depositor | Deposited | Filled (kept) | Refunded | Tokens received | +|-----------|-----------|---------------|----------|-----------------| +| Alice | 50 SOL | ~33.33 SOL | ~16.67 SOL | 333,333 (1/3) | +| Bob | 100 SOL | ~66.67 SOL | ~33.33 SOL | 666,667 (2/3) | +| **Total** | **150 SOL** | **100 SOL** | **50 SOL** | **1,000,000** | + +The effective price is 0.0001 SOL per token (100 SOL / 1,000,000), not the 0.00015 SOL per token the uncapped deposit total would have implied. On-chain values are computed in lamports with `filled` rounded up, so real figures differ from the rounded SOL amounts above by a few lamports. + +### Refunding Excess Deposits with refundLaunchPoolV2 + +`refundLaunchPoolV2` returns a depositor's excess quote tokens after an oversubscribed deposit window closes. It takes no amount argument — the program computes the refundable amount from the deposit, the soft cap, and the bucket's deposit total. + +{% code-tabs-imported from="genesis/refund_launch_pool_v2" frameworks="umi" filename="refundLaunchPool" /%} + +Key properties of the refund path: + +- **Cranking is permissionless.** Only `payer` must sign. If the depositor also signs, their empty base token account is closed for them. +- **No fee and no penalty** are applied to a refund; deposit and withdraw penalty schedules do not affect it. +- **Claim order does not matter.** An excess refund is allowed before or after `claimLaunchPoolV2`; both orderings converge on the same final state. +- **One refund per deposit.** A second call returns `DepositAlreadyRefunded`. +- **Refunds are gated on the deposit window ending.** Calling earlier returns `LaunchPoolNotEnded`. +- **Refunds require a failed floor or an exceeded cap.** If neither applies, the call returns `LaunchPoolThresholdMet`. + +### Soft Cap and Minimum Quote Token Threshold Together + +`softCap` and `minimumQuoteTokenThreshold` are independent extensions that bound a Launch Pool from opposite directions, and `refundLaunchPoolV2` serves both. When the floor fails, that takes precedence and the refund is a full one. + +| Configuration | Deposits below the floor | Deposits between floor and cap | Deposits above the cap | +|---------------|--------------------------|--------------------------------|------------------------| +| Neither set | Launch succeeds, no refunds | Launch succeeds, no refunds | Launch succeeds, no refunds | +| Floor only | Launch fails, full refunds | Launch succeeds, no refunds | Launch succeeds, no refunds | +| Cap only | Launch succeeds, no refunds | Launch succeeds, no refunds | Launch succeeds, excess refunded pro-rata | +| Floor and cap | Launch fails, full refunds | Launch succeeds, no refunds | Launch succeeds, excess refunded pro-rata | + +{% callout type="note" %} +A full refund removes the depositor's weighted contribution from the bucket and cannot follow a claim — a failed floor means no claim was possible. An excess refund leaves the weighted contribution intact so the pro-rata claim formula stays correct. +{% /callout %} ## Fees @@ -305,6 +438,10 @@ After the deposit period ends and claims open: Token allocation: `userTokens = (userDeposit / totalDeposits) * bucketTokenAllocation` +### Refunding a Deposit + +Refunds are available in two cases: the launch missed its `minimumQuoteTokenThreshold` (full refund), or it exceeded its `softCap` (excess-only refund). Both use the same instruction — see [Refunding Excess Deposits with refundLaunchPoolV2](#refunding-excess-deposits-with-refund-launch-pool-v2). + ## Admin Operations ### Executing `triggerBehaviorsV2` @@ -388,6 +525,36 @@ endBehaviors: [ {% /totem %} +### Launch Pool Extensions + +Extensions are optional guards configured on the Launch Pool bucket. All are set through `addLaunchPoolBucketV2`, or added and removed individually with `addLaunchPoolBucketV2Extensions` and `removeLaunchPoolBucketV2Extensions` before `finalizeV2`. + +| Extension | Type | Purpose | +|-----------|------|---------| +| `softCap` | `{ amount: bigint }` | Ceiling on quote tokens kept; excess refunded pro-rata | +| `minimumQuoteTokenThreshold` | `{ amount: bigint }` | Floor below which the launch fails and full refunds open | +| `minimumDepositAmount` | `{ amount: bigint }` | Minimum quote tokens per deposit | +| `depositLimit` | `{ limit: bigint }` | Maximum quote tokens per account | +| `allowlist` | `Allowlist` | Gates deposits to an allowlisted set of wallets | +| `claimSchedule` | `ClaimSchedule` | Vests claimed base tokens over time | +| `bonusSchedule` | `LinearBpsScheduleV2` | Time-weighted deposit bonus | +| `depositPenalty` | `LinearBpsScheduleV2` | Time-weighted deposit penalty | +| `withdrawPenalty` | `LinearBpsScheduleV2` | Time-weighted withdrawal penalty | +| `backendSigner` | `BackendSigner` | Requires a backend co-signer on user actions | + +### Common Errors + +The errors below cover invalid soft cap configurations and the Launch Pool states in which `refundLaunchPoolV2` rejects a refund request. + +| Error | Code | Cause | +|-------|------|-------| +| `InvalidSoftCap` | 221 | `softCap.amount` is zero — omit the extension instead of setting it to `0` | +| `SoftCapBelowThreshold` | 222 | `softCap.amount` is below `minimumQuoteTokenThreshold.amount` | +| `LaunchPoolNotEnded` | — | `refundLaunchPoolV2` called before the deposit window closed | +| `LaunchPoolThresholdMet` | 173 | Refund requested when the floor was met and the cap was not exceeded | +| `DepositAlreadyRefunded` | — | `refundLaunchPoolV2` called twice for the same deposit | +| `DepositAlreadyClaimed` | — | Full refund requested after the depositor already claimed tokens | + ### Fetching State **Bucket state:** @@ -402,6 +569,10 @@ console.log('Total deposits:', bucket.quoteTokenDepositTotal); console.log('Deposit count:', bucket.depositCount); console.log('Claim count:', bucket.claimCount); console.log('Token allocation:', bucket.bucket.baseTokenAllocation); + +// Soft cap state (Option) +console.log('Soft cap:', bucket.extensions.softCap); +console.log('Floor:', bucket.extensions.minimumQuoteTokenThreshold); ``` {% /totem %} @@ -419,6 +590,7 @@ const maybeDeposit = await safeFetchLaunchPoolDepositV2(umi, depositPda); // ret if (deposit) { console.log('Amount deposited:', deposit.amountQuoteToken); console.log('Claimed:', deposit.claimed); + console.log('Refunded:', deposit.refunded); } ``` @@ -431,6 +603,11 @@ if (deposit) { - If a user withdraws their entire balance, the deposit PDA closes - `triggerBehaviorsV2` must be executed after deposits close for end behaviors to process - Users must have wSOL (wrapped SOL) to deposit +- `softCap` is a required argument on `addLaunchPoolBucketV2` in `@metaplex-foundation/genesis` 0.42.0 — pass `softCap: null` when no cap is wanted +- Launch Pool extensions, including `softCap`, can only be added or removed before `finalizeV2` +- Soft caps are supported by the Genesis program and JavaScript SDK; the [`mplx` CLI](/dev-tools/cli/genesis/launch-pool) does not expose a soft cap flag yet +- An oversubscribed Launch Pool leaves rounding dust of at most `depositCount - 1` quantum units in the bucket, because each depositor's filled portion rounds up +- `quoteTokenDepositTotal` and `depositCount` are preserved as historical records after refunds; `refundCount` tracks refunds processed ## FAQ @@ -449,6 +626,18 @@ After the deposit period ends and the claim window opens (defined by `claimStart ### What's the difference between Launch Pool and Presale? Launch Pool discovers price organically based on deposits with proportional distribution. Presale has a fixed price set upfront with first-come-first-served allocation up to the cap. +### What is a Launch Pool soft cap? +A soft cap is a ceiling on the quote tokens a Launch Pool keeps, set with the `softCap` extension. Deposits above the cap are still accepted, the launch still succeeds, and the excess is refunded pro-rata after the deposit window closes. + +### What is the difference between a soft cap and a minimum quote token threshold? +A soft cap is a ceiling on capital raised and never causes a launch to fail. A `minimumQuoteTokenThreshold` is a floor — if total deposits fall below it, the launch fails and every depositor can take a full refund. They are separate extensions and can be used together. + +### Do depositors receive fewer tokens when a Launch Pool is oversubscribed? +No. The full base token allocation is still distributed pro-rata across all deposits. Oversubscription refunds excess quote tokens instead of cutting token allocations, so the effective price is capped at `softCap / baseTokenAllocation`. + +### Does a soft cap change the Raydium graduation start price? +Yes. When a Launch Pool is oversubscribed, the graduation start price is derived from the capped proceeds rather than the raw deposit total, so it matches the amount actually forwarded by `SendQuoteTokenPercentage`. + ## Glossary | Term | Definition | @@ -461,6 +650,11 @@ Launch Pool discovers price organically based on deposits with proportional dist | **Proportional Distribution** | Token allocation based on user's share of total deposits | | **Quote Token** | The token users deposit (usually wSOL) | | **Base Token** | The token being distributed | +| **Soft Cap** | Ceiling on the quote tokens a Launch Pool keeps; excess is refunded pro-rata | +| **Minimum Quote Token Threshold** | Floor below which a Launch Pool fails and full refunds open | +| **Oversubscription** | State where total deposits exceed the configured soft cap | +| **Filled Portion** | The part of a deposit that counts toward the soft cap and is kept by the launch | +| **Excess Refund** | Return of the portion of a deposit above the soft cap, leaving token allocation intact | ## Next Steps diff --git a/src/pages/en/tokens/launch-token.md b/src/pages/en/tokens/launch-token.md index 565343d9f..c9e9ab241 100644 --- a/src/pages/en/tokens/launch-token.md +++ b/src/pages/en/tokens/launch-token.md @@ -177,6 +177,7 @@ async function main() { triggeredTimestamp: null, }, minimumDepositAmount: null, + softCap: null, // required since genesis 0.42.0; pass null for no soft cap endBehaviors: [ { __kind: 'SendQuoteTokenPercentage',