[KO] 솔라나에서의 NFT 구현 3편: 캔디머신 코드 분석하기

c0wjay
DSRV
Published in
32 min readJul 18, 2022
솔라나 캔디머신 프로그램 코드 리뷰

DSRV Dev Guild에서는 더 많은 개발자들과 Web3 인프라를 만들어가기 위해, 다양한 메인넷과 스마트 컨트랙트에 대한 가이드를 연재합니다.

Disclaimer: 이 글은 정보 전달을 위한 목적으로 작성되었으며, 특정 프로젝트에 대한 투자 권고, 법률적 자문 등 목적으로 하지 않습니다. 모든 투자의 책임은 개인에게 있으며, 이로 발생된 결과에 대해 어떤 부분에서도 DSRV는 책임을 지지 않습니다. 본문이 포괄하는 내용들은 특정 자산에 대한 투자를 추천하는 것이 아니며, 언제나 본문의 내용만을 통한 의사결정은 지양하시길 바랍니다.

[솔라나 NFT 시리즈]

  1. 솔라나 기본 개념 이해하기 — 컨트랙트와 스토리지의 구조
  2. 캔디머신을 활용한 NFT 민팅 과정 이해하기
  3. 캔디머신 코드 분석해보기

시작하기 전에..

지난 번 [솔라나 NFT 시리즈] 2편: NFT 민팅 과정의 이해에서는 메타플렉스의 캔디머신을 이용하여 솔라나에서 NFT를 민팅하는 과정을 추상적으로 다루었는데요. NFT를 민팅하기 위해 필요한 캔디머신 프로그램의 Instruciton을 InitializeCandyMachineMintNFT 였던 것 기억하시나요?

이번 시간에는 InitializeCandyMachineMintNFT Instruction을 핸들하는 메소드인 handle_initialize_candy_machinehandle_mint_nft 의 코드를 분석해보도록 하겠습니다.

예상 독자는 캔디머신의 동작 원리가 궁금하신 분이나, 솔라나에 배포되어 작동되는 실제 프로그램의 코드가 어떻게 되어있는지 궁금하신 분입니다. 또한, NFT를 Mint하는 Custom Program을 개발하고자 하는 사람에게 도움이 될 것입니다.

❗️ 본 글을 읽기 전에, 다음 자료를 통해 Rust와 Solana의 개념을 사전에 숙지하면 이해에 도움이 될 것입니다. 이번 글은 Candy Machine v4.0.0 버전을 중심으로 설명합니다.

1. 전체 과정 개요

1.1. Connection points with the client

Candy Machine Program의 트랜잭션 흐름도
[그림 3–1] Candy Machine Program의 트랜잭션 흐름도, 출처: Metaplex Docs

캔디머신을 이용하여 NFT를 민팅하는 과정에서, Client와 Program 간의 트랜잭션은 상기 [그림 3-1] 과 같이 진행됩니다. 2편에서 다루었던 내용이지만, 다시 한번 짚고 넘어갑시다.

  1. Arweave에 메타데이터를 업로드하기 위해, AR_HOLDER_ADDRESSSOL을 송금합니다.
  2. SPL_TOKEN_PROGRAM 을 호출하여 NFT Mint Account를 생성합니다.
  3. SPL_TOKEN_ASSOCIATED_PROGRAM을 호출하여 Associated Token Account를 생성합니다.
  4. METAPLEX_TOKEN_METADATA_PROGRAM 을 호출하여 NFT Mint Account에 연결된 Metadata Account 를 생성하고, 메타데이터를 업로드합니다.

1.2. Account Flows

Candy Machine의 Account 흐름도
[그림 3-2] Candy Machine의 Account 흐름도, 출처: @c0wjay
  • 참조 (mint.rs L77 ~ L89)
> Only needed if candy machine has a gatekeeper 
gateway_token
> Only needed if candy machine has a gatekeeper and it has expire_on_use set to true:
gateway program
network_expire_feature
> Only needed if candy machine has whitelist_mint_settings whitelist_token_account > Only needed if candy machine has whitelist_mint_settings and mode is BurnEveryTime
whitelist_token_mint
whitelist_burn_authority
> Only needed if candy machine has token mint
token_account_info
transfer_authority_info

MintNFT Instruction을 통해 위 순서대로 계정들이 전송됩니다. 이 때 CandyMachineData struct에 저장된 설정 값(config)에 따라 요구되는 remaining_account 들의 수가 달라집니다. remaining_account들의 기능은 아래 코드 분석에서 더 자세히 다루겠습니다.

💡 예를 들어, gatekeeper == false && whitelist_mint_settings == false && token_mint == true 라면, 마지막 2개의 remaining_account들만 전송됩니다. ctx.remaining_accounts 대한 내용은 다음 링크를 참고하시기 바랍니다.

1.3. Json schema

컨트랙트로 보내는 NFT.json 파일과 config.json 파일은 CandyMachineData struct에 저장됩니다.

다음은 프론트엔드에서 사용하는 JSON 파일입니다.

다음은 Contract 측면에서 받는 CandyMachineData Struct입니다.

1.4. Candy Machine 파일 계층 구조

캔디머신 프로그램은 다음의 디렉토리 구조를 가지는데요. 이번 글에서는 캔디머신 이해를 위해 필수적인 src/processor/initialize.rs, src/processor/mint.rs, src/state.rs 코드를 다루겠습니다.

src 
├── processor
│ ├── collection
│ │ ├── mod.rs
│ │ ├── remove_collection.rs
│ │ ├── set_collection_during_mint.rs
│ │ └── set_collection.rs
│ ├── add_config_lines.rs
│ ├── initialize.rs
│ ├── mint.rs
│ ├── mod.rs
│ ├── update.rs
│ └── withdraw.rs
├── constants.rs
├── errors.rs
├── lib.rs
├── state.rs
└── utils.rs

1.5. Candy Machine program (lib.rs)

다음 코드는 캔디머신 프로그램의 초입부인데, 인자값으로 받은 Instruction를 각 함수로 연결합니다. 참고로 이전 버전의 캔디머신에서는 모듈화가 잘 이루어지지 않았습니다. 캔디머신 프로그램 코드 안에 mint_nft, initialize_candy_machine, update_candy_machine 등의 함수를 한꺼번에 담았기 때문이죠. 하지만, 캔디머신 v4.0.0 코드 업데이트에서 리팩토링이 진행되어 아래와 같이 깔끔한 형태를 띄게 되었습니다.

본 글에서는 이 중에서 가장 핵심적인 initialize_candy_machinemint_nft 함수의 기능에 대해 분석해보겠습니다.

2. handle_initialize_candy_machine 함수 (initialize.rs) 분석

먼저 코드 분석을 진행하기 전에, 2편에서 소개드렸던 아래 [그림 3–3]를 다시 한 번 참고해봅시다. 우리가 이번에 다룰 handle_initialize_candy_machine 함수는 아래 [그림 3–3]를 실현시키는 과정입니다.

Candy Machine Program Account 간의 관계도
[그림 3-3] Candy Machine Program Account 간의 관계도, 출처: @c0wjay

2.1 InitializeCandyMachine instruction (initialize.rs L11 ~ L25)

Candy Machine PDA Account 구조도
[그림 3-4] Candy Machine PDA Account 구조도, 출처: @c0wjay

이번 Instruction은 캔디머신 PDA 계정을 Initialize하는 과정을 보여줍니다.

우선, #[instruction(..)] 선언이 눈에 띠는데 이게 무엇일까요? 위 attribute macro 아래의 struct (본 글에서 InitializeCandyMachine)는 일종의 프로그램이 받는 instruction을 서술하는 인터페이스라고 생각하시면 됩니다. #[instruction(..)] 밑의 struct에서 정의한 순서대로 instruction은 account들을 프로그램에 넘겨줘야 합니다. 또한 본 글에서의 (data: CandyMachineData)처럼, instruction을 통해 프로그램에 넘겨줄 account가 아닌 다른 data들의 경우엔 attribute macro 내에 정의 가능합니다.

그렇다면 #[instruction(..)]InitializeCandyMachine 에서 어떤 역할을 수행할까요? Instruction은 Program ID Index와 Account 주소 Index의 배열, 그리고 8비트의 데이터 배열로 이루어져 있는데요. 여기서 attribute macro는 Instruction을 deserialize하는 과정을 담당합니다. Instruction Struct는 아래와 같습니다.

여기서 program_id 는 여기서 캔디머신의 ID이고, 위 Instruction에 담겨진 정보 중 캔디머신이 사용할 정보는 accountsdata 입니다. Anchor에서는 프로그램이 Instruction에서 받은 인자값을 #[instruction (..)] attribute와 Context Struct를 활용하여 실행할 수 있도록 구현했습니다. Context Struct의 구체적인 사용 예시는 다음 Anchor Book의 내용을 참고해주세요.

이제 InitializeCandyMachine Struct에 대해 알아봅시다. InitializeCandyMachine Struct는 Context Struct와 함께 사용되어, Instruction으로 전달받은 Account 객체들을 deserialize합니다. 여기서는 #[account(..)] attribute로 감싸져 있는 candy_machine account를 볼 수 있네요.

💡 DSRV’s Tip: #[account(zero)] attribute가 무엇인가요?#[account(init, payer = <target_account>, space = <num_bytes>)] 다음 attribute는 System Program에 CPI (Cross Program Invocation) 과정을 거쳐 Account를 Initialize합니다. 이 때, Initialize 과정에서 Account Discriminator가 설정됩니다. Account 생성 과정에서 드는 비용은 payer가 부담하며, account가 사용하는 공간만큼 space Constraint에 지정해야 합니다. 여기서 Account Discriminator가 사용하는 공간이 8 byte이므로, 최소 8 byte 이상은 지정해야 합니다.#[account(zero)] 다음 Attribute는 Initialize하려는 Account의 크기가 10 Kibibyte 보다 클 때 init Attribute 대신 사용됩니다. Account의 크기가 10Kibibyte보다 크다면, CPI를 통해 account 생성이 불가합니다. 따라서 미리 이전 instruction으로부터 생성은 되었으나 비어있는 account를 받아서, 해당 account를 initialize합니다. 이 때 #[account(zero)] 의 역할은, 해당 Account의 Discriminator가 0인지 확인하여, Initialize되지 않은 Account가 맞는지 확인합니다.

정리하자면, #[account(zero)] attribute는 이전에 만들어졌으나 비어있는 candy_machine Account를 받습니다. 이 때 다음의 조건을 확인합니다.

  • 해당 Account가 Initialize 되지 않은 상태가 맞는지 확인한다.
  • 해당 Account의 Owner가 Candy Machine Program이 맞는지 확인한다.
  • 해당 Account에 할당된 Space가 Data를 담는데 충분한 지를 확인한다.
💡 Account Constraint에 대해 더욱 궁금하시다면, 다음 Anchor Docs의 내용을 참고하시기 바랍니다.

2.2. handle_initialize_candy_machine 함수 (initialize.rs L27 ~ L99)

Candy Machine Data Account 설계도
[그림 3–5] Candy Machine Data Account 설계도, 출처: @c0wjay

이번에는 handle_initialize_candy_machine 함수의 첫 라인을 함께 보도록 하겠습니다. 앞서 설명하였듯이,Context<InitializeCandyMachine> Struct로 deserialize된 Account들을 인자값으로 받고 Instruction에 담겨진 dataCandyMachineData Struct로 받습니다.

위 함수에서 인자값으로 주어지는 data 는 위의 Candy Machine Data struct인데 내부적으로 go_live_data, whitelist_mint_settings 등 환경설정 값이 담겨져 있습니다.

다음으로, InitializeCandyMachine context에서 인자값으로 받은 비어있는 Account를 candy_machine_account 객체에 할당합니다.

Candy Machine Program의 Wallet과 Authority 관계도
[그림 3–6] Candy Machine Program의 Wallet과 Authority 관계도, 출처: @c0wjay

아래 코드는 CandyMachine struct를 candy_machine 변수에 선언하고 있습니다.

CandyMachine Struct의 data에는 Instruction으로 받은 여러 환경설정 값이 포함된 CandyMachineData Struct를 저장합니다. authoritywallet에는 InitializeCandyMachine context를 통해 Instruction으로 부터 전달되는 값을 저장합니다. 위 코드는 아직 캔디머신 PDA 계정에 값을 넣기 전이기 때문에, 위 [그림 3–6]의 상태를 완전히 구현한 상태는 아닙니다.

💡 여기부터는 ctx.accounts.wallet, ctx.remaining_accounts 와 같은 Anchor 문법에 익숙하다는 것을 가정하겠습니다. 별도로 추가적인 설명은 하지 않고, ctx.accounts.wallet 와 같은 형태를 차용하겠습니다.

자, 이제 아래 [그림 3–7]처럼 캔디머신 PDA 계정에 Token Mint Account를 연결하는 코드를 다뤄보도록 하겠습니다.

2편에서 다뤘듯이, token_mint가 설정된다는 것은 특정 SPL 토큰으로 값을 치룰 수 있게 설정한다는 것을 의미합니다. 다시 말해서 값을 치루는 화폐가 될 SPL 토큰의 Mint Account를 연결하는 과정입니다.

Candy Machine PDA Account와 CW Token Mint Account 간의 관계
[그림 3–7] Candy Machine PDA Account와 CW Token Mint Account 간의 관계, 출처: @c0wjay

CandyMachine Struct에서 변수 token_mint를 설정하고자 하면, 컬렉션 소유자는 캔디머신 PDA 계정을 Initialize 하는 과정에서 remaining_accounts 로 SPL 토큰의 Mint Account를 전달합니다.

remaining_accounts 는 별도로 Deserialized 되거나 Validate 되지 않은 Account를 의미하는데요. 해당 변수는 Deserialize가 진행된 Account의 집합인 accounts 와는 다른 변수입니다. 제 경험상, 동적으로 변할 수 있는 Optional한 Account들을 remaining_accounts 변수를 활용하여 프로그램에 전달했습니다.

만약 remaining_accounts로 전달된 값이 있다면, token_mint_info 객체에 해당 값을 할당하고, 해당 Mint Account가 Initialize되어 있는지 확인합니다. 또한 ctx.accounts.wallet으로 전달받은 Account가 Initialize되어 있는지 확인합니다. 만약 Initialize가 된 Account라면 token_account 객체에 할당합니다.

다음으로, 위에서 사용한 Account의 유효성을 검사하기 위해 Account의 Owner를 확인합니다. 이 때 token_account 가 연결된 Mint Account가 token_mint_info 인지를 확인하는데, 만약 일치한다면 candy_machine 객체의 token_mint 변수에 token_mint_info 의 주소를 저장합니다.

이제 candy_machine 객체의 Initialize 과정이 완료되었습니다. 이제 candy_machine 객체의 값을 candy_machine_account 에 덮어씌우는 일만 남았습니다. 앞서 설명했듯이 candy_machine_account 는 캔디머신 PDA 계정이지만 내부적으로 비어있는 상태입니다.

아래 코드는 candy_machine 객체의 값을 new_data 라는 객체에 Result<Vec<u8>> 형태로 Serialize하여 옮겨 담고, 이를 다시 candy_machine_account.data에 옮기고 있습니다.

candy_machine_account 의 구조를 이해하기 위해서는AccountInfo Struct를 참고하시기 바랍니다.

이제 [그림 3–3]의 상태가 성공적으로 구현되었습니다!

3. handle_mint_nft 메소드 (mint.rs) 분석

NFT Token Mint와 Standard Edition, Master Edition Account 간의 관계 구조도
[그림 3–8] NFT Token Mint와 Standard Edition, Master Edition Account 간의 관계 구조도, 출처: @c0wjay

역시 코드 분석을 진행하기 전에 2편에서 언급하였던 도표인 [그림 3–8]를 참고하시기 바랍니다. 2편에서 언급했듯이 mint_nft 메소드가 실행되기 이전에 Token Mint AccountToken Account는 이미 Initialize가 완료되었습니다.

3.1. MintNFT instruction (mint.rs L35 ~ L90)

다음 코드에서는 MintNFT Struct를 통해 Instruction의 API를 선언하고 있습니다. 이번 시간에는 다양한 #[account(..)] Attribute 중에서 Instruction을 통해 전달받은 candy_machine_creator의 주소를 확인하는 과정을 짚어보겠습니다. 아래 #[account(.. 매크로는 해당 주소가 candy_machine이라는 String (PREFIX)으로 만든 byte array와 캔디머신 PDA 계정의 공개키로 만들어졌는 지 검증하는 attribute입니다.

클라이언트에서는 Buffercandy_machine이라는 String과, Candy Machine PDA Account의 공개 키, 그리고 Candy Machine Program ID를 사용하여 ed25519 타원곡선 위의 PDA를 찾습니다. 이후, 이렇게 찾은 PDA와 생성된 bump를 각각 candy_machine_creator, creator_bump란 필드로 프로그램에 전달합니다.

3.2. 객체 선언 (L96 ~ L108)

다음 코드에서는 ctx로 받은 Account를 객체에 할당하고 있습니다.

3.3. Validation (L109 ~ L192)

다음 코드는 객체로 받은 Account를 검증하는 과정인데, handle_mint_nft 중간의 코드와, punish_bots 함수를 설명하겠습니다. CPI를 이용해 Candy Machine을 호출할 수 있는 Program을 제한하는 코드는 Candy Machine의 이전 버전에는 존재하지 않았습니다. 2022년 5월 2일에 민팅봇들에 의한 DDoS (6m TPS) 공격 이후, 재발을 방지하기 위해 추가된 코드로 보여집니다.

첫 번째 Instruction을 보낸 Program이 Candy Machine 및 Gumdrop이 아니면, 다음 메세지를 반환합니다.

”Suspicious transaction detected, Candy Machine Botting is taxed at 10000000 lamports”

이제 Bot으로 의심되는 Payer 계정에서 캔디머신 PDA 계정으로 10000000 lamports 만큼 Transfer합니다. 이 때, 두 번째 Instruction이 get_instruction_relative의 결과로 Ok(_) 를 return하는 지 확인하는데, 다음 세 가지 조건 중 단 하나라도 만족한다면 Unauthorized Instruction으로 취급되어 에러를 반환합니다.

1) 2번째 instruction을 candy machine이 아닌 다른 프로그램이 보냈는가?

2) 2번째 instruction의 식별자인 == ix.data[0..8][103, 17, 200, 25, 118, 95, 125, 61] 와 다른가?

3) 3번째 instruction이 Ok(_) 를 반환하는가?

만약 처음부터 get_instruction_relative의 결과로 Err(_) 를 반환하면 Bot으로 간주하고 처벌 절차를 밟습니다. 다음 코드에서 instruction_sysvar 는 Instructions의 개수만큼 순회하면서 program_id 를 읽어옵니다. 각각의 program_id 가 다음 조건을 만족하지 못한다면 Bot으로 간주하고, Bot을 처벌하기 위한 목적으로 payer로부터 BOT_FEE(1,000,000 lamports) 를 가져옵니다.

조건 1. Candy Machine Program의 ID인가?

조건 2. SPL Token Program의 ID인가?

조건 3. System Program의 ID인가?

조건 4. Associated Token Program의 ID인가?

instruction_sysvar 의 내부 구조는 다음과 같이 구성되어 있습니다.

instruciton_sysvar의 unsigned integer 8byte로 구성된 배열의 내부 구조
[그림 3–9] instruciton_sysvar의 unsigned integer 8byte로 구성된 배열의 내부 구조, 출처: @c0wjay

3.4. EndSetting 확인 (L195 ~ L227)

EndSetting은 Candy Machine NFT를 Mint할 때 마감 시간을 넘기지는 않았는지, 최대 민팅 가능 개수를 초과하지는 않았는지 확인합니다. 다음은 config.json 에서 endSettings 가 설정된 경우에 대한 코드입니다.

먼저, EndSettingTypeDate 로 설정되어 있는 경우입니다. 현재 유닉스 시간이 EndSetting으로 설정된 숫자보다 크고, Payer에게 Authority가 없을 경우에는 CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

EndSettingType이 Amount 로 설정되어 있다면, 현재 Redeem된 NFT의 수가 EndSetting으로 설정된 숫자보다 크고, Payer에게 Authority가 없을 경우에는 CandyMachineEmpty 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

3.5. Gatekeeper (L228 ~ L292)

다음은 config.json 에서 Gatekeeper 가 설정된 경우입니다. 이는 NFT 민팅 과정에서 GatekeeperToken 요구하겠다고 선언한 것인데, 민팅하는 사람의 신원 확인을 요구하는 것입니다. 예를 들어, Bot이 NFT를 반복적으로 Mint하는 사태를 방지하기 위해 Mint 과정에서 Captcha를 요구하는 것이라고 생각하시면 됩니다. 자세한 사항은 Solana Gateway 구조를 참고해주세요.

Solana Gateway의 구조
[그림 3-10] Solana Gateway의 구조, 출처: identity-com gateway 백서

먼저, remaining_accounts 의 수가 0 이하라면, GatewayToken이 전송되지 않았다는 뜻입니다. 이 때, GatewayTokenMissing 에러를 반환하고 Payer를 Bot으로 인식하여 BOT_FEE 를 부과합니다.

만약 remaining_accounts 의 수가 0보다 크다면, 전송된 remaining_accounts 정보를 gateway_token_info에 할당합니다.

다음 코드는 Gateway CPI에 사용될 eval_function 을 정의하는 코드입니다. Gateway Token의 생성이 민팅 시작일보다 이르다는 것을 의미하기 때문에 해당 토큰이 유효하지 않음을 알려주는 함수입니다. 이를 Pre-Solving Captcha라고 부릅니다. Rust의 Closure 문법에 대해 익숙하지 않은 사람은 다소 난해할 수 있습니다.

  1. 캔디머신 PDA 계정의 CandyMachineData struct에 저장된 go_live_date, whitelist_mint_settingspresale 값들을 가져옵니다.
  2. Gateway Token에서는 expire_time 값을 가져와서, presale도 지정되어있지 않고, gateway token이 생성된 시간이 go_live_date값 보다 작은지 확인합니다 (expire_time - EXPIRE_OFFSET)

위에서 정의한 eval_function 메소드를 CPI를 통해 Gateway 프로그램에 전달되면, 해당 프로그램 내에서 처리됩니다. 다음 코드는 위에서 정의한 eval_functiongateway_token_info, gatekeeper의 키 및 여러 인자를 CPI를 통해 Gateway 프로그램에 전달하여 게이트웨이 토큰의 진위성을 인증하는 과정입니다.

여기서 gatekeeperexpire_on_use == true 라면 NFT 민팅 후 게이트웨이 토큰이 만료된다는 뜻입니다. 이 때는 remaining_accountsgateway_appnetwork_expire_feature 를 받아 함께 전달합니다.

❗️ 이번 글에서 언급한 Gateway 코드는 Candy Machine v3.2.5 까지는 Candy Machine Program 내부에서 많은 연산이 진행되었습니다. 그러나 v4.0.0 이후 대부분의 연산이 CPI를 통해 Gateway Program으로 비즈니스 로직이 이전되었습니다. CPI 이후의 자세한 과정이 궁금하신 분은 다음 링크를 참고하시기 바랍니다.

3.6. Whitelist Mint Settings (L294 ~ L444)

Whitelist Mint Mode는 크게 burnEveryTimeneverBurn 로 나뉘는데요. 먼저, Whitelist Mint가 설정된 경우에는 Whitelist Token을 가진 사람만 Whitelist 기간에 Mint가 가능합니다.

만약 burnEveryTime 모드라면, 매번 Mint할 때마다 Whitelist Token이 1개씩 줄어듭니다. 즉, Whitelist Token을 1개만 갖고 있는 사람은 단 1회만 Mint가 가능한 것이죠. 이와 달리 neverBurn 모드라면, 매번 Mint할 때마다 Whitelist Token이 줄지 않습니다. 즉, Whitelist Token을 1개만 소유한 사람이더라도 여러 번 Mint할 수 있습니다.

먼저 Whitelist Mint가 설정된 코드를 보겠습니다. 이 때 ctx.remaining_accounts로 받은 account를 whitelist_token_account에 할당합니다.

assert_is_ata 함수는 src/utils.rs에 있는 코드로 아래와 같습니다.

다음 함수는 인자값으로 ATA (Associated Token Address) 의 AccountInfoWallet의 공개 키, Mint Account의 공개 키를 받습니다. 이 때, 해당 ATA가 Mint Account에 해당되는 ATA이고, Wallet이 ATA의 소유주가 맞는지 유효성을 검증합니다. 해당 ATA의 주인이 생성하기 전에 미리 deterministic 하게 결정되는 ATA 주소의 특성상 해당 주소를 검증하기 위함입니다. 자세한 사항은 1편의 ATA 문단을 참고하시기 바랍니다.

이제 assert_is_ata(whitelist_token_account, &payer.key(), &ws.mint) == Ok(wta) 인 경우에 대해 먼저 다뤄보겠습니다. mint.rs 파일의 L302 ~ L400을 참조하시기 바랍니다.

만약 Minter가 화이트리스트 토큰을 가지고 있다면 (if wta.amount > 0), go_live_date가 설정되어 있는지 확인합니다. 만약 go_live_date 가 미설정되어 있을 때 Payer에게 authority가 없고, presale이 설정되어 있지 않다면, CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

Minter가 화이트리스트 토큰을 가지고 있을 때 go_live_date 변수가 설정되어 있을 때는 어떨까요? 현재 유닉스 시간이 go_live_date 보다 이르고, Payer에게 Authority가 없고 presale이 설정되어 있지 않다면, CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

이제 WhitelistMintModeBurnEveryTime 으로 설정되어 있다면, remaining_accounts 들을 각각의 객체에 할당합니다. CandyMachinewhitelist_mint_settings에 저장된 Whitelist Token의 MintAaccount의 주소와 remaining_accounts로 전달받은 whitelist_token_mint의 주소가 다르다면, CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

마지막으로 Whitelist Token 1개를 소각합니다.

whitelist_mint_settingsdiscount_price 가격이 설정되어 있으면, 해당 가격으로 판매합니다.

만약 Minter가 화이트리스트 토큰을 갖고 있지 않다면 어떻게 될까요? 가격 할인도 미설정이고, presale 기간도 아니라면 NoWhitelistToken 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

다음으로, 현재 Mint하는 시간이 정상적인 Live Date인지 확인합니다. 만약 현재 시간이 설정한 Mint 시간 범위가 아니라면 CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

WhitelistMintModeBurnEveryTime 라면, remaining_accounts_counter를 2만큼 증가시킵니다. Minter가 Whitelist Token을 갖고 있지 않기 때문에, whitelist_token_mintwhitelist_burn_authority 가 의미가 없으므로 넘어갑니다.

Whitelist Token Account에 문제가 있는 경우는 L401 ~ L429에 서술되어 있습니다. 이 때는 Minter가 Whitelist Token을 갖고 있지 않은 경우와 동일하게 검증이 진행됩니다.

마지막으로, Whitelist Mint 설정이 되지 않은 경우입니다. 이는 L431 ~ L444에 서술되어 있습니다. 먼저 정상적인 Live Date인지 확인합니다. 만약 현재 시간이 설정한 Mint 시간 범위가 아니라면, CandyMachineNotLive 에러를 반환하고, Bot으로 인식하여 BOT_FEE 를 부과합니다.

3.7. Token mint 설정 (L457 ~ L491)

token_mint가 설정되지 않았다면, NFT Mint 비용을 SOL로 받습니다. token_mint가 설정되었다면, NFT Mint 비용을 설정된 SPL Token으로 받습니다. token_mint 는 Candy Machine PDA Account의 Initialize 또는 Update 과정 중 전달 받은 remaining_accounts 에 의해 값이 설정됩니다.

만약 token_mint에 값이 설정되어 있다면 remaining_accounts 를 각각 token_account_info, transfer_authority_info 에 할당합니다. 이후, token_account_info를 검증하고 token_account로 unpack합니다.

이 때 token_account 의 잔액이 사전에 지정한 Mint 가격보다 낮다면, NotEnoughTokens 에러를 반환합니다. 만약 token_account 의 잔액이 사전에 지정한 Mint 가격보다 같거나 높다면, 해당 token_account에서 Candy Machine PDA 계정에 저장된 Wallet으로 사전에 지정한 Mint 가격만큼의 SPL Token을 전송합니다.

만약 token_mint가 None이라면, Minter 지갑의 남은 잔액이 사전에 지정한 Mint 가격보다 적을 때 NotEnoughSOL 에러를 반환합니다. 만약 Minter 지갑의 남은 잔액이 사전에 지정한 Mint 가격보다 크다면, Minter의 지갑에서 Candy Machine PDA Account의 Wallet으로 사전에 지정한 Mint 가격만큼의 SOL을 송금합니다.

3.9. 메타데이터 생성 (L508 ~ L611)

먼저, CPI를 진행하기 위하여 캔디머신 PDA 계정의 주소와 authority_seeds를 준비합니다. 여기서 authority_seedsCandyMachineCreator에 대한 seed입니다.

다음은 candy_machine.data.creatorsVec 형식의 creators 를 저장하는 과정입니다. 여기에 저장되는 정보는 다음과 같이 Creator Address에 포함됩니다.

[그림 3–11] DSRV Dev Guild Example을 Metaplex NFT로 발행한 모습

이제 CPI를 통해 메타데이터 정보들을 담아 Metadata Account를 생성합니다.

마지막으로, CPI를 통해 메타데이터 정보들을 담아 Master Edition Account를 생성합니다.

4. ErrorCode (errors.rs)

글을 마무리하며..

본 글에서는 캔디머신의 코드를 Line by Line으로 분석해봤습니다.

솔라나 해커톤을 준비할 때 처음 캔디머신 코드(v2.0.1)를 분석하였고, 당시에 참고하였던 자료들은 캔디머신 v1 기준으로 작성되었습니다. 그리고 약 두 달이 지난 상태에서 본 글을 쓰기 위해 재분석을 할 때에는 v3.2.5 버전의 코드를 다시 분석하였습니다. 마지막으로 본 글을 리뷰하는 동안 캔디머신 v4.0.0 버전 업데이트가 진행되어서, v4.0.0 기준으로 다시 글을 썼습니다.

이 과정에서 의도치않게 v1부터 v4까지 캔디머신 코드의 업데이트 과정을 직/간접적으로 보게 되었습니다. 처음 코드를 볼 때에는 코드 가독성 등의 면에서 여러 아쉬운 점이 있었는데, 버전 업데이트 과정(특히 v4.0.0)에서 코드 리팩토링이 많이 진행되어 많이 개선된 것이 느껴집니다. 처음 코드를 볼 때에는 주석조차 제대로 달려있질 않아서, 이 코드가 뭐하는 친구인지 감을 잡는데만 꽤 오래 걸렸던 기억이 남네요.

캔디머신을 쓰는 것이 성능면에서 가장 최선의 선택은 아닙니다. 대중성을 위해 여러 기능을 넣다 보니 무거워졌고, 최근엔 캔디머신 프로그램이 민팅 봇에 의한 DDoS 공격을 당해, 이를 막기위한 로직을 추가하다 보니 더 무거워졌죠.

다만 Rust를 잘 모르는 프로젝트 팀들이 보다 쉽게 솔라나에서 NFT를 발행할 수 있다는 점에서 훌륭한 프로그램이라고 생각합니다. 캔디머신 덕분에 많은 NFT 팀들은 솔라나의 코어나 Anchor에 대해 공부하지 않아도 NFT를 발행할 수 있게 되었죠. 이 생태계가 더 발전하기를 바라며, 이만 글을 마치도록 하겠습니다. 솔라나 NFT 시리즈를 끝까지 읽어주신 독자분들께 감사드리고, 다음에는 또 다른 흥미로운 주제로 찾아오겠습니다.

Author
c0wjay | Jay Soh (Twitter @c0wjay)

Reviewed by
Sigrid Jin of DSRV, Technical Writer & Developer Evangelist (Twitter @sigridjin_eth)

Reference
[1] Candy Machine Github Repository

--

--

c0wjay
DSRV
Writer for

Rustacean interested in Programming Languages.