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

fndomendez

Fernando Mendez

Posted on December 13, 2021

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

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

demo app home

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).

    const offer = anchor.web3.Keypair.generate();
    const [escrowedTokensOfOfferMaker, escrowedTokensOfOfferMakerBump] = await anchor.web3.PublicKey.findProgramAddress(
      [offer.publicKey.toBuffer()],
      program.programId
    )
Enter fullscreen mode Exit fullscreen mode

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:

    anchor_spl::token::transfer(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            anchor_spl::token::Transfer {
                from: ctx
                    .accounts
                    .token_account_from_who_made_the_offer
                    .to_account_info(),
                to: ctx
                    .accounts
                    .escrowed_tokens_of_offer_maker
                    .to_account_info(),
                authority: ctx.accounts.who_made_the_offer.to_account_info(),
            },
        ),
        im_offering_this_much,
    )
Enter fullscreen mode Exit fullscreen mode

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:

    // Transfer what's on the escrowed account to the offer reciever.
    anchor_spl::token::transfer(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            anchor_spl::token::Transfer {
                from: ctx
                    .accounts
                    .escrowed_tokens_of_offer_maker
                    .to_account_info(),
                to: ctx
                    .accounts
                    .where_the_escrowed_account_was_funded_from
                    .to_account_info(),
                authority: ctx
                    .accounts
                    .escrowed_tokens_of_offer_maker
                    .to_account_info(),
            },
            &[&[
                ctx.accounts.offer.key().as_ref(),
                &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
            ]],
        ),
        ctx.accounts.escrowed_tokens_of_offer_maker.amount,
    )?;

    // Close the escrow account
    anchor_spl::token::close_account(CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        anchor_spl::token::CloseAccount {
            account: ctx
                .accounts
                .escrowed_tokens_of_offer_maker
                .to_account_info(),
            destination: ctx.accounts.who_made_the_offer.to_account_info(),
            authority: ctx
                .accounts
                .escrowed_tokens_of_offer_maker
                .to_account_info(),
        },
        &[&[
            ctx.accounts.offer.key().as_ref(),
            &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
        ]],
    ))
Enter fullscreen mode Exit fullscreen mode

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:

        // Transfer token to who started the offer
        anchor_spl::token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token::Transfer {
                    from: ctx
                        .accounts
                        .account_holding_what_receiver_will_give
                        .to_account_info(),
                    to: ctx
                        .accounts
                        .account_holding_what_maker_will_get
                        .to_account_info(),
                    authority: ctx.accounts.who_is_taking_the_offer.to_account_info(),
                },
            ),
            ctx.accounts.offer.amount_received_if_offer_accepted,
        )?;

        // Transfer what's on the escrowed account to the offer reciever.
        anchor_spl::token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token::Transfer {
                    from: ctx
                        .accounts
                        .escrowed_tokens_of_offer_maker
                        .to_account_info(),
                    to: ctx
                        .accounts
                        .account_holding_what_receiver_will_get
                        .to_account_info(),
                    authority: ctx
                        .accounts
                        .escrowed_tokens_of_offer_maker
                        .to_account_info(),
                },
                &[&[
                    ctx.accounts.offer.key().as_ref(),
                    &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
                ]],
            ),
            ctx.accounts.escrowed_tokens_of_offer_maker.amount,
        )?;

        // Close the escrow account
        anchor_spl::token::close_account(CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            anchor_spl::token::CloseAccount {
                account: ctx
                    .accounts
                    .escrowed_tokens_of_offer_maker
                    .to_account_info(),
                destination: ctx.accounts.who_made_the_offer.to_account_info(),
                authority: ctx
                    .accounts
                    .escrowed_tokens_of_offer_maker
                    .to_account_info(),
            },
            &[&[
                ctx.accounts.offer.key().as_ref(),
                &[ctx.accounts.offer.escrowed_tokens_of_offer_maker_bump],
            ]],
        ))
Enter fullscreen mode Exit fullscreen mode

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:

    const cowSeed = Buffer.from(anchor.utils.bytes.utf8.encode("cow-mint-faucet"));
    const pigSeed = Buffer.from(anchor.utils.bytes.utf8.encode("pig-mint-faucet"));

    const [cowMintPda, cowMintPdaBump] = await anchor.web3.PublicKey.findProgramAddress(
      [cowSeed],
      program.programId);

    const [pigMintPda, pigMintPdaBump] = await anchor.web3.PublicKey.findProgramAddress(
      [pigSeed],
      program.programId);

Enter fullscreen mode Exit fullscreen mode

The airdrop code simplifies to this:

    anchor_spl::token::mint_to(
        CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            anchor_spl::token::MintTo {
                mint: ctx.accounts.mint.to_account_info(),
                to: ctx.accounts.destination.to_account_info(),
                authority: ctx.accounts.mint.to_account_info(),
            },
            &[&[&mint_seed, &[mint_bump]]],
        ),
        amount,
    )?;
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
fndomendez
Fernando Mendez

Posted on December 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related