# Escrow Smart Contract ## Solana & Anchor 設置 務必看[這篇](https://lorisleiva.com/create-a-solana-dapp-from-scratch/getting-started-with-solana-and-anchor),已經寫得太好。 ### 主要操作 ([參考 repo & 圖片來源](https://github.com/ironaddicteddog/anchor-escrow?tab=readme-ov-file)) 1. initialize 2. cancel 3. exchange ### 其他參考網站 #### [Anchor Examples](https://examples.anchor-lang.com/docs/non-custodial-escrow) #### [架構圖來源](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#what-is-an-escrow) ![escrow](https://paulx.dev/assets/img/escrow_token_accounts_2.9291f5c8.png) ## 程式碼說明 ### src/states/escrow.ts ``` use anchor_lang::prelude::*; #[account] pub struct Escrow { pub seed: u64, pub bump: u8, pub initializer: Pubkey, pub mint_a: Pubkey, pub mint_b: Pubkey, pub initializer_amount: u64, pub taker_amount: u64, } impl Space for Escrow { // First 8 Bytes are Discriminator (u64) const INIT_SPACE: usize = 8 + 8 + 1 + 32 + 32 + 32 + 8 + 8; } ``` - Discriminator:每個 Account 最前面的 8 bytes 都會有,用來存 Account 種類。(不需要特別在 struct 裡面定義) ### 檔案:```src/lib.rs``` #### 模組設置 ```rust= use anchor_lang::prelude::*; // 引入 anchor_lang library 的 prelude module mod contexts; // 宣告 src/contexts/mod.rs module use contexts::*; // 引入 src/contexts/mod.rs module mod states; // 宣告 src/states/mod.rs module ``` - 模組後面有 ```::*``` 代表引入所有 public 的東西 - ```mod contexts;``` 可能代表 ```src/contexts/mod.rs``` 或 ```src/contexts.rs``` #### Program ID ```rust= declare_id!(<YOUR-PROGRAM_ID>); ``` - 在執行 ```anchor deploy``` 之後可以取得 id 並填入 #### Program Module ```rust= #[program] // 代表接下來的 module 是一個 program pub mod anchor_escrow { // 定義一個 public module 名為 anchor_escrow use super::*; // 引入所有 parent module 的 public module pub fn initialize( // 定義一個 function 名為 initialize 有四個參數 ctx: Context<Initialize>, // 參數 ctx 型別為 Context<Initialize> seed: u64, // 參數 seed 為 u64 (unsinged, 64 bits) initializer_amount: u64, taker_amount: u64, ) -> Result<()> { // 函式回傳 Result<()> (可能是 Error 或 Null) ctx.accounts .initialize_escrow(seed, &ctx.bumps, initializer_amount, taker_amount)?; ctx.accounts.deposit(initializer_amount) } pub fn cancel(ctx: Context<Cancel>) -> Result<()> { ctx.accounts.refund_and_close_vault() } pub fn exchange(ctx: Context<Exchange>) -> Result<()> { ctx.accounts.deposit()?; ctx.accounts.withdraw_and_close_vault() } } ``` - entrypoint!(process_instruction); ### 檔案:src/contexts/initialize.rs #### 模組設置 ```rust= use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{transfer_checked, Mint, Token, TokenAccount, TransferChecked}, }; // 使用 src/states/escrow.rs 定義的 Escrow 數據 use crate::states::Escrow; ``` #### 定義結構 ```rust= #[derive(Accounts)] #[instruction(seed: u64, initializer_amount: u64)] // 用於初始化的參數 pub struct Initialize<'info> { // 定義初始化結構 #[account(mut)] // mutable pub initializer: Signer<'info>, // Signer Type Account pub mint_a: Account<'info, Mint>, // Token A 的 Mint Account pub mint_b: Account<'info, Mint>, // Token B 的 Mint Account #[account( mut, constraint = initializer_ata_a.amount >= initializer_amount, // 限制:確認有足夠 token 進行初始化 associated_token::mint = mint_a, // 確保是 Token A 的 ATA associated_token::authority = initializer // 確保 Token 擁有者是 initializer )] pub initializer_ata_a: Account<'info, TokenAccount>, #[account( init_if_needed, payer = initializer, // 指定誰付 rent space = Escrow::INIT_SPACE, // 指定空間大小 seeds = [b"state".as_ref(), &seed.to_le_bytes()], //生成 PDA bump )] pub escrow: Account<'info, Escrow>, #[account( init_if_needed, payer = initializer, // 指定誰付 rent associated_token::mint = mint_a, // 確保是 Token A 的 ATA associated_token::authority = escrow // 確保擁有者是 Escrow )] pub vault: Account<'info, TokenAccount>, // Escorw 裡面存 Token A 的 account pub associated_token_program: Program<'info, AssociatedToken>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, } ``` #### 定義方法 ```rust= impl<'info> Initialize<'info> { // 設置 Escrow Account pub fn initialize_escrow( &mut self, seed: u64, bumps: &InitializeBumps, initializer_amount: u64, taker_amount: u64, ) -> Result<()> { // 設定結構的內部參數 self.escrow.set_inner(Escrow { seed, bump: bumps.escrow, initializer: self.initializer.key(), mint_a: self.mint_a.key(), mint_b: self.mint_b.key(), initializer_amount, taker_amount, }); Ok(()) } // deposit token into Escrow pub fn deposit(&mut self, initializer_amount: u64) -> Result<()> { transfer_checked( self.into_deposit_context(), initializer_amount, self.mint_a.decimals, ) } // deposit 所需要的 context // cpi: Cross-Program Invocation fn into_deposit_context(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { let cpi_accounts = TransferChecked { from: self.initializer_ata_a.to_account_info(), mint: self.mint_a.to_account_info(), to: self.vault.to_account_info(), authority: self.initializer.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } } ``` ### 檔案:src/contexts/cancel.rs #### 模組設置 ```rust= use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{ close_account, transfer_checked, CloseAccount, Mint, Token, TokenAccount, TransferChecked, }, }; use crate::states::Escrow; ``` #### 定義結構 ```rust= #[derive(Accounts)] pub struct Cancel<'info> { #[account(mut)] initializer: Signer<'info>, mint_a: Account<'info, Mint>, #[account( mut, associated_token::mint = mint_a, associated_token::authority = initializer )] initializer_ata_a: Account<'info, TokenAccount>, #[account( mut, has_one = initializer, // 確保 Escrow 和 Cancel 的 initializer 相同 has_one = mint_a, // 確保 Escrow 和 Cancel 的 Token A 是同種 close = initializer, // 確保關掉的人是 initializer seeds=[b"state", escrow.seed.to_le_bytes().as_ref()], // 生成 PDA bump = escrow.bump, )] escrow: Account<'info, Escrow>, #[account( mut, associated_token::mint = mint_a, associated_token::authority = escrow )] pub vault: Account<'info, TokenAccount>, associated_token_program: Program<'info, AssociatedToken>, token_program: Program<'info, Token>, system_program: Program<'info, System>, } ``` #### 定義方法 ```rust= impl<'info> Cancel<'info> { pub fn refund_and_close_vault(&mut self) -> Result<()> { let signer_seeds: [&[&[u8]]; 1] = [&[ // 生成 PDA Signer b"state", &self.escrow.seed.to_le_bytes()[..], &[self.escrow.bump], ]]; transfer_checked( self.into_refund_context().with_signer(&signer_seeds), self.escrow.initializer_amount, self.mint_a.decimals, )?; close_account(self.into_close_context().with_signer(&signer_seeds)) } // refund 所需要的 context fn into_refund_context(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { let cpi_accounts = TransferChecked { from: self.vault.to_account_info(), mint: self.mint_a.to_account_info(), to: self.initializer_ata_a.to_account_info(), authority: self.escrow.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } // close 所需要的 context fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { let cpi_accounts = CloseAccount { account: self.vault.to_account_info(), destination: self.initializer.to_account_info(), authority: self.escrow.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } } ``` ### 檔案:src/contexts/exchange.rs #### 模組設置 ```rust= use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{ close_account, transfer_checked, CloseAccount, Mint, Token, TokenAccount, TransferChecked, }, }; use crate::states::Escrow; ``` #### 定義結構 ```rust= #[derive(Accounts)] pub struct Exchange<'info> { #[account(mut)] pub taker: Signer<'info>, // 接收交易者為 Signer Type Account #[account(mut)] pub initializer: SystemAccount<'info>, // 初始 Exchange 的帳戶為 System Account pub mint_a: Box<Account<'info, Mint>>, // A Token Mint Account pub mint_b: Box<Account<'info, Mint>>, // B Token Mint Account #[account( init_if_needed, payer = taker, associated_token::mint = mint_a, associated_token::authority = taker )] pub taker_ata_a: Box<Account<'info, TokenAccount>>, // Taker 的 A Token ATA Account #[account( mut, associated_token::mint = mint_b, associated_token::authority = taker )] pub taker_ata_b: Box<Account<'info, TokenAccount>>, // Taker 的 B Token ATA Account #[account( init_if_needed, payer = taker, associated_token::mint = mint_b, associated_token::authority = initializer )] pub initializer_ata_b: Box<Account<'info, TokenAccount>>, // Maker 的 B Token ATA Account #[account( mut, has_one = mint_b, constraint = taker_ata_b.amount >= escrow.taker_amount, close = initializer, seeds=[b"state", escrow.seed.to_le_bytes().as_ref()], bump = escrow.bump, )] pub escrow: Box<Account<'info, Escrow>>, #[account( mut, associated_token::mint = mint_a, associated_token::authority = escrow )] pub vault: Box<Account<'info, TokenAccount>>, pub associated_token_program: Program<'info, AssociatedToken>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, } ``` #### 定義方法 ```rust= impl<'info> Exchange<'info> { // 將 taker 的 B Token 放入 initializer_ata_b pub fn deposit(&mut self) -> Result<()> { transfer_checked( self.into_deposit_context(), self.escrow.taker_amount, self.mint_b.decimals, ) } // 從 vault 中取出 initializer 的 Token A 並關閉 vault account pub fn withdraw_and_close_vault(&mut self) -> Result<()> { let signer_seeds: [&[&[u8]]; 1] = [&[ // 生成 PDA Signer b"state", &self.escrow.seed.to_le_bytes()[..], &[self.escrow.bump], ]]; transfer_checked( self.into_withdraw_context().with_signer(&signer_seeds), self.escrow.initializer_amount, self.mint_a.decimals, )?; close_account(self.into_close_context().with_signer(&signer_seeds)) } // deposit 所需要的 context (這邊是準備 Taker Token B) fn into_deposit_context(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { let cpi_accounts = TransferChecked { from: self.taker_ata_b.to_account_info(), mint: self.mint_b.to_account_info(), to: self.initializer_ata_b.to_account_info(), authority: self.taker.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } // withdraw 所需要的 context (這邊是準備 Taker Token A) fn into_withdraw_context(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> { let cpi_accounts = TransferChecked { from: self.vault.to_account_info(), mint: self.mint_a.to_account_info(), to: self.taker_ata_a.to_account_info(), authority: self.escrow.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } // close vault 所需要的 context fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { let cpi_accounts = CloseAccount { account: self.vault.to_account_info(), destination: self.initializer.to_account_info(), authority: self.escrow.to_account_info(), }; CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } } ``` ## Program Derived Addresses (PDAs) ![PDA](https://solana-developer-content.vercel.app/assets/docs/core/pda/pda.svg) - 透過 Optional seeds、Bump seed(nonce)和 Program ID 生成 PDA ![PDA2](https://solana-developer-content.vercel.app/assets/docs/core/pda/pda-derivation.svg) - Optional seeds - 自行定義,例如 ```const seed = new anchor.BN(randomBytes(8));```。 - Bump seed(1 byte) - 用於確認 PDA 不在 ed25519 橢圓曲線上的參數(不會有對應的 Private Key),不同 bump 會導致產生的 PDA 不同。 - Program ID - 允許程式當 Signer - 有點像是 Public Key,但沒有對應的 Private Key。 - PDA 不會自己創建 Account,需要有 Program 以及 Program ID。 ### FindProgramAddress function - 去找到一個 PDA 並且確保該地址是有效且唯一的 - 回傳包含 PDA 和 bump seed ### CreateProgramAddressCreateProgramAddress - 生成一個 PDA 但是不會確保唯一性 - 回傳只包含 PDA - 如果 PDA 無效會返回錯誤 ### 創建 Account 範例 - 生成 PDA 然後用他 sign 來創建一個 Account - 使用 init_if_needed (因為如果 init 過了再 init 一次會 fail) ``` #[account( init_if_needed, payer = initializer, // 指定誰付 rent space = Escrow::INIT_SPACE, // 指定空間大小 seeds = [b"state".as_ref(), &seed.to_le_bytes()], //生成 PDA bump )] pub escrow: Account<'info, Escrow> ``` ## Cross Program Invocation (CPI) 一種機制:一個 Program 去觸發其他 Program 的 Instructions,允許 Programs 之間互相組合。 ![program invoke](https://solana-developer-content.vercel.app/assets/docs/core/cpi/cpi.svg) ### A Program 對 B Program 發起 CPI - A 呼叫 B 時傳遞 signer 參數,因此 B 可以繼承 A 的 signer 資訊。 - B 可以再發起 CPI,呼叫其他 programs,深度最多可以到 4。 - B 可以使用該 PDA signer 資訊來簽名。 ### 寫 CPI 寫 CPI 就跟寫其他 instruction 有點類似 - Program Address:要呼叫的 program - Accounts:列出要讀寫的所有 accounts (包含其他 programs) - Instruction Data:要呼叫的 instruction 與要傳遞的資料 (類似函式的參數) #### Basic CPI 使用 invoke function ``` pub fn invoke( instruction: &Instruction, account_infos: &[AccountInfo<'_>] ) -> Result<(), ProgramError> ``` #### CPI with PDA Signer 使用 invoke_signed function - 多了一個 signers_seeds 可以讓 PDA 傳入 ``` pub fn invoke_signed( instruction: &Instruction, account_infos: &[AccountInfo<'_>], signers_seeds: &[&[&[u8]]] ) -> Result<(), ProgramError> ``` ### 程式解釋 - Escrow 的範例程式沒有使用 invoke 或 invoke_signed,但使用了同樣在 solana_program 裡面的 anchor_spl,是更高階的操作。 ``` use anchor_spl::{ associated_token::AssociatedToken, token::{transfer_checked, Mint, Token, TokenAccount, TransferChecked}, }; ``` ## Error 解法 - Error: Stack offset of 4584 exceeded max offset of 4096 by 488 bytes, please minimize large stack variables - 解法: https://github.com/solana-labs/solana/issues/35003 - Error: account data too small for instruction - 解法: https://stackoverflow.com/questions/71267943/solana-deploy-account-data-too-small-for-instruction