Summary
- Using the same PDA for multiple authority domains opens your program up to the possibility of users accessing data and funds that don’t belong to them
- Prevent the same PDA from being used for multiple accounts by using seeds that are user and/or domain-specific
- Use Anchor’s
seedsandbumpconstraints to validate that a PDA is derived using the expected seeds and bump
Lesson
PDA sharing refers to using the same PDA as a signer across multiple users or domains. Especially when using PDAs for signing, it may seem appropriate to use a global PDA to represent the program. However, this opens up the possibility of account validation passing but a user being able to access funds, transfers, or data not belonging to them.Insecure global PDA
In the example below, theauthority of the vault account is a PDA derived
using the mint address stored on the pool account. This PDA is passed into
the instruction as the authority account to sign for the transfer tokens from
the vault to the withdraw_destination.
Using the mint address as a seed to derive the PDA to sign for the vault is
insecure because multiple pool accounts could be created for the same vault
token account, but a different withdraw_destination. By using the mint as a
seed derive the PDA to sign for token transfers, any pool account could sign
for the transfer of tokens from a vault token account to an arbitrary
withdraw_destination.
Secure account specific PDA
One approach to create an account specific PDA is to use thewithdraw_destination as a seed to derive the PDA used as the authority of the
vault token account. This ensures the PDA signing for the CPI in the
withdraw_tokens instruction is derived using the intended
withdraw_destination token account. In other words, tokens from a vault
token account can only be withdrawn to the withdraw_destination that was
originally initialized with the pool account.
Anchor’s seeds and bump constraints
PDAs can be used as both the address of an account and allow programs to sign
for the PDAs they own.
The example below uses a PDA derived using the withdraw_destination as both
the address of the pool account and owner of the vault token account. This
means that only the pool account associated with correct vault and
withdraw_destination can be used in the withdraw_tokens instruction.
You can use Anchor’s seeds and bump constraints with the #[account(...)]
attribute to validate the pool account PDA. Anchor derives a PDA using the
seeds and bump specified and compare against the account passed into the
instruction as the pool account. The has_one constraint is used to further
ensure that only the correct accounts stored on the pool account are passed
into the instruction.
Lab
Let’s practice by creating a simple program to demonstrate how a PDA sharing can allow an attacker to withdraw tokens that don’t belong to them. this lab expands on the examples above by including the instructions to initialize the required program accounts.1. Starter
To get started, download the starter code on thestarter branch of
this repository.
The starter code includes a program with two instructions and the boilerplate
setup for the test file.
The initialize_pool instruction initializes a new TokenPool that stores a
vault, mint, withdraw_destination, and bump. The vault is a token
account where the authority is set as a PDA derived using the mint address.
The withdraw_insecure instruction will transfer tokens in the vault token
account to a withdraw_destination token account.
However, as written the seeds used for signing are not specific to the vault’s
withdraw destination, thus opening up the program to security exploits. Take a
minute to familiarize yourself with the code before continuing on.
2. Test withdraw_insecure instruction
The test file includes the code to invoke the initialize_pool instruction and
then mint 100 tokens to the vault token account. It also includes a test to
invoke the withdraw_insecure using the intended withdraw_destination. This
shows that the instructions can be used as intended.
After that, there are two more tests to show how the instructions are vulnerable
to exploit.
The first test invokes the initialize_pool instruction to create a “fake”
pool account using the same vault token account, but a different
withdraw_destination.
The second test withdraws from this pool, stealing funds from the vault.
anchor test to see that the transactions complete successfully and the
withdraw_instrucure instruction allows the vault token account to be drained
to a fake withdraw destination stored on the fake pool account.
3. Add initialize_pool_secure instruction
Now let’s add a new instruction to the program for securely initializing a pool.
This new initialize_pool_secure instruction will initialize a pool account
as a PDA derived using the withdraw_destination. It will also initialize a
vault token account with the authority set as the pool PDA.
4. Add withdraw_secure instruction
Next, add a withdraw_secure instruction. This instruction will withdraw tokens
from the vault token account to the withdraw_destination. The pool account
is validated using the seeds and bump constraints to ensure the correct PDA
account is provided. The has_one constraints check that the correct vault
and withdraw_destination token accounts are provided.
5. Test withdraw_secure instruction
Finally, return to the test file to test the withdraw_secure instruction and
show that by narrowing the scope of our PDA signing authority, we’ve removed the
vulnerability.
Before we write a test showing the vulnerability has been patched let’s write a
test that simply shows that the initialization and withdraw instructions work as
expected:
vault authority is
the pool PDA derived using the intended withdraw_destination token account,
there should no longer be a way to withdraw to an account other than the
intended withdraw_destination.
Add a test that shows you can’t call withdraw_secure with the wrong withdrawal
destination. It can use the pool and vault created in the previous test.
pool account is a PDA derived using the
withdraw_destination token account, we can’t create a fake pool account
using the same PDA. Add one more test showing that the new
initialize_pool_secure instruction won’t let an attacker put in the wrong
vault.
anchor test and to see that the new instructions don’t allow an attacker
to withdraw from a vault that isn’t theirs.
solution branch of
the same repository.

