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