Skip to content
Fernando Mendez
TwitterGithub

Exploring Program Derive Addresses (PDA's) with Solana, Anchor and React

rust, programming, solana, crypto, anchor, react6 min read

Note: As of the time of this writing (Monday, December 13, 2021), Solana's testnet environment (faucet/airdrop) seems to be having issues. Please select the devnet on the selector (or just don't change it, since is the default value). Remember to change your wallet to point to the devnet network.

Note: all the code for this post can be found here. There's a demo here showcasing the concepts in this post.

Use cases

Let's image the following scenarios. You built an dApp that uses tokens you created / minted. For testing purposes, you want to allow users to self-airdrop some amount of those tokens (on testings environments). The problem is -since you minted the tokens- the one with the authority to both mint more tokens or transfer them is you. That means you have to sign every transaction dealing with those mints.

Another scenario is a user wanting to trade some items with other users. For safety, the items to trade should be put in some sort of temporary account (escrow account) and only be released to a 3rd party they accept the offer. The difficulty is, if the escrow account belongs to the user, they need to approve / sign the transaction for the tokens to be released. We don't want the user to be involved in the release of the items directly.

In both scenarios, we need to have a sort of "proxy" that can sign a transaction on behalf of the owner of the program. For that, we'll need Program Derive Addresses (PDA's).

In the scenarios that I described above, we would need to use Cross-Program Invocations. In both scenarios, we would interact with the Token Program. For the case of airdropping, we will mint more of the existing tokens to a user and in the second case we will transfer tokens.

In both of these scenarios, it is the PDA that would have the authority to sign these transactions in our behalf.

PDA's defined

These are accounts that are owned by a program and not controlled by a private key like other accounts. Pubkey::create_program_address generates these addresses. This method will hash the seeds with program ID to create a new 32-byte address. There's a change (50%) that this may be a point on the ed25519 curve. That means there is a private key associated with this address. In such cases, the safety of the Solana programming model would be compromised. The Pubkey::create_program_address will fail in case the generated address lie on the ed25519 curve.

To simplify things, the method Pubkey::find_program_address will internally call the create_program_address as many times as necessary until it finds a valid one for us.

PDAs in action

To explore PDA's further, I decided to build a farm animal trading app. The animals that you trade are tokens. In this app, PDA are used in 2 different ways. The first way is an escrow account. Users put away (escrow) the tokens they are offering. These tokens will be release if either some other user accept the offer or the user initiating the offer decides to cancel it. In both cases, it is the escrow account itself that has the authority to sign the transferring of tokens to the destination.

Note: For the code snippets, I'll only be showing the relevant sections, and I'll be linking the line number on the repo. All the code can be found here.

Escrow accounts

First, we need to derive an address. This will be our escrow account(code).

1const offer = anchor.web3.Keypair.generate();
2const [escrowedTokensOfOfferMaker, escrowedTokensOfOfferMakerBump] =
3 await anchor.web3.PublicKey.findProgramAddress(
4 [offer.publicKey.toBuffer()],
5 program.programId
6 );

We store the bump so that we don't have to keep recalculating it by call the findProgrammAddress method and having to pass it down from the frontend.

In the contract / program this is how we use it (here you'll find the entire file). Here, we're creating an offer:

1anchor_spl::token::transfer(
2 CpiContext::new(
3 ctx.accounts.token_program.to_account_info(),
4 anchor_spl::token::Transfer {
5 from: ctx
6 .accounts
7 .token_account_from_who_made_the_offer
8 .to_account_info(),
9 to: ctx
10 .accounts
11 .escrowed_tokens_of_offer_maker
12 .to_account_info(),
13 authority: ctx.accounts.who_made_the_offer.to_account_info(),
14 },
15 ),
16 im_offering_this_much,
17 )

We're transferring the tokens from the account initiating the offer to the escrow account. We're also specifying how much we're transferring.

At this point, we can either accept or cancel an offer. For the cancelling part:

1// Transfer what's on the escrowed account to the offer reciever.
2 anchor_spl::token::transfer(
3 CpiContext::new_with_signer(
4 ctx.accounts.token_program.to_account_info(),
5 anchor_spl::token::Transfer {
6 from: ctx
7 .accounts
8 .escrowed_tokens_of_offer_maker
9 .to_account_info(),
10 to: ctx
11 .accounts
12 .where_the_escrowed_account_was_funded_from
13 .to_account_info(),
14 authority: ctx
15 .accounts
16 .escrowed_tokens_of_offer_maker
17 .to_account_info(),
18 },
19 &[&[
20 ctx.accounts.offer.key().as_ref(),
21 &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
22 ]],
23 ),
24 ctx.accounts.escrowed_tokens_of_offer_maker.amount,
25 )?;
26
27 // Close the escrow account
28 anchor_spl::token::close_account(CpiContext::new_with_signer(
29 ctx.accounts.token_program.to_account_info(),
30 anchor_spl::token::CloseAccount {
31 account: ctx
32 .accounts
33 .escrowed_tokens_of_offer_maker
34 .to_account_info(),
35 destination: ctx.accounts.who_made_the_offer.to_account_info(),
36 authority: ctx
37 .accounts
38 .escrowed_tokens_of_offer_maker
39 .to_account_info(),
40 },
41 &[&[
42 ctx.accounts.offer.key().as_ref(),
43 &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
44 ]],
45 ))

We're sending the tokens back to the account that initiated the offer. Notice that the authority that's signing-off the transaction is the PDA, since it "owns" the tokens. We're also closing the escrow account since it no longer needed.

The last relevant part is the "swapping" of tokens after accepting an offer:

1// Transfer token to who started the offer
2 anchor_spl::token::transfer(
3 CpiContext::new(
4 ctx.accounts.token_program.to_account_info(),
5 anchor_spl::token::Transfer {
6 from: ctx
7 .accounts
8 .account_holding_what_receiver_will_give
9 .to_account_info(),
10 to: ctx
11 .accounts
12 .account_holding_what_maker_will_get
13 .to_account_info(),
14 authority: ctx.accounts.who_is_taking_the_offer.to_account_info(),
15 },
16 ),
17 ctx.accounts.offer.amount_received_if_offer_accepted,
18 )?;
19
20 // Transfer what's on the escrowed account to the offer reciever.
21 anchor_spl::token::transfer(
22 CpiContext::new_with_signer(
23 ctx.accounts.token_program.to_account_info(),
24 anchor_spl::token::Transfer {
25 from: ctx
26 .accounts
27 .escrowed_tokens_of_offer_maker
28 .to_account_info(),
29 to: ctx
30 .accounts
31 .account_holding_what_receiver_will_get
32 .to_account_info(),
33 authority: ctx
34 .accounts
35 .escrowed_tokens_of_offer_maker
36 .to_account_info(),
37 },
38 &[&[
39 ctx.accounts.offer.key().as_ref(),
40 &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
41 ]],
42 ),
43 ctx.accounts.escrowed_tokens_of_offer_maker.amount,
44 )?;
45
46 // Close the escrow account
47 anchor_spl::token::close_account(CpiContext::new_with_signer(
48 ctx.accounts.token_program.to_account_info(),
49 anchor_spl::token::CloseAccount {
50 account: ctx
51 .accounts
52 .escrowed_tokens_of_offer_maker
53 .to_account_info(),
54 destination: ctx.accounts.who_made_the_offer.to_account_info(),
55 authority: ctx
56 .accounts
57 .escrowed_tokens_of_offer_maker
58 .to_account_info(),
59 },
60 &[&[
61 ctx.accounts.offer.key().as_ref(),
62 &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
63 ]],
64 ))

We do this is 3 steps. First, we send the tokens wanted to the user that initiated the offer. We then transfer the escrowed tokens to the user accepting the offer. Then, as with the last snipped, we're closing the escrow account since it no longer required.

Airdropping

The other way the application uses PDA is with airdropping. In this case, we want to allow users to self-mint (airdrop) a limited amount of something we own (the tokens). In those cases, the PDA has the authority to sign the minting of new tokens on our behalf.

Same as before, we're using the findProgramAddress to get a PDA:

1const cowSeed = Buffer.from(anchor.utils.bytes.utf8.encode("cow-mint-faucet"));
2 const pigSeed = Buffer.from(anchor.utils.bytes.utf8.encode("pig-mint-faucet"));
3
4 const [cowMintPda, cowMintPdaBump] = await anchor.web3.PublicKey.findProgramAddress(
5 [cowSeed],
6 program.programId);
7
8 const [pigMintPda, pigMintPdaBump] = await anchor.web3.PublicKey.findProgramAddress(
9 [pigSeed],
10 program.programId);

The airdrop code simplifies to this:

1anchor_spl::token::mint_to(
2 CpiContext::new_with_signer(
3 ctx.accounts.token_program.to_account_info(),
4 anchor_spl::token::MintTo {
5 mint: ctx.accounts.mint.to_account_info(),
6 to: ctx.accounts.destination.to_account_info(),
7 authority: ctx.accounts.mint.to_account_info(),
8 },
9 &[&[&mint_seed, &[mint_bump]]],
10 ),
11 amount,
12 )?;

Same as before, the most important thing to notice here is that the PDA itself has the authority to sign off transactions.

Putting all together.

There's a demo app deployed here. Both devnet and testnet have the app deployed. You can use the selector on the page to change between the two (if you do, remember to change what network you're pointing in your walled).

You can airdrop some SOL if you don't have any. Furthermore, you can airdrop some farm animals to start trading.

Note: Every 20 seconds, I'm pulling to an off-chain db to display the full list of offers available to all users.

Final thoughts.

This was another fun experiment with Solana. I wanted to keep everything on chain but ended up having an off-chain DB with all offers created to make them available to all users. I'll explore putting all the offers on-chain.

Overall, I'm enjoying my time playing with Solana. I'll keep experimenting and reporting back. Until the next time.

Resources

© 2024 by Fernando Mendez. All rights reserved.
Theme by LekoArts