# Anchor Example: Escrow Program ### Notice: This document is deprecated. Please refer to the latest version of doc [here](https://hackmd.io/@ironaddicteddog/solana-anchor-escrow) or check the latest code base [here](https://github.com/ironaddicteddog/anchor-escrow) ## Overview Since this program is extended from the original [Escrow Program](https://github.com/paul-schaaf/solana-escrow), I assumed you have gone through the [original blog post](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#instruction-rs-part-1-general-code-structure-and-the-beginning-of-the-escrow-program-flow) 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 ![](https://i.imgur.com/VmRKZUy.png) `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 ![](https://i.imgur.com/f6ahGXy.png) `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 ![](https://i.imgur.com/MzG26dm.png) `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](https://docs.solana.com/cli/install-solana-cli-tools#build-from-source) for more details Next, we will build and deploy the program via Anchor. Install Anchor: ```bash $ 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 ``` <!-- > Since some features is not supported by the current stable release of Anchor, we will have to run the `anchor-cli` from the source directly. > Ex: > ``` > $ cargo run --manifest-path ../../cli/Cargo.toml build > ``` --> > Maker sure to terminate the `solana-test-validator` before you run the `test` command --- ## Develop the Escrow Program with Anchor ### Recommended Prerequisites Before we start, here are some materials that I strongly recommend all the readers to go through at least once. - [Solana Program: Hello World](https://github.com/solana-labs/example-helloworld) - [**Programming on Solana - An Introduction (by paulx)**](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/#instruction-rs-part-1-general-code-structure-and-the-beginning-of-the-escrow-program-flow): In this comprehensive tutorial, you will learn the basics of Solana program, such as... - Account Model - Architecuture of Program - Program Derived Address (PDA) - Cross-Program Invocation (CPI) - [Anchor Tutorials 0~4](https://project-serum.github.io/anchor/tutorials/tutorial-0.html): In these tutorials, you will learn the basic ideas behind Anchor <!-- - Design a program that can: - Own and Initialize an account that stores escrow state - Perform atomic exchange - Reset back to initial setting --> ### 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) ```rust= // 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) ```rust= // 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: ```rust= // 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**. ```rust= // 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 ```rust= // 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. ```rust= // 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. ```rust= // 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 ```rust= #[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](https://docs.rs/anchor-lang/0.13.2/anchor_lang/derive.Accounts.html) for more constraints. And that's it! We have walk through the escrow program essentially step by step. ## References - https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/ - https://project-serum.github.io/anchor/tutorials/tutorial-0.html - https://docs.rs/anchor-lang/0.18.0/anchor_lang/derive.Accounts.html - https://github.com/project-serum/anchor/pull/668 ###### tags: `solana` `anchor`