# 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)

## 程式碼說明
### 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)

- 透過 Optional seeds、Bump seed(nonce)和 Program ID 生成 PDA

- 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 之間互相組合。

### 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