런치 유형

Presale

Last updated January 31, 2026

Presale은 솔라나에서 고정가 토큰 배포를 제공합니다. 할당량과 SOL 상한을 기반으로 SPL 토큰 가격을 미리 설정합니다. 사용자는 받을 수량을 정확히 알고, 여러분은 모금액을 정확히 파악할 수 있습니다. Genesis에서 "Presale"이란 초기 거래 직전에 토큰이 판매되는 것을 의미합니다. 구매자는 토큰을 직접 받으며, 미래에 토큰을 받을 권리가 아닙니다.

학습 내용

이 가이드에서 다루는 내용:

  • Presale 가격 책정 방식 (할당량 + 상한 = 가격)
  • 예치 기간과 청구 기간 설정
  • 예치 한도와 쿨다운 설정
  • 사용자 작업: SOL 래핑, 예치, 청구

요약

Presale은 기존 고정가 토큰 판매와 유사하게 미리 정해진 가격으로 토큰을 판매합니다. 가격은 설정한 토큰 할당량과 SOL 상한에서 계산되며, 알려진 밸류에이션으로 암호화폐 자금 조달에 이상적입니다.

  • 고정 가격 = SOL 상한 / 토큰 할당량
  • 사용자는 예치 기간 동안 SOL을 예치합니다 (2% 수수료 적용)
  • SOL 상한까지 선착순
  • 옵션: 최소/최대 예치 한도, 쿨다운, 백엔드 인증

범위 외

자연스러운 가격 발견(Launch Pool 참조), 입찰 기반 경매(Uniform Price Auction 참조), 베스팅 스케줄.

작동 방식

  1. SOL 상한을 설정하여 고정 가격을 결정하고, 토큰을 Presale에 할당합니다
  2. 사용자는 예치 기간 동안 고정 비율로 SOL을 예치합니다
  3. 예치 기간 종료 후, 트랜지션을 실행하여 자금을 이동합니다
  4. 사용자는 예치금에 따라 토큰을 청구합니다

가격 계산

토큰 가격은 할당 토큰과 SOL 상한의 비율로 결정됩니다:

price = allocationQuoteTokenCap / baseTokenAllocation
tokens = deposit / price

예를 들어, 100 SOL 상한으로 1,000,000 토큰을 할당한 경우:

  • 가격 = 100 SOL / 1,000,000 토큰 = 토큰당 0.0001 SOL
  • 10 SOL 예치로 100,000 토큰을 받습니다

수수료

인스트럭션Solana
Deposit2%
Graduation5%

빠른 시작

설정 가이드

사전 요구 사항

npm install @metaplex-foundation/genesis @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-toolbox

1. Genesis Account 초기화

Genesis Account는 토큰을 생성하고 모든 배포 Bucket을 조정합니다.

initializeV2.ts
1import {
2 findGenesisAccountV2Pda,
3 genesis,
4 initializeV2,
5} from '@metaplex-foundation/genesis'
6import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
7import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
9
10const umi = createUmi('https://api.mainnet-beta.solana.com')
11 .use(mplToolbox())
12 .use(genesis())
13
14// umi.use(keypairIdentity(yourKeypair));
15
16const baseMint = generateSigner(umi)
17const TOTAL_SUPPLY = 1_000_000_000_000_000n // 1 million tokens (9 decimals)
18
19// Store this account address for later or recreate it when needed.
20const [genesisAccount] = findGenesisAccountV2Pda(umi, {
21 baseMint: baseMint.publicKey,
22 genesisIndex: 0,
23})
24
25await initializeV2(umi, {
26 baseMint,
27 fundingMode: 0,
28 totalSupplyBaseToken: TOTAL_SUPPLY,
29 name: 'My Token',
30 symbol: 'MTK',
31 uri: 'https://example.com/metadata.json',
32}).sendAndConfirm(umi)

totalSupplyBaseToken은 모든 Bucket 할당량의 합계와 같아야 합니다.

2. Presale Bucket 추가

Presale Bucket은 예치금을 수집하고 토큰을 배포합니다. 여기서 타이밍과 선택적 제한을 설정합니다.

addPresaleBucket.ts
1import {
2 genesis,
3 addPresaleBucketV2,
4 findPresaleBucketV2Pda,
5 findUnlockedBucketV2Pda,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, baseMint, and TOTAL_SUPPLY from the Initialize step.
18
19const [presaleBucket] = findPresaleBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
20const [unlockedBucket] = findUnlockedBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 })
21
22const now = BigInt(Math.floor(Date.now() / 1000))
23const depositStart = now
24const depositEnd = now + 86400n // 24 hours
25const claimStart = depositEnd + 1n
26const claimEnd = claimStart + 604800n // 1 week
27
28await addPresaleBucketV2(umi, {
29 genesisAccount,
30 baseMint: baseMint.publicKey,
31 baseTokenAllocation: TOTAL_SUPPLY,
32 allocationQuoteTokenCap: 100_000_000_000n, // 100 SOL cap (sets price)
33
34 // Timing
35 depositStartCondition: {
36 __kind: 'TimeAbsolute',
37 padding: Array(47).fill(0),
38 time: depositStart,
39 triggeredTimestamp: null,
40 },
41 depositEndCondition: {
42 __kind: 'TimeAbsolute',
43 padding: Array(47).fill(0),
44 time: depositEnd,
45 triggeredTimestamp: null,
46 },
47 claimStartCondition: {
48 __kind: 'TimeAbsolute',
49 padding: Array(47).fill(0),
50 time: claimStart,
51 triggeredTimestamp: null,
52 },
53 claimEndCondition: {
54 __kind: 'TimeAbsolute',
55 padding: Array(47).fill(0),
56 time: claimEnd,
57 triggeredTimestamp: null,
58 },
59
60 // Optional: Deposit limits
61 minimumDepositAmount: null, // or { amount: sol(0.1).basisPoints }
62 depositLimit: null, // or { limit: sol(10).basisPoints }
63
64 // Where collected SOL goes after transition
65 endBehaviors: [
66 {
67 __kind: 'SendQuoteTokenPercentage',
68 padding: Array(4).fill(0),
69 destinationBucket: publicKey(unlockedBucket),
70 percentageBps: 10000, // 100%
71 processed: false,
72 },
73 ],
74}).sendAndConfirm(umi)

3. Unlocked Bucket 추가

Unlocked Bucket은 트랜지션 후 Presale에서 SOL을 받습니다.

addUnlockedBucket.ts
1import {
2 addUnlockedBucketV2,
3 genesis,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { generateSigner, keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, baseMint, claimStart, and claimEnd from previous steps.
16
17await addUnlockedBucketV2(umi, {
18 genesisAccount,
19 baseMint: baseMint.publicKey,
20 baseTokenAllocation: 0n,
21 recipient: umi.identity.publicKey,
22 claimStartCondition: {
23 __kind: 'TimeAbsolute',
24 padding: Array(47).fill(0),
25 time: claimStart,
26 triggeredTimestamp: null,
27 },
28 claimEndCondition: {
29 __kind: 'TimeAbsolute',
30 padding: Array(47).fill(0),
31 time: claimEnd,
32 triggeredTimestamp: null,
33 },
34 backendSigner: null,
35}).sendAndConfirm(umi)

4. Finalize

모든 Bucket이 설정되면 Finalize하여 Presale을 활성화합니다. 이는 되돌릴 수 없습니다.

finalize.ts
1import {
2 genesis,
3 finalizeV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount and baseMint from the Initialize step.
16
17await finalizeV2(umi, {
18 baseMint: baseMint.publicKey,
19 genesisAccount,
20}).sendAndConfirm(umi)

사용자 작업

SOL 래핑

사용자는 예치 전에 SOL을 wSOL로 래핑해야 합니다.

wrapSol.ts
1import {
2 findAssociatedTokenPda,
3 createTokenIfMissing,
4 transferSol,
5 syncNative,
6 mplToolbox,
7} from '@metaplex-foundation/mpl-toolbox'
8import { WRAPPED_SOL_MINT, genesis } from '@metaplex-foundation/genesis'
9import { keypairIdentity, publicKey, sol } from '@metaplex-foundation/umi'
10import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
11
12const umi = createUmi('https://api.mainnet-beta.solana.com')
13 .use(mplToolbox())
14 .use(genesis())
15
16// umi.use(keypairIdentity(yourKeypair));
17
18const userWsolAccount = findAssociatedTokenPda(umi, {
19 owner: umi.identity.publicKey,
20 mint: WRAPPED_SOL_MINT,
21})
22
23await createTokenIfMissing(umi, {
24 mint: WRAPPED_SOL_MINT,
25 owner: umi.identity.publicKey,
26 token: userWsolAccount,
27})
28 .add(
29 transferSol(umi, {
30 destination: publicKey(userWsolAccount),
31 amount: sol(10),
32 })
33 )
34 .add(syncNative(umi, { account: userWsolAccount }))
35 .sendAndConfirm(umi)

예치

depositPresale.ts
1import {
2 genesis,
3 depositPresaleV2,
4 findPresaleDepositV2Pda,
5 fetchPresaleDepositV2,
6} from '@metaplex-foundation/genesis'
7import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
8import { keypairIdentity, sol } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com')
12 .use(mplToolbox())
13 .use(genesis())
14
15// umi.use(keypairIdentity(yourKeypair));
16
17// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
18
19await depositPresaleV2(umi, {
20 genesisAccount,
21 bucket: presaleBucket,
22 baseMint: baseMint.publicKey,
23 amountQuoteToken: sol(1).basisPoints,
24}).sendAndConfirm(umi)
25
26// Verify
27const [depositPda] = findPresaleDepositV2Pda(umi, {
28 bucket: presaleBucket,
29 recipient: umi.identity.publicKey,
30})
31const deposit = await fetchPresaleDepositV2(umi, depositPda)
32
33console.log('Deposited (after fee):', deposit.amountQuoteToken)

동일한 사용자의 여러 예치는 단일 예치 계정에 누적됩니다.

토큰 청구

예치 기간 종료 후 청구가 시작되면:

claimPresale.ts
1import {
2 genesis,
3 claimPresaleV2,
4} from '@metaplex-foundation/genesis'
5import { mplToolbox } from '@metaplex-foundation/mpl-toolbox'
6import { keypairIdentity } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com')
10 .use(mplToolbox())
11 .use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair));
14
15// Assumes genesisAccount, presaleBucket, and baseMint from previous steps.
16
17await claimPresaleV2(umi, {
18 genesisAccount,
19 bucket: presaleBucket,
20 baseMint: baseMint.publicKey,
21 recipient: umi.identity.publicKey,
22}).sendAndConfirm(umi)

토큰 할당: userTokens = (userDeposit / allocationQuoteTokenCap) * baseTokenAllocation

관리자 작업

트랜지션 실행

예치 종료 후, 트랜지션을 실행하여 수집된 SOL을 Unlocked Bucket으로 이동합니다.

transitionPresale.ts
1import {
2 genesis,
3 transitionV2,
4 WRAPPED_SOL_MINT,
5} from '@metaplex-foundation/genesis'
6import {
7 findAssociatedTokenPda,
8 mplToolbox,
9} from '@metaplex-foundation/mpl-toolbox'
10import { keypairIdentity } from '@metaplex-foundation/umi'
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
12
13const umi = createUmi('https://api.mainnet-beta.solana.com')
14 .use(mplToolbox())
15 .use(genesis())
16
17// umi.use(keypairIdentity(yourKeypair));
18
19// Assumes genesisAccount, presaleBucket, unlockedBucket, and baseMint from previous steps.
20
21const [unlockedBucketQuoteTokenAccount] = findAssociatedTokenPda(umi, {
22 owner: unlockedBucket,
23 mint: WRAPPED_SOL_MINT,
24})
25
26await transitionV2(umi, {
27 genesisAccount,
28 primaryBucket: presaleBucket,
29 baseMint: baseMint.publicKey,
30})
31 .addRemainingAccounts([
32 { pubkey: unlockedBucket, isSigner: false, isWritable: true },
33 { pubkey: unlockedBucketQuoteTokenAccount, isSigner: false, isWritable: true },
34 ])
35 .sendAndConfirm(umi)

이것이 중요한 이유: 트랜지션이 없으면 수집된 SOL은 Presale Bucket에 잠긴 상태로 유지됩니다. 사용자는 토큰을 청구할 수 있지만 팀은 모금한 자금에 접근할 수 없습니다.

참조

설정 옵션

이 옵션들은 Presale Bucket 생성 시 설정됩니다:

옵션설명예시
minimumDepositAmount거래당 최소 예치{ amount: sol(0.1).basisPoints }
depositLimit사용자당 최대 총 예치{ limit: sol(10).basisPoints }
depositCooldown예치 간 대기 시간{ seconds: 60n }
perCooldownDepositLimit쿨다운 기간당 최대 예치{ amount: sol(1).basisPoints }

시간 조건

4가지 조건이 Presale 타이밍을 제어합니다:

조건목적
depositStartCondition예치 시작 시점
depositEndCondition예치 종료 시점
claimStartCondition청구 시작 시점
claimEndCondition청구 종료 시점

Unix 타임스탬프와 함께 TimeAbsolute를 사용합니다:

const condition = {
__kind: 'TimeAbsolute',
padding: Array(47).fill(0),
time: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1시간 후
triggeredTimestamp: null,
};

종료 동작

예치 기간 후 수집된 SOL의 처리를 정의합니다:

endBehaviors: [
{
__kind: 'SendQuoteTokenPercentage',
padding: Array(4).fill(0),
destinationBucket: publicKey(unlockedBucket),
percentageBps: 10000, // 100% = 10000 베이시스 포인트
processed: false,
},
]

상태 조회

Bucket 상태:

import { fetchPresaleBucketV2 } from '@metaplex-foundation/genesis';
const bucket = await fetchPresaleBucketV2(umi, presaleBucket);
console.log('Total deposits:', bucket.quoteTokenDepositTotal);
console.log('Deposit count:', bucket.depositCount);
console.log('Token allocation:', bucket.bucket.baseTokenAllocation);
console.log('SOL cap:', bucket.allocationQuoteTokenCap);

예치 상태:

import { fetchPresaleDepositV2, safeFetchPresaleDepositV2 } from '@metaplex-foundation/genesis';
const deposit = await fetchPresaleDepositV2(umi, depositPda); // 찾을 수 없으면 에러 발생
const maybeDeposit = await safeFetchPresaleDepositV2(umi, depositPda); // null 반환
if (deposit) {
console.log('Amount deposited:', deposit.amountQuoteToken);
console.log('Amount claimed:', deposit.amountClaimed);
console.log('Fully claimed:', deposit.claimed);
}

참고 사항

  • 예치에는 2% 프로토콜 수수료가 적용됩니다
  • 사용자는 예치 전에 SOL을 wSOL로 래핑해야 합니다
  • 동일한 사용자의 여러 예치는 하나의 예치 계정에 누적됩니다
  • 팀이 자금에 접근하려면 예치 종료 후 트랜지션을 실행해야 합니다
  • Finalize는 영구적입니다—finalizeV2를 호출하기 전에 모든 설정을 다시 확인하세요

FAQ

Presale에서 토큰 가격은 어떻게 계산되나요?

가격은 SOL 상한을 토큰 할당량으로 나눈 값입니다. 100 SOL 상한에 1,000,000 토큰의 경우, 가격은 토큰당 0.0001 SOL입니다.

SOL 상한에 도달하지 못하면 어떻게 되나요?

사용자는 예치금에 비례하여 토큰을 받습니다. 100 SOL 상한에 대해 50 SOL만 예치되면, 예치자는 할당된 토큰의 50%를 받습니다.

사용자별 예치 한도를 설정할 수 있나요?

네. 거래당 최소 한도에는 minimumDepositAmount를, 사용자당 최대 총 예치금에는 depositLimit을 사용합니다.

Presale과 Launch Pool의 차이점은 무엇인가요?

Presale은 토큰 할당량과 SOL 상한에 의해 결정되는 고정 가격입니다. Launch Pool은 총 예치금을 기반으로 자연스럽게 가격이 결정됩니다.

Presale과 Launch Pool은 언제 사용해야 하나요?

예측 가능한 가격 책정이 필요하고 모금 목표가 명확할 때 Presale을 사용합니다. 자연스러운 가격 발견에는 Launch Pool을 사용합니다.

용어집

용어정의
Presale미리 정해진 비율의 고정가 토큰 판매
SOL CapPresale이 수락하는 최대 SOL (가격 결정)
Token AllocationPresale에서 사용 가능한 토큰 수
Deposit Limit사용자당 허용되는 최대 총 예치금
Minimum Deposit예치 거래당 필요한 최소 금액
Cooldown사용자가 예치 사이에 기다려야 하는 시간
End Behavior예치 기간 종료 후 자동화된 동작
Transition종료 동작을 처리하는 명령

다음 단계