Try   HackMD

Anchor Example: Escrow Program

Notice: This document is deprecated. Please refer to the latest version of doc here or check the latest code base here

Overview

Since this program is extended from the original Escrow Program, I assumed you have gone through the original blog post at least once.

However, there is one major difference between this exmaple and the original Escrow program: Instead of letting initializer create a token account to be reset to a PDA authority, we create a token account Vault that has both a PDA key and a PDA authority.

Initialize

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Initializer can send a transaction to the escrow program to initialize the Vault. In this transaction, two new accounts: Vault and EscrowAccount, will be created and tokens (Token A) to be exchanged will be transfered from Initializer to Vault.

Cancel

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Initializer can also send a transaction to the escrow program to cancel the demand of escrow. The tokens will be transfered back to the Initialzer and both Vault and EscrowAccount will be closed in this case.

Exchange

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Taker can send a transaction to the escrow to exchange Token B for Token A. First, tokens (Token B) will be transfered from Taker to Initializer. Afterward, the tokens (Token A) kept in the Vault will be transfered to Taker. Finally, both Vault and EscrowAccount will be closed.

Build, Deploy and Test

Let's run the test once to see what happens.

First, install dependencies:

$ npm install

Make sure you have your local solana validator running if you want to deploy the program locally:

$ solana-test-validator

If you are on Apple Sillicon M1 chip, you will have to build Solana from the source. See this document for more details

Next, we will build and deploy the program via Anchor.

Install Anchor:

$ cargo install --git https://github.com/project-serum/anchor --tag v0.18.0 anchor-cli --locked

First, let's build the program:

$ anchor build

Deploy the program:

$ anchor deploy

Finally, run the test:

$ anchor test

Maker sure to terminate the solana-test-validator before you run the test command


Develop the Escrow Program with Anchor

Before we start, here are some materials that I strongly recommend all the readers to go through at least once.

What can Anchor do for you?

  • Make Solana program more intuitive to understand
  • More clear buisness Logic
  • Remove a ton of biolderplate code
  • 3 Main parts: Processor => Instruction => Account
    • Main buisiness logic locates in processor
    • Instruction data packing/unpacking and account constraints and access control locate in Instruction handling part
    • Declaration of account owned by program locates in account part

Processor (Part 1)

// Processor (unimplemented) #[program] pub mod escrow { use super::*; pub fn initialize_escrow( ctx: Context<InitializeEscrow>, initializer_amount: u64, taker_amount: u64, ) -> ProgramResult { // TODO Ok(()) } pub fn cancel_escrow(ctx: Context<CancelEscrow>) -> ProgramResult { // TODO Ok(()) } pub fn exchange(ctx: Context<Exchange>) -> ProgramResult { // TODO Ok(()) } }
  • Corresponding 3 tasks listed above
  • In ctx argument, notice that we have to use a type InitializeEscrow for Context<T> generic. InitializeEscrow can be considered as a wrapper for instructions. This wrapper is enhanced by Anchor via derived macro (#[deive(account)])
  • Each function has a corresponding instruction. As a result, there will be 3 instruction wappers.

Instructions (Part 1)

// Instructions (unimplemented) #[derive(Accounts)] pub struct InitializeEscrow<'info> { // TODO } #[derive(Accounts)] pub struct Exchange<'info> { // TODO } #[derive(Accounts)] pub struct CancelEscrow<'info> { // TODO }
  • Depending on the program functions, the instructions should bring in the accounts that are needed for operations
  • To see what are accounts needed for initializing escrow account, we have to consider what data stored in escrow account first.

Program-owned Account

  • What state should be managed?
Field Description
initializer_key To authorize the actions properly
initializer_amount To record how much token should the initializer transfer to taker
taker_amount To record how much token should the initializer receive from the taker
initializer_deposit_token_account To record the deposit account of initialzer
initializer_receive_token_account To record the receiving account of initializer
  • As a result, we can have a account like this:
// ProgramAccount (fully implemented) #[account] pub struct EscrowAccount { pub initializer_key: Pubkey, pub initializer_deposit_token_account: Pubkey, pub initializer_receive_token_account: Pubkey, pub initializer_amount: u64, pub taker_amount: u64, }

Instructions (Part 2)

  • According to what we have in EscrowAccount, we need the following accounts to initialize it.
  • Here are the accounts needed for InitialEscrow function:
Field Description
initializer Signer of InitialEscrow instruction. To be stored in EscrowAccount
initializer_deposit_token_account The account of token account for token exchange. To be stored in EscrowAccount
initializer_receive_token_account The account of token account for token exchange. To be stored in EscrowAccount
token_program The account of TokenProgram
escrow_account The account of EscrowAccount
vault_account The account of Vault, which is created by Anchor via constraints. (Will be explained in part 3)
mint -
system_program -
rent -
  • Here are the accounts needed for CancelEscrow instruction:
Field Description
initializer The initializer of EscrowAccount
initializer_deposit_token_account The address of token account for token exchange
vault_account The program derived address
vault_authority The program derived address
escrow_account The address of EscrowAccount. Have to check if the EscrowAccount follows certain constraints.
token_program The address of TokenProgram
  • Similarily, here are the accounts needed for Exchange instruction:
Field Description
taker Singer of Exchange instruction
taker_deposit_token_account Token account for token exchange
taker_receive_token_account Token account for token exchange
initializer_deposit_token_account Token account for token exchange
initializer_receive_token_account Token account for token exchange
initializer_main_account To be used in constraints. (Will explain in part 3)
escrow_account The address of EscrowAccount. Have to check if the EscrowAccount follows certain constraints.
vault_account The program derived address
vault_authority The program derived address
token_program The address of TokenProgram
  • You can see that it is a very long list of inputs since Solana programs are stateless.
// Instructions (partially implemented) #[derive(Accounts)] #[instruction(initializer_amount: u64)] pub struct InitializeEscrow<'info> { pub initializer: AccountInfo<'info>, pub mint: Account<'info, Mint>, pub vault_account: Account<'info, TokenAccount>, pub initializer_deposit_token_account: Account<'info, TokenAccount>, pub initializer_receive_token_account: Account<'info, TokenAccount>, pub escrow_account: ProgramAccount<'info, EscrowAccount>, pub system_program: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct CancelEscrow<'info> { pub initializer: AccountInfo<'info>, pub initializer_deposit_token_account: Account<'info, TokenAccount>, pub vault_account: Account<'info, TokenAccount>, pub vault_authoitiy: AccountInfo<'info>, pub escrow_account: ProgramAccount<'info, EscrowAccount>, pub token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct Exchange<'info> { pub taker: AccountInfo<'info>, pub taker_deposit_token_account: Account<'info, TokenAccount>, pub taker_receive_token_account: Account<'info, TokenAccount>, pub initializer_deposit_token_account: Account<'info, TokenAccount>, pub initializer_receive_token_account: Account<'info, TokenAccount>, pub initializer_main_account: AccountInfo<'info>, pub escrow_account: ProgramAccount<'info, EscrowAccount>, pub vault_account: Account<'info, TokenAccount>, pub vault_authority: AccountInfo<'info>, pub token_program: AccountInfo<'info>, }
  • ProgramAccount vs AccountInfo vs Account
  • We will circle back to instructions later.

Processor (Part 2)

  • With necessary accounts, we can implement the business logic inside processor
// Processor (fully implenmented) #[program] pub mod escrow { use super::*; const ESCROW_PDA_SEED: &[u8] = b"escrow"; pub fn initialize_escrow( ctx: Context<InitializeEscrow>, _vault_account_bump: u8, initializer_amount: u64, taker_amount: u64, ) -> ProgramResult { ctx.accounts.escrow_account.initializer_key = *ctx.accounts.initializer.key; ctx.accounts .escrow_account .initializer_deposit_token_account = *ctx .accounts .initializer_deposit_token_account .to_account_info() .key; ctx.accounts .escrow_account .initializer_receive_token_account = *ctx .accounts .initializer_receive_token_account .to_account_info() .key; ctx.accounts.escrow_account.initializer_amount = initializer_amount; ctx.accounts.escrow_account.taker_amount = taker_amount; let (vault_authority, _vault_authority_bump) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id); token::set_authority( ctx.accounts.into_set_authority_context(), AuthorityType::AccountOwner, Some(vault_authority), )?; token::transfer( ctx.accounts.into_transfer_to_pda_context(), ctx.accounts.escrow_account.initializer_amount, )?; Ok(()) } pub fn cancel_escrow(ctx: Context<CancelEscrow>) -> ProgramResult { let (_vault_authority, vault_authority_bump) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id); let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]]; token::transfer( ctx.accounts .into_transfer_to_initializer_context() .with_signer(&[&authority_seeds[..]]), ctx.accounts.escrow_account.initializer_amount, )?; token::close_account( ctx.accounts .into_close_context() .with_signer(&[&authority_seeds[..]]), )?; Ok(()) } pub fn exchange(ctx: Context<Exchange>) -> ProgramResult { // Transferring from initializer to taker let (_vault_authority, vault_authority_bump) = Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id); let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]]; token::transfer( ctx.accounts.into_transfer_to_initializer_context(), ctx.accounts.escrow_account.taker_amount, )?; token::transfer( ctx.accounts .into_transfer_to_taker_context() .with_signer(&[&authority_seeds[..]]), ctx.accounts.escrow_account.initializer_amount, )?; token::close_account( ctx.accounts .into_close_context() .with_signer(&[&authority_seeds[..]]), )?; Ok(()) } }
  • Now, without the boilerplate code doing checking and data packing/unpacking, the business logic is simple and clear to understand.
  • In initialize_escrow, what happens is that the input accouns are assigned to EscrowAccount fields one by one.
  • Then, a program derived address, or PDA, is derived to be going to become new authority of initializer_deposit_token_account.
  • In cancel_escrow, it just simply reset the authority from PDA back to the initializer.
  • In exchange, 3 things happen: First, token A gets tranfered from pda_deposit_token_account to taker_receive_token_account. Next, token B gets transfered from taker_deposit_token_account to initializer_receive_token_account. Finally, the authority of pda_deposit_token_account gets set back to the initializer.

Utils

  • There are some util functions used for wrapping the data to be passed in tokens::transfer, token::close_account and token::set_authority
  • It might look a bit overwhelmed in the first place. However, the purpose behind these functions are clear and simple.
// Utils (fully implemented) impl<'info> InitializeEscrow<'info> { fn into_transfer_to_pda_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: self .initializer_deposit_token_account .to_account_info() .clone(), to: self.vault_account.to_account_info().clone(), authority: self.initializer.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } fn into_set_authority_context(&self) -> CpiContext<'_, '_, '_, 'info, SetAuthority<'info>> { let cpi_accounts = SetAuthority { account_or_mint: self.vault_account.to_account_info().clone(), current_authority: self.initializer.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } } impl<'info> CancelEscrow<'info> { fn into_transfer_to_initializer_context( &self, ) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: self.vault_account.to_account_info().clone(), to: self .initializer_deposit_token_account .to_account_info() .clone(), authority: self.vault_authority.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { let cpi_accounts = CloseAccount { account: self.vault_account.to_account_info().clone(), dest: self.initializer.clone(), authority: self.vault_authority.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } } impl<'info> Exchange<'info> { fn into_transfer_to_initializer_context( &self, ) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: self.taker_deposit_token_account.to_account_info().clone(), to: self .initializer_receive_token_account .to_account_info() .clone(), authority: self.taker.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } fn into_transfer_to_taker_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { let cpi_accounts = Transfer { from: self.vault_account.to_account_info().clone(), to: self.taker_receive_token_account.to_account_info().clone(), authority: self.vault_authority.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> { let cpi_accounts = CloseAccount { account: self.vault_account.to_account_info().clone(), dest: self.initializer.clone(), authority: self.vault_authority.clone(), }; CpiContext::new(self.token_program.clone(), cpi_accounts) } }

Instructions (Part 3)

  • Finally, let's talk about the account constraints.
  • Here comes a very handy funcionality that Anchor provides: Account Constraints.
  • Constraints are useful for basic checkings such as whether the initializer is the signer of instruction.
  • If you are familiar of Solidity, you can map this concept to solidity modifier.
// Instructions (fully implementated) #[derive(Accounts)] #[instruction(vault_account_bump: u8, initializer_amount: u64)] pub struct InitializeEscrow<'info> { #[account(mut, signer)] pub initializer: AccountInfo<'info>, pub mint: Account<'info, Mint>, #[account( init, seeds = [b"token-seed".as_ref()], bump = vault_account_bump, payer = initializer, token::mint = mint, token::authority = initializer, )] pub vault_account: Account<'info, TokenAccount>, #[account( mut, constraint = initializer_deposit_token_account.amount >= initializer_amount )] pub initializer_deposit_token_account: Account<'info, TokenAccount>, pub initializer_receive_token_account: Account<'info, TokenAccount>, #[account(zero)] pub escrow_account: ProgramAccount<'info, EscrowAccount>, pub system_program: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct CancelEscrow<'info> { #[account(mut, signer)] pub initializer: AccountInfo<'info>, #[account(mut)] pub vault_account: Account<'info, TokenAccount>, pub vault_authority: AccountInfo<'info>, #[account(mut)] pub initializer_deposit_token_account: Account<'info, TokenAccount>, #[account( mut, constraint = escrow_account.initializer_key == *initializer.key, constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key, close = initializer )] pub escrow_account: ProgramAccount<'info, EscrowAccount>, pub token_program: AccountInfo<'info>, } #[derive(Accounts)] pub struct Exchange<'info> { #[account(signer)] pub taker: AccountInfo<'info>, #[account(mut)] pub taker_deposit_token_account: Account<'info, TokenAccount>, #[account(mut)] pub taker_receive_token_account: Account<'info, TokenAccount>, #[account(mut)] pub initializer_deposit_token_account: Account<'info, TokenAccount>, #[account(mut)] pub initializer_receive_token_account: Account<'info, TokenAccount>, #[account(mut)] pub initializer: AccountInfo<'info>, #[account( mut, constraint = escrow_account.taker_amount <= taker_deposit_token_account.amount, constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key, constraint = escrow_account.initializer_receive_token_account == *initializer_receive_token_account.to_account_info().key, constraint = escrow_account.initializer_key == *initializer.key, close = initializer )] pub escrow_account: ProgramAccount<'info, EscrowAccount>, #[account(mut)] pub vault_account: Account<'info, TokenAccount>, pub vault_authority: AccountInfo<'info>, pub token_program: AccountInfo<'info>, }

Create an token account that has a PDA key

#[derive(Accounts)] #[instruction(token_bump: u8)] pub struct TestTokenSeedsInit<'info> { #[account( init, seeds = [b"my-token-seed".as_ref()], bump = token_bump, payer = authority, token::mint = mint, token::authority = authority, )] pub my_pda: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, pub authority: AccountInfo<'info>, pub system_program: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub token_program: AccountInfo<'info>, }

Here, we can see a few new attributes, such as:

Attribute Description
#[account(signer)] Checks the given account signed the transaction
#[account(mut)] Marks the account as mutable and persists the state transition
#[account(constraint = <expression\>)] Executes the given code as a constraint. The expression should evaluate to a boolean
#[account(close = <target\>)] Marks the account as being closed at the end of the instruction’s execution, sending the rent exemption lamports to the specified

Check the official document for more constraints.

And that's it! We have walk through the escrow program essentially step by step.

References

tags: solana anchor