# Escrow - An introduction to DeFi in Solana :rocket:
[ToC]
## Intro & Motivation
Using blockchain opens our hands to do things that were not possible before.
Due to the low cost of Solana Network, its use has become very popular in the last year. In this post, we are going to develop a tool that could not exist in a world outside of blockchain. We are going to develop the program step by step, and hope that it serves as a good resource to start programming on the [Solana](https://solana.com/) Blockchain.
As we ramped up on Solana programming, we relied heavily on [this excellent blog post](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/). You could loosely view our post as a "fork" of it with (hopefully) cleaner code design and clearer explanation.
You do not need any previous programming experience in Solana; the backend code is written in [Rust](https://doc.rust-lang.org/book) and client side code is written in [python3](https://docs.python.org/3/). This is different from [3] where the client is in typescript.
### What is an Escrow?
(This section is just a copy of the [3], which has a good explanation of the details.)
An escrow smart contract is a good starting point to solana programming. It highlights well different nuances of a blockchain whilst being easy to understand conceptually. In most trustless mechanisms, escrows play an important role in exchange of valuables.

Imagine Alice has an asset A and Bob has an asset B. They would like to trade their assets but neither wants to send their asset first. After all, what if the other party does not hold up their end of the trade and runs away with both assets? A deadlock will be reached where no party wants to send their asset first.
The traditional way to solve this problem is to introduce a third party C which both A and B trust. A or B can now go first and send their asset to C. C then waits for the other party to send their asset and only then does C release both assets.
The blockchain way is to replace the trusted third party C with code on a blockchain, specifically a smart contract that verifiably acts the same way a trusted third party would. A smart contract is superior to a trusted third party because of a number of reasons. For example, can you be sure that the trusted third party isn't colluding with the person on the other side of the trade? You can be sure with a smart contract because you can look at the code before running it.
## A brief introduction on Solana
### High-level Structure of Solana Programming

On a high level, we code up the Client (e.g., in Python, TypeScript) and Program A (e.g., in Rust or C). Then we deploy Program A on Solana network, and users can interact with it through Client.
### Accounts in Solana
According to Solana documentation, you can think of accounts in Solana as files in an operating system. Each file has an address, some data inside it and some metadata. In Solana, each Account is readable by everyone. However, if any user could modify any account, it would lead to a total mess. To avoid that, each account has an *owner* (stored in the account's metadata) that specifies which program is allowed to modify its contents.
An account may be generated or modified by a transaction in Solana. A program is needed to generate an Account, and that program is the owner of the Account and the only authority to modify its data/metadata later. The owner of the Account is the one who has the private key of the Account. For e.g.: all user accounts are owned by a special program called the *System Program*. Among many responsibilites the owner is that of signing, i.e., for the following actions the owner should sign the transaction:
- Change the data of the Account
- Pay lamports either for the transaction cost or to anyone else in the network
- Give the authority of the Account to someone else
- Any action defined in the account owner program that needs permission
An account can be of different types, some of important account types are:
- Executable Account: We call this type of Account a program as well. The data saved in the data field is the code of the executable code of the program.
- Mint account: This Account is generated by Token Program and stores the information of the mint account. You may consider this as the treasury of a currency. It saves the data related to the currency in its data field in a struct named Mint. Let's take a look at some of the fields:
- mint_authority: Public key of the authority of the mint account. The authority could generate mints and send them to any token account.
- supply: The total amount of that Mint.
- Token Account: This Account is generated by Token Program and stores the information related to the value and owner of the Account. This information is saved in the data field of the [`AccountInfo`](https://docs.rs/solana-program/1.5.0/solana_program/account_info/struct.AccountInfo.html#fields). A struct named [Account](https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/state.rs#L86) is usually used to store in the `AccountInfo`'s data field:
- owner: Public key of the Account owner (usually a person); note that this is different from the owner field in `AcountInfo`, which is the token program. So let's call it user space owner (or "inner owner") to avoid confusing with system space owner (or "outer owner").
- mint: The public key of the mint account
- amount: The amount of the token this Account has.
- User account: This Account is generated by the System Program
- Data account: This Account is generated to just keep some data inside it. The generator of the account is usually a program that would store the state of the program inside this account.
To make things clear, each Account has a (system space) owner field in its [AccountInfo](https://docs.rs/solana-program/1.5.0/solana_program/account_info/struct.AccountInfo.html#fields), which is not possible to change. However, some accounts, such as token Accounts, have a (user space) owner as well, which is stored in the data field of its account info which has the permission authority for some actions.
Let's scrutinize the token Account a little bit more as it is crucial in understanding how the escrow works. As we said, the token Account has a user-space owner who has the authority to sign the transactions to permit specific changes in the account data. These changes include sending some tokens from the Account to some other account or changing the user-space owner of the Account. However, as it happens in escrow, sometimes we want a program-owned Account to authorize the permissions of a token account.
We would need to know a bit about transactions in Solana.
### Transactions and Instructions
Each action in Solana is get done by a transaction. A transaction is a sequence of instructions. Each instruction consist of three components:
- program_id: the public key of the program which the instruction wants to run
- accounts: list of accounts metadata to be passed to instruction; this includes the public key, is_signer, and is writable of the account
- data: the parameters we want to pass to the program for doing the job. This data is the serialized version of whatever we want to pass to the program other than accounts metadata serialized into a sequence of bytes.
Note that if the program needs to read, write or execute any account, the account information should pass from the client-side information in the accounts field. Solana will not allow accessing the account if the information is sent through the data field.
To send a transaction, the client needs to have the signature of the transaction payer and all the accounts that their sign flag is True.
For example, if a user account wants to send some token from their token account, the signer of that account should be True, and the user should sign it with the key pair. However, sometimes the owner of an account is not a user account but a program account. So we may want a program to send tokens. Let's name the program-owned token account a vault. As you can imagine, this scenario is a bit different as the program account can not store any private key inside it (there is no hidden data in the blockchain). The other difference here is that the signature from the client-side serves two purposes, one is authentication, and the other is authorization. However, as the program rest inside the blockchain, it does not need authentication; it just needs to prove somehow the vault belongs to the program. Solana has a genuise solution for this which is Program Derived Address (PDA).
#### PDAs
The PDA is a public address derived uniquely from a program public address and a seed, using a hash function `find_program_address`, which has two important features:
- There is no private address matching this public address. It means that the PDA address is off the elliptic [curve](https://en.wikipedia.org/wiki/Curve25519). This makes it impossible for someone else to take control of this address using a private key.
- It is computationally impossible to reverse the `find_program_address`. In other words if you can't find a seed to derive the same PDA from another program address.
With these two features when the program ID signs the transaction with the seed, solana can be sure that the program has the correct credentials to sign the transaction.
## Escrow Program
Each action in Solana is done by a transaction where a transaction is a sequence of instructions. An instruction consist of three components:
- program_id: the public key of the program which the instruction wants to run
- accounts: list of accounts metadata to be passed to instruction; this includes the public key, is_signer, and is_writable of the account
- data: the parameters of the program. This data is the serialized version of whatever we want to pass to the program other than accounts metadata serialized into a sequence of bytes.
Note that if the program needs to read, write or execute any account, the account information should pass from the client-side information in the accounts field. Solana will not allow accessing the account if the information is sent through the data field.
To send a transaction, the client needs to have the signature of the transaction payer and all the accounts that their sign flag is True.
For example, if a user account wants to send some token from their token account, the signer of that account should be True, and the user should sign it with the key pair. However, sometimes the owner of an account is not a user account but a program account. So we may want a program to send tokens. Let's name the program-owned token account a vault. As you can imagine, this scenario is a bit different as the program account can not store any private key inside it (there is no hidden data in the blockchain). The other difference here is that the signature from the client-side serves two purposes, one is authentication, and the other is authorization. However, as the program rest inside the blockchain, it does not need authentication; it just needs to prove somehow the vault belongs to the program. Solana has a genuine solution for this which is Program Derived Address (PDA).
#### States
We use the following diagram as the states of the escrow. When it is generated for the first time it will be in the initialized state. In initialized state there are only two acceptable instructions, Alice and Bob deposit, if the escrow receives any other instruction it will fail. After Alice/Bob sends the first deposit instruction, the state will be Deposit Alice/Bob and then after the other one sends the deposit instruction state of the machine will be commited.
In Deposit Alice/Bob state, if Alice/Bob want can withdraw her/his token and go back to initialized state. This could happen only before the other party sends the deposit instruction. As soon as both of them sends deposit instruction, the state will be Commited. Then each party could withdraw the other party token from escrow vault account. When both withdrawed the tokens state will be Uninitialized and the initiator would be able to change the Size X/Y.

### Setting up
Head over to the [template repo](https://github.com/mvines/solana-bpf-program-template), click `Use this template`, and set up a repo. Vscode with the rust-analyzer extension is what we use. You'll also need [Rust](https://www.rust-lang.org/tools/install). Additionally, go [here](https://docs.solana.com/cli/install-solana-cli-tools) to install the Solana dev tools. (If you're on mac and there are no binaries for the version you want, follow the "build from source" section and add installed the bins to path.
If you don't know how to test solana programs yet, remove all the testing code. Testing programs is a topic for another blog post. Remove the testing code in `lib.rs` as well as the `tests` folder next to `src`. Lastly, remove the testing dependencies from [`Cargo.toml`](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html?highlight=cargo#creating-a-project-with-cargo). It should now look like this:
```rust=1
[package]
name = "solana-escrow"
version = "0.1.0"
edition = "2018"
license = "WTFPL"
publish = false
[dependencies]
solana-program = "1.6.9"
[lib]
crate-type = ["cdylib", "lib"]
````
In addition to the above, we need a `lib.rs` file that includes all the modules you use in the program.
```rust=
// src/lib.rs
pub mod instruction;
pub mod processor;
pub mod state;
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint;
```
It might be a good idea to create empty versions of these files, and we will fill them with code as we move along.
- Program side
- Possible things to do on blockchain, i.e. transactions (making accounts, transferring ownership, writing data,)
- Ownership (outer space/inner space)
- Sign/Write
- CPI/PDA
In solana every entity is an account and each account belongs to one program.
For example, user accounts belongs to system program
### Initialization
To start the escrow one first needs to have two accounts who want to make an exchange of asset X with Y. So each account should own token accounts for each of these two assets. In the real world, users already exist and they have the accounts related to the tokens they want to exchange. However here to test the system it is needed to make the accounts Alice and Bob and make X and Y tokens for each one. Also note that each token needs to be connected with a mint account. So we also need to make the mint accounts for assets X and Y.

Note that all of the boxes in the above figure are [accounts](https://docs.solana.com/developing/programming-model/accounts). The blue, white, and yellow boxes are respectively user account, token account, and mint account. As mentioned before, in data field of a token account there are references to the user-space owner and also the mint account. The arrows coming out of token accounts are showing this references. Also note that the system-space owner of the token accounts and mint accounts is Token Program and the system-space owner of the user account is System Proram.
#### Client Side
The client side is python code (or typescript) as `client/setup2.py`. It requires transactions for initialization to be sent to the http client. To that end, the following things need to be known:
a) `Program ID`: We need the public key for the deployed program for the creation of `vaultx`/`vaulty` which will store the token X sent by Alice (token Y sent by Bob). This is required to ensure that `vaultx`/`vaulty` are owned by the program. The design principle is different from the methodology employed in [3] where the ownership is transferred to the escrow program from Alice/Bob.
b) `Payer Account`: The payer account is a keypair that pays (in solana) for mint accounts for individual tokens, token accounts for Alice and Bob. By default, we set the payer to be Alice (it can be Bob, or even a third party). The creation of these accounts is critical to the next steps of depositing/withdrawing tokens.
c) `Alice Public Key`: To create the token accounts for
To initialize an escrow account first we assume we already have Alice and Bob accounts which both have X and Y tokens.
d) `Bob Public Key`
The initialization step generates
- `X_mint_account`
- `Y_mint_account`
- `alice_x_token_account`
- `alice_y_token_account` (similarly for Bob)
```python=3
# Creating Token Accounts
X_mint_account_address = Token.create_mint(conn=http_client, payer=payer_loaded_account,\
mint_authority=payer_loaded_account.public_key,\
decimals=0, program_id=TOKEN_PROGRAM_ID)
Y_mint_account_address = Token.create_mint(conn=http_client, payer=payer_loaded_account,\
mint_authority=payer_loaded_account.public_key,\
decimals=0, program_id=TOKEN_PROGRAM_ID)
alice_X_token_account = X_mint_account_address.create_associated_token_account(alice_pubkey)
alice_Y_token_account = Y_mint_account_address.create_associated_token_account(alice_pubkey)
bob_X_token_account = X_mint_account_address.create_associated_token_account(bob_pubkey)
bob_Y_token_account = Y_mint_account_address.create_associated_token_account(bob_pubkey)
X_mint_account_address.mint_to(alice_X_token_account,payer_loaded_account,1000)
Y_mint_account_address.mint_to(bob_Y_token_account,payer_loaded_account,100)
```
The vault accounts are the intermediate accounts that store the tokens transferred by Alice and/or Bob. As mentioned before they are owned by the escrow program. To facilitate this we need a PDAs (program derived addresses). To create a PDA account derived from program address (why we need the pda account derived from the program address?), we need to define seeds for all the three accounts. We want three different seeds for three different addresses. Also, note that if we used static seeds (i.e. those that did not depend on user specific details), it would generate the same address for anybody who wants to generate it. So we will use public addresses of Alice and Bob and public addresses of mints X and Y to generate the seeds. Finally we use a pass phrase that can be known only to Alice and Bob.
- `vaultx`, `vaulty` accounts: These are PDAs as they need to store the tokens sent by Alice/Bob.
Specifically, `alice_x_token_account` (along with other token accounts) is created from a `create_associated_token_account()` and is associated to the respective token and user. Finally, we create the `EscrowAccount` using the `escrow_address`. The escrow account stores the necessary state information.
The following figure shows the accounts generated in the backend side. Again the arrows shows the references to user-space ownership and mints. The system-space owner of the Escrow Account and vaults are the Escrow program and Token program respectively.

The seeds we use are as follows:
```python=3
# Client side:
password = (20).to_bytes(8,byteorder='little')
x_seeds = [
b"vault_x",
password,
bytes(alice_pubkey),
bytes(bob_pubkey),
bytes(X_mint_account_address.pubkey),
bytes(Y_mint_account_address.pubkey),
]
y_seeds = [
b"vault_y",
password,
bytes(alice_pubkey),
bytes(bob_pubkey),
bytes(X_mint_account_address.pubkey),
bytes(Y_mint_account_address.pubkey),
]
escrow_seeds = [
b"escrow",
password,
bytes(alice_pubkey),
bytes(bob_pubkey),
bytes(X_mint_account_address.pubkey),
bytes(Y_mint_account_address.pubkey),
]
```
To generate the vault accounts and the escrow account we simply call `find_program_address` on the seeds and `program_id`
```python 1
vaultx, xseed = PublicKey.find_program_address(seeds=x_seeds,program_id=program_id)
vaulty, yseed = PublicKey.find_program_address(seeds=y_seeds,program_id=program_id)
escrow_address, escrow_seed = PublicKey.find_program_address(seeds=escrow_seeds,program_id=program_id)
```
By passing the seeds and program adress to `find_program_address` function, it returns a tuple of public address of PDA and bump seed. To generate the same bump seed on server side we use same seeds as on the client side. We send the password in the data of the instruction to ensure this and send the public keys in the account part of the instruction.
```python = 1
# Client side
print('Start Initializing Accounts')
# initialize transaction
x_val = x_tokens_to_send
y_val = y_tokens_to_send
data = pack('<BQQ', 0,x_val,y_val)+password
process_init(program_id, escrow_address, x_mint_pubkey,\
y_mint_pubkey, vaultx, vaulty, payer_public_key,\
alice_pubkey, bob_pubkey, data, http_client, payer_loaded_account)
```
The data is sent as a stream of bytes via `pack('<BQQ', 0, 13, 15)` described in the python [struct](https://docs.python.org/3/library/struct.html) page. It simply concatenates (serializes) the bytes of the data and sends it to the backend (Rust side). We decode the arguments for pack in `data`: in `<BQQ`, `<` stands for little endian, `B` denotes that a 1 byte is expected and `Q` indicates that a 8 byte integer is expected. The first byte here is `0` which maps to `InitEscrow` in `instruction.rs`. Note that `InitEscrow` takes three inputs `amount_x, amount_y, pass` corresponding to `x_val, y_val, password` respectively. The backend of the initialization will be described in the next section.
We conclude this section by describing `process_init`. `process_init` simply sends the initialization transaction to backend to be processed. This is achieved by
```python=3
tx = Transaction()
tx_instruction = TransactionInstruction(
program_id=program_id,
keys=[
AccountMeta(pubkey=escrow_address, is_signer=False, is_writable=True),
AccountMeta(pubkey=x_mint_pubkey, is_signer=False, is_writable=False),
AccountMeta(pubkey=y_mint_pubkey, is_signer=False, is_writable=False),
AccountMeta(pubkey=vaultx, is_signer=False, is_writable=True),
AccountMeta(pubkey=vaulty, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer_public_key, is_signer=True, is_writable=False),
AccountMeta(pubkey=alice_pubkey, is_signer=False, is_writable=False),
AccountMeta(pubkey=bob_pubkey, is_signer=False, is_writable=False),
AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False),
],
data=data,
)
tx = tx.add(tx_instruction)
transaction_results = http_client.send_transaction(tx, *[payer_loaded_account])#, *[payer_loaded_account])
print('Initializing Accounts Ended')
```
`tx_instruction` has all necessary information about account creation and initialization. `AccountMeta` describes the signer/writer behavior of each account. The vaults and escrow are writeable accounts, whereas the only signer is the payer. The field `data` describes which instruction from `EscrowInstruction` in `instruction.rs` will be executed.
#### Backend
Once the transaction is sent from the client, rust goes to `entrypoint.rs` which serves as the "entry point" to the rust code once the transaction is issued from the client side.
```rust=1
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Processor::process(program_id, accounts, instruction_data)
}
```
Following this rust looks into `processor.rs` at the function `process` which describes what to do next.
```rust=1
...
pub struct Processor;
impl Processor {
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = EscrowInstruction::try_from_slice(instruction_data)?;
}
}
```
The `EscrowInstruction::try_from_slice(instruction_data)?` reads `data` sent from the client byte-by-byte. The deserialization process is done using [borsh](https://borsh.io/). The first byte is `0` in `data` which makes it so that `InitEscrow` is chosen from the enum `EscrowInstruction` in `instruction.rs`. Following that `x_val` and `y_val` are the number of tokens Alice and Bob are willing to transfer from `x_token` and `y_token` accounts and the `password` is the pass used for generating the keys.
```python=3
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug)]
pub enum EscrowInstruction {
/// Starts the trade by creating and populating an escrow account and transferring ownership of the given temp token account to the PDA
/// Accounts expected:
///
/// 0. `[signer]` The account of the person initializing the escrow
/// 1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
/// 2. `[]` The initializer's token account for the token they will receive should the trade go through
/// 3. `[writable]` The escrow account, it will hold all necessary info about the trade.
/// 4. `[]` The rent sysvar
/// 5. `[]` The token program
InitEscrow {
amount_x: u64, //amounts_x:x_val, amounts_y:y_val, password:pass
amount_y: u64,
pass: [u8; 32],
},
Deposit{
pass: [u8; 32],
},
Withdrawal {
pass: [u8; 32],
},
}
```
We can now check the implementation of `InitEscrow` in `processor.rs`
```rust=1
match instruction {
EscrowInstruction::InitEscrow {
amount_x,
amount_y,
pass,
} => {
msg!("Instruction: InitEscrow");
Self::process_init_escrow(accounts, amount_x, amount_y, pass, program_id)
}
```
Once the instruction is matched to `InitEscrow`, the `process_init_escrow` is called with the account details, the deserialized data and program_id.
```rust=1
let account_info_iter = &mut accounts.iter();
let escrow_info = next_account_info(account_info_iter)?;
let mint_x_info = next_account_info(account_info_iter)?;
let mint_y_info = next_account_info(account_info_iter)?;
let vault_x_info = next_account_info(account_info_iter)?;
let vault_y_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let alice_info = next_account_info(account_info_iter)?;
let bob_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let escrow_bump = if escrow_info.data_len() == 0 {
msg!("Creating escrow metadata");
let escrow_seeds = &[
b"escrow",
alice_info.key.as_ref(),
bob_info.key.as_ref(),
mint_x_info.key.as_ref(),
mint_y_info.key.as_ref(),
pass.as_ref(), //.to_le_bytes(),
];
let rent = &Rent::from_account_info(rent_info)?;
let required_lamports = rent
.minimum_balance(EscrowData::LEN)
.max(1)
.saturating_sub(escrow_info.lamports());
let (_, bump) = Pubkey::find_program_address(escrow_seeds, program_id);
let escrow_seeds = &[
b"escrow",
alice_info.key.as_ref(),
bob_info.key.as_ref(),
mint_x_info.key.as_ref(),
mint_y_info.key.as_ref(),
pass.as_ref(),
&[bump],
];
solana_program::program::invoke_signed(
&system_instruction::create_account(
payer_info.key, //from_pubkey
escrow_info.key, //to_pubkey
required_lamports, //lamports
EscrowData::LEN as u64, //space
program_id,
),
&[
payer_info.clone(),
escrow_info.clone(),
system_program_info.clone(),
],
&[escrow_seeds],
)?;
bump
}
```
The first part of `process_init_escrow` is getting individual account information sent from the client side.
```rust=1
let account_info_iter = &mut accounts.iter();
let escrow_info = next_account_info(account_info_iter)?;
let mint_x_info = next_account_info(account_info_iter)?;
let mint_y_info = next_account_info(account_info_iter)?;
let vault_x_info = next_account_info(account_info_iter)?;
let vault_y_info = next_account_info(account_info_iter)?;
let payer_info = next_account_info(account_info_iter)?;
let alice_info = next_account_info(account_info_iter)?;
let bob_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
let rent_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
```
This is standard rust code, but the flow of the function is important. This will be the same flow for `process_deposit` and `process_withdraw`. The next part is generating the seed on the rust side (that should match the client side) :
```rust=1
let escrow_bump = if escrow_info.data_len() == 0 {
msg!("Creating escrow metadata");
let escrow_seeds = &[
b"escrow",
alice_info.key.as_ref(),
bob_info.key.as_ref(),
mint_x_info.key.as_ref(),
mint_y_info.key.as_ref(),
pass.as_ref(), //.to_le_bytes(),
];
let rent = &Rent::from_account_info(rent_info)?;
let required_lamports = rent
.minimum_balance(EscrowData::LEN)
.max(1)
.saturating_sub(escrow_info.lamports());
let (_, bump) = Pubkey::find_program_address(escrow_seeds, program_id);
let escrow_seeds = &[
b"escrow",
alice_info.key.as_ref(),
bob_info.key.as_ref(),
mint_x_info.key.as_ref(),
mint_y_info.key.as_ref(),
pass.as_ref(),
&[bump],
];
```
The requirement for seeds is necessary for PDAs, as described in the previous sections. This is followed by the `create_account`
```rust=1
solana_program::program::invoke_signed(
&system_instruction::create_account(
payer_info.key, //from_pubkey
escrow_info.key, //to_pubkey
required_lamports, //lamports
EscrowData::LEN as u64, //space
program_id,
),
&[
payer_info.clone(),
escrow_info.clone(),
system_program_info.clone(),
],
&[escrow_seeds],
)?;
bump
}
```
`create_account` creates the escrow account in this example. If you notice there is `invoke_signed` which ensures that the account is owned by the program and not the payer. The `invoke_seed` requires that we send the escrow seed so runtime can verify that the program indeed owns the account.
### Deposit
After various accounts are created, it's time for the Alice and Bob to separately deposit tokens to the escrow account. Intuitively, a single deposit transaction (in the example of Alice) is just that: transfering token X to the escrow-program-owned vault account for X (Vault_X), with a bunch of checks in the process.

Let's start from the Client end.
``` python = 1
# Client end
data = pack('<BQ', 1, password)
```
As before, `pack('<BQ', 1, password)` uses the `pack` function in [Python struct package](https://docs.python.org/3/library/struct.html) to simply concatenate (serialize) the bytes representation of the data we want to send to the backend side (where it is deserialized using `borsh`). The first byte is `1` which is used in the instruction in the backend to choose `Deposit`, as the index for it in `enum EscrowInstruction` in `instruction.rs`. The `password` is used for re-generating the escrow and vault account public keys (this is a design decision; the alternative is to save those public keys in state in `initialization` step).
Then we include `AccountMeta` and `program_id` to build an instruction through `TransactionInstruction`, and add it to a transaction, same as the `initialization` step.
``` python = 1
# Client
import struct
tx = Transaction()
tx_instruction = TransactionInstruction(
keys=[AccountMeta(pubkey=escrow_address, is_signer=False, is_writable=True),
AccountMeta(pubkey=alice_X_token_account, is_signer=False, is_writable=True), # x_a info
AccountMeta(pubkey=vaultx, is_signer=False, is_writable=True),
AccountMeta(pubkey=alice_pubkey, is_signer=True, is_writable=False),
AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False),
],
program_id=program_id,
data = data,
)
tx = tx.add(tx_instruction)
```
Finally we send the transaction to interact with our escrow program in the backend (in the example of Alice depositing).
```python =1
http_client.send_transaction(tx, *[alice_keypair])
```
Next, we move on to the backend Rust code. When a transaction is sent, `process_instruction` function in `entrypoint.rs` is called, which in turn calls `process` method in `processor.rs`. After deserialize instruction and matching with `EscrowInstruction::Deposit`, `process_deposit` is called.
```rust=
// in processor.rs
pub struct Processor;
impl Processor {
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = EscrowInstruction::try_from_slice(instruction_data)?;
match instruction {
...
EscrowInstruction::Deposit { pass } => {
msg!("Instruction: Deposit");
Self::process_deposit(accounts, pass, program_id)
}
...
}
}
```
In the `process_deposit` function, we start by creating accoun_info variables to store the `AccountMeta` that we passed in. We also deserialize the `data` field of `escrow_info` into `escrow_data`. At a high level, we will use information contained in `escrow_data` to check if the transaction sent is valid and update state, before re-serialize this information and store again in the escrow account.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let escrow_info = next_account_info(account_info_iter)?; // mint public address
let payer_token_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?; // mint public address
let payer_info = next_account_info(account_info_iter)?; // payer_account, is it both public and private key? yeah
let token_program_info = next_account_info(account_info_iter)?; // token_program_id
let mut escrow_data = EscrowData::try_from_slice(&escrow_info.data.borrow())?;
...
Ok(())
}
```
Then, as discussed above, we will first update the state. Inside the `match` control flow operator, we have a few cases.
1. `EscrowState::Initialized`: this means that all accounts have been initialized but no deposit has been made yet.
- If the depositer's public key matches Alice's, then we change the state to `EscrowState::DepositAlice`, meaning Alice has deposited after this function call;
- Or if the depositer's public key matches Bob's, then we change the state to `EscrowState::DepositBob`, meaning Bob has deposited after this function call;
- Otherwise throw an `Err(ProgramError::InvalidAccountData)`.
2. `EscrowState::DepositAlice`: this means that Alice already deposited, so we only need to check if the depositer's public key matches Bob's, and if so, change the state to `EscrowState::Committed` which means both parties have deposited.
3. `EscrowState::DepositBob` is handled analogously as case 2.
4. All other cases we throw an `Err(ProgramError::InvalidAccountData)`.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
msg!("Validating and changing state");
match escrow_data.state {
EscrowState::Initialized => {
if *payer_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::DepositAlice;
} else if *payer_info.key == escrow_data.pubkey_bob {
escrow_data.state = EscrowState::DepositBob;
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::DepositAlice => {
if *payer_info.key == escrow_data.pubkey_bob {
escrow_data.state = EscrowState::Committed;
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::DepositBob => {
if *payer_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::Committed;
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
_ => {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
...
Ok(())
}
```
Now we need to do some checks to ensure the transaction sent from the client end is valid.
1. The depositer's token account's "outer ownership" belongs to the token program. Strictly speaking this check is not necessary since when we call `&spl_token::instruction::transfer` below to actually transfer the token, it will fail if the token program does not own the depositer's token account.
2. The depositer's token account's "inner ownership" belongs to the depositer.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
msg!("Validating account ownership");
if payer_token_info.owner != token_program_info.key {
msg!("Invalid Token Account (system account not owned by Token Program)");
return Err(ProgramError::InvalidAccountData);
}
let token_account: Account = Account::unpack_unchecked(&payer_token_info.data.borrow())?;
if token_account.owner != *payer_info.key {
msg!("Invalid Token Account (\"User space\" owner mismatch)");
return Err(ProgramError::InvalidAccountData);
}
...
Ok(())
}
```
3. If the depositer is Alice, and if her token account contains token X, which is what we expect, then we extract `vault_seed`, `bump_seed`, and `size`. The case where Bob is the depositer is handled analogously. Beyond these two cases we throw an `Err(ProgramError::InvalidAccountData)`.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
let (vault_seed, bump_seed, size) = if *payer_info.key == escrow_data.pubkey_alice {
if token_account.mint != escrow_data.pubkey_mint_x {
msg!("Invalid Mint");
return Err(ProgramError::InvalidAccountData);
}
(b"vault_x", escrow_data.vault_x_bump, escrow_data.size_x)
} else if *payer_info.key == escrow_data.pubkey_bob {
if token_account.mint != escrow_data.pubkey_mint_y {
msg!("Invalid Mint");
return Err(ProgramError::InvalidAccountData);
}
(b"vault_y", escrow_data.vault_y_bump, escrow_data.size_y)
} else {
msg!("Invalid Owner");
return Err(ProgramError::InvalidAccountData);
};
...
Ok(())
}
```
Then we use all the information above to re-generate `vault_pubkey` and check if it matches with what we passed in. Again, this is a design decision; we could have saved the `vault_pubkey` when we initiated it and do the same check here. The program will throw `Err(ProgramError::InvalidAccountData)` if they do not match.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
let seeds = &[
vault_seed,
escrow_data.pubkey_alice.as_ref(),
escrow_data.pubkey_bob.as_ref(),
escrow_data.pubkey_mint_x.as_ref(),
escrow_data.pubkey_mint_y.as_ref(),
pass.as_ref(), // .to_le_bytes(),
&[bump_seed],
];
let vault_pubkey = Pubkey::create_program_address(seeds, program_id)?;
if vault_pubkey != *vault_info.key {
msg!("Vault key mismatch");
return Err(ProgramError::InvalidAccountData);
}
...
Ok(())
}
```
We also validate if the re-generated escrow public key matches what's passed in, same as above, and throw `Err(ProgramError::InvalidAccountData)` if it does not.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
msg!("Validating escrow data");
let escrow_seeds = &[
b"escrow",
escrow_data.pubkey_alice.as_ref(),
escrow_data.pubkey_bob.as_ref(),
escrow_data.pubkey_mint_x.as_ref(),
escrow_data.pubkey_mint_y.as_ref(),
pass.as_ref(), // .to_le_bytes(),
&[escrow_data.escrow_bump],
];
let escrow_key = Pubkey::create_program_address(escrow_seeds, program_id)?;
if escrow_key != *escrow_info.key {
msg!("Escrow key mismatch");
return Err(ProgramError::InvalidAccountData);
}
...
Ok(())
}
```
Finally, it's time to actually transfer the token from depositer's token account to the vault. We `invoke` `transfer` function in `spl_token::instruction`.
```rust=1
pub fn process_deposit(
accounts: &[AccountInfo],
pass: [u8;32],
program_id: &Pubkey,
) -> ProgramResult {
...
msg!("Sending transfer");
solana_program::program::invoke(
&spl_token::instruction::transfer(
token_program_info.key,
payer_token_info.key,
&vault_info.key,
&payer_info.key,
&[],
size,
)?,
&[
payer_token_info.clone(),
payer_info.clone(),
vault_info.clone(),
token_program_info.clone(),
],
)?;
escrow_data.serialize(&mut &mut escrow_info.data.borrow_mut()[..])?;
Ok(())
}
```
### Withdraw
We need to add withdrawal process to the instructions first.
```rust=
// src/instruction.rs
...
pub enum EscrowInstruction {
InitEscrow {
amount_x: u64,
amount_y: u64,
pass: [u8; 32],
},
Deposit{
pass: [u8; 32],
},
Withdrawal {
pass: [u8; 32],
},
}
```
Let's think again about the withdraw process in the state machine, by studying one of the parties. As long as Alice has deposited her token she can withdraw a token from escrow. However the token she can withdraw depends on the state. If bob has not deposited his token yet, Alice can withdraw her X token already deposited. As soon as Bob has deposit his token, Alice can only withdraw Y token from escrow. For now `process_withdrawal` function of the following code just handles the states.
```rust=
// src/processor.rs
impl Processor {
pub fn process(...) -> ProgramResult {
...
match instruction {
...
EscrowInstruction::Withdrawal { pass } => {
Self::process_withdrawal(accounts, pass, program_id)
}
}
}
...
pub fn process_withdrawal(
accounts: &[AccountInfo],
pass: [u8; 32],
program_id: &Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let escrow_info = next_account_info(account_info_iter)?;
let taker_token_info = next_account_info(account_info_iter)?;
let vault_info = next_account_info(account_info_iter)?;
let taker_info = next_account_info(account_info_iter)?;
let token_program_info = next_account_info(account_info_iter)?;
msg!("process_withdrawal 1");
let mut escrow_data = EscrowData::try_from_slice(&escrow_info.data.borrow())?;
let withdraw_mint = match escrow_data.state {
EscrowState::Committed => {
if *taker_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::WithdrawAlice;
escrow_data.pubkey_mint_y
} else if *taker_info.key == escrow_data.pubkey_bob {
escrow_data.state = EscrowState::WithdrawBob;
escrow_data.pubkey_mint_x
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::WithdrawAlice => {
if *taker_info.key == escrow_data.pubkey_bob {
escrow_data.state = EscrowState::Uninitialized;
escrow_data.pubkey_mint_x
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::WithdrawBob => {
if *taker_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::Uninitialized;
escrow_data.pubkey_mint_y
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::DepositAlice => {
if *taker_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::Initialized;
escrow_data.pubkey_mint_x
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
EscrowState::DepositBob => {
if *taker_info.key == escrow_data.pubkey_alice {
escrow_data.state = EscrowState::Initialized;
escrow_data.pubkey_mint_y
} else {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
}
_ => {
msg!("Invalid State");
return Err(ProgramError::InvalidAccountData);
}
};
}
```
Then there are the checks we have to have before starting the transfer to prevent any possible malicious activities.
```rust=
// src/processor.rs
pub fn process_withdrawal(
accounts: &[AccountInfo],
pass: [u8; 32],
program_id: &Pubkey,
) -> ProgramResult {
...
msg!("Validating account ownership");
if taker_token_info.owner != token_program_info.key {
msg!("Invalid Token Account (system account not owned by Token Program)");
return Err(ProgramError::InvalidAccountData);
}
let token_account: Account = Account::unpack_unchecked(&taker_token_info.data.borrow())?;
if token_account.owner != *taker_info.key {
msg!("Invalid Token Account (\"User space\" owner mismatch)");
return Err(ProgramError::InvalidAccountData);
}
if token_account.mint != withdraw_mint {
msg!("Invalid Mint");
return Err(ProgramError::InvalidAccountData);
}
let (vault_seed, bump_seed, size) = if withdraw_mint == escrow_data.pubkey_mint_y {
(b"vault_y", escrow_data.vault_y_bump, escrow_data.size_y)
} else if withdraw_mint == escrow_data.pubkey_mint_x {
(b"vault_x", escrow_data.vault_x_bump, escrow_data.size_x)
} else {
msg!("Invalid Mint");
return Err(ProgramError::InvalidAccountData);
};
msg!("Validating vault");
let vault_seeds = &[
vault_seed,
escrow_data.pubkey_alice.as_ref(),
escrow_data.pubkey_bob.as_ref(),
escrow_data.pubkey_mint_x.as_ref(),
escrow_data.pubkey_mint_y.as_ref(),
pass.as_ref(),
&[bump_seed],
];
let vault_pubkey = Pubkey::create_program_address(vault_seeds, program_id)?;
if vault_pubkey != *vault_info.key {
msg!("Vault key mismatch");
return Err(ProgramError::InvalidAccountData);
}
msg!("Validating escrow data");
let escrow_seeds = &[
b"escrow",
escrow_data.pubkey_alice.as_ref(),
escrow_data.pubkey_bob.as_ref(),
escrow_data.pubkey_mint_x.as_ref(),
escrow_data.pubkey_mint_y.as_ref(),
pass.as_ref(),
&[escrow_data.escrow_bump],
];
let escrow_key = Pubkey::create_program_address(escrow_seeds, program_id)?;
if escrow_key != *escrow_info.key {
msg!("Escrow key mismatch");
return Err(ProgramError::InvalidAccountData);
}
msg!("Sending transfer");
}
```
After the program make sure that all the inputs are consistent by doing the checks, it will do the main part of the withdrawal which is sending money from vault account to the token account of the user who requested for the withdrawal, and then serialize the data to save it in state.
```rust=
// src/processor.rs
pub fn process_withdrawal(
accounts: &[AccountInfo],
pass: [u8; 32],
program_id: &Pubkey,
) -> ProgramResult {
...
solana_program::program::invoke_signed(
&transfer(
token_program_info.key,
&vault_info.key,
taker_token_info.key,
&escrow_info.key,
&[],
size,
)?,
&[
vault_info.clone(),
escrow_info.clone(),
taker_token_info.clone(),
token_program_info.clone(),
],
&[escrow_seeds],
)?;
escrow_data.serialize(&mut *escrow_info.data.borrow_mut())?;
Ok(())
}
```
## Future Directions
1. Currently we have an escrow which can serve only two specific parties. One possible further step of this could be an escrow which can keep the book for more than one pair.
2. This escrow is the simplest version of the contract between two parties, which can be extended to future contracts.
## Sources
[[1]](https://docs.solana.com/) Solana Documentation
[[2]](https://github.com/solana-labs/solana) Solana Source Code
[[3]](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/) Programming on Solana - An Introduction
[[4]](https://github.com/tsarkar23/escrow) Escrow GitHub code