Using PDAs and SPL Token in Anchor

Let’s build a Safe Token transfer app!

Solana Beach

Like many other web3 devs, I’ve slowly been getting into Solana. First as a tester of the technology, and now I’m getting deeper into the development aspect. During the last 2–3 years at 0x, most of the challenges I’ve been exposed are Ethereum-specific: EVM, ABI-encoding, gas auctions, Solidity contracts, block reorgs, and so forth. Some of these concepts map over nicely to Solana, but a majority of concepts require a mindset shift. The technology behind Proof of History and being able to parallelize reads while still locking on writes make things more scalable than a consensus-based model.

Compared to many other chains that “mount” an EVM to be backward-compatible with Ethereum, and compromise on some of the most fundamental aspects of a blockchain (like security), the Solana blockchain has to re-think fundamentals at a lower level to reduce the price of its transactions and to provide the best user experience.

Introduction

As a Software Engineer by trade and profession, I’ve really enjoyed writing Solana programs, it’s challenging and fun, and the community around it is nice and friendly. I’ve learned to appreciate the development choices around some of the Solana complexity, and this has pushed me to help fill some of the gaps around program development. Today I want to walk us through some recent exciting things that I’ve learned in Anchor (Solana’s popular development framework):

  • CPI (Cross-program invocation)
  • PDAs (Program-derived accounts)
  • Token program within your Anchor program.

Disclaimer

This article will require prior knowledge of Solana and Anchor. There are plenty of incredible resources out there that do a great job describing how Anchor, Solana accounts, and general Anchor development. I would do a disservice to the authors of these resources by starting from scratch and repeating their explanations, I would rather build on top of their work and reference their articles during our path. I suggest you go and read up on these articles that will give you a head start on everything to know. Any new concept that may be covered, however, will be explained.

Important links

Another obvious disclaimer that I want to share is that the code below has not been audited by any means! It’s not production-ready, and it may contain bugs. The content below is purely educational and, because your attention is pea-sized dear anon, I will be focusing exclusively on the core concepts. I want to share what I learned on CPI invocation, Token accounts, Associated Token accounts, and Solana programs in general. This was a fun application that involves all of the above.

Building a Safe Token Transfer app

What is this all about?

We want to build the “Safe Token Transfer app”, that prevents funds from being lost.

What is this all about?

When someone wants to transfer tokens to a particular user, they do it in 2 steps to enforce safety. First, they deposit those tokens into an escrow token account that is controlled by our program. Second, the final receiver of the tokens confirms the withdrawal and receives the funds from the escrow.

Why do we want to build this?

Crypto payments are scary. In Ethereum and Bitcoin, you can send Ether and any ERC20/721 token to any 20-byte encoded hex string. This means that you may want to send some DAI to a friend, but by mistake you may send that DAI to someone else or, even worse, no one at all!

This app is an attempt to solve the core problem of sending funds that get stuck forever. The sender of funds has an opportunity to safely withdraw their tokens in case of issues, and the receiver ensures that they have access to the keys needed to receive the tokens.

The high-level user flow

Our program features 2 users, Alice and Bob.

  • Alice wants to pay Bob
  • Bob knows that they will be paid by Alice

👇 Our story between Alice and Bob goes like this:

  1. Alice needs to pay Bob 10,000 USDC as prize money for a Hackathon that they won. Alice knows Bob’s Solana (EOA) address
  2. Alice initiates a “Safe Payment” with Bob because she wants to have peace of mind and avoid any “fat finger” scenario.
  3. Alice funds the Safe Payment and creates a new Safe Pay ID that can be used to reference her specific payment intent (send 10,000 USDC to Bob)
  4. Alice sends Bob a unique URL (that references the new Safe Pay ID). Bob can use that Safe Payment to redeem his tokens.
  5. If Alice fat-fingered Bob’s address, or if Bob cannot find his keys anymore, Alice can safely pull back her Safe Payment before the transfer is complete, and initiate a new Safe Payment towards Bob.

Breaking down our user flow into a set of instructions

  1. Initialize and deposit instruction invoked by Alice: Creates a new Safe Payment intent and defines the parameters for it, initialized the escrow wallet owned by the Safe Transfer program. moves the funds from Alice’s wallet to the escrow wallet
  2. Complete instruction invoked by Bob: moves funds from the escrow wallet to Bob’s wallet
  3. PullBack instruction invoked by Alice: moves funds from the escrow wallet to Alice’s wallet

Defining the state

In order to keep track of what section of the flow we are in, and the various parameters being initialized, we need to have an account to store the state.

In Solana, the state is decoupled from logic (or program execution) and is stored in an account. In our case, the state is created by Alice as soon as a new Safe Payment is initialized. So there should be 1 new state account for each Safe Payment. Let’s think of the state as the core bit of storage where we keep track of what is happening, who are the actors involved, and what parameters are set.

Initialize and Deposit instruction

What does it do?
Initializes the State account and initializes the escrow wallet account, and moves the funds from Alice’s wallet to the escrow wallet

Why is it needed?
Alice uses this instruction when they have to pay Bob. Funds go from Alice’s wallet to an escrow wallet.

📝 Parameters
- amount_tokens:
unsigned integer representing how many tokens Alice wants to give to Bob
- application_idx: a semi-unique identifier (like a timestamp) that serves to make the payment instance unique. This can be represented by a UNIX timestamp

🖋 Signers: Alice

You may notice that our constructor accepts 2 surprising parameters. These parameters are passed in the instruction set in order to compute PDAs for the accounts application_state and escrow_wallet_state. Anchor recognizes that those two are PDA accounts because we specify seeds and bump for them in the instruction set. You may be asking, why should these 2 accounts be PDAs?

PDAs are accounts that can only be signed by a program. Think of a PDA as an account whose private key was only known to the program. In our case, this means that updating the program state, and authority over the escrow token wallet, is possible only through the program that we are building which is gated by the business logic that we code up.
As you can see, these PDAs have an init statement, which means that Anchor will automagically create these accounts through a SystemProgram call.

Complete instruction

What does it do?
Moves the funds from the previously-funded escrow wallet to Bob’s wallet

Why is it needed?
Requiring Bob to explicitly withdraw funds to their own account ensures they have the key to access the funds.

🖋 Signers: Bob

Complete finally brings Bob into the picture! One important difference between the last 2 accounts is that Alice is not a signer anymore, but now Bob is the signer.
Another important note is the presence of AssociatedToken the program. This program defines the convention and provides the mechanism for finding the unique wallet address to USDC the user holds. Another benefit AssociatedToken is that it allows our program to send tokens to Bob even if they do not yet have a USDC token account (hence, the init_if_needed clause during the account definition).

Factoring out a small payment function

Paying out of the escrow can be as simple as calling the transfer function from spl_token once again, but we should be doing more than this if we want to do things correctly. Since the escrow wallet is created specifically for the Safe Payment instance in question, we should clear it up (close the wallet) once the payment is complete and there is no use for that account anymore. By closing the wallet, Alice can get back the lamports she put as rent, and we can avoid polluting the blockchain.

And below is the instruction code:

Pull Back instruction

What does it do?
Moves the funds from the escrow wallet to Alice’s wallet

Why is it needed?
If Bob is unable to access the funds in escrow, Alice can still pull the funds back and initialize a new Safe Payment

🖋 Signers: Alice

Source Code

The above snippets have been extracted from my GitHub repo. The repository has a few unit tests that will also show you how to appropriately test Anchor apps.

Let’s connect!

Want to learn more about Anchor development? I’m hoping to post more content in the future. You can follow me on https://twitter.com/pirosb3 and be the first to know about the content I create and share.

I also run Office Hours — If you want to chat about your project, and you need someone to help you grow in the space (from SWE to Crypto stuff), feel free to reach out!

Thank You!

Thank you to everyone who has encouraged me to enter the space, and who has provided support from the sidelines!

  • Phil Liao, my colleague and partner in crime at 0x
  • Nader Dabit, founder of Developer DAO
  • King Maven, for introducing me to many community members
  • Chase Barker, for providing so much useful information and being so responsive in DMs!