Understand the need for consistent PDA calculation by storing and reusuing the canonical bump.
create_program_address
function derives a PDA without searching for the canonical bump. This
means there are multiple valid bumps, all of which will produce different
addresses.find_program_address
ensures that the highest valid bump, or canonical bump, is used for the
derivation, thus creating a deterministic way to find an address given
specific seeds.seeds
and bump
constraint to
ensure that PDA derivations in the account validation struct always use the
canonical bumpbump = <some_bump>
constraint when verifying the address of a PDAfind_program_address
can be expensive, best practice is to store the
derived bump in an account’s data field to be referenced later on when
re-deriving the address for verification
create_program_address
is a valid PDA. The canonical bump is the highest bump value that produces a
valid PDA. The standard in Nexis Native Chain is to always use the canonical bump when
deriving PDAs, both for security and convenience.
create_program_address
create_program_address
function will produce a valid
PDA about 50% of the time. The bump seed is an additional byte added as a seed
to “bump” the derived address into valid territory. Since there are 256 possible
bump seeds and the function produces valid PDAs approximately 50% of the time,
there are many valid bumps for a given set of input seeds.
You can imagine that this could cause confusion for locating accounts when using
seeds as a way of mapping between known pieces of information to accounts. Using
the canonical bump as the standard ensures that you can always find the right
account. More importantly, it avoids security exploits caused by the open-ended
nature of allowing multiple bumps.
In the example below, the set_value
instruction uses a bump
that was passed
in as instruction data to derive a PDA. The instruction then derives the PDA
using create_program_address
function and checks that the address
matches
the public key of the data
account.
find_program_address
find_program_address
to derive the PDA.
The
find_program_address
always uses the canonical bump. This function iterates through calling
create_program_address
, starting with a bump of 255 and decrementing the bump
by one with each iteration. As soon as a valid address is found, the function
returns both the derived PDA and the canonical bump used to derive it.
This ensures a one-to-one mapping between your input seeds and the address they
produce.
seeds
and bump
constraintsseeds
and bump
constraints. These can even be combined with the
init
constraint to initialize the account at the intended address. To protect
the program from the vulnerability we’ve been discussing throughout this lesson,
Anchor does not even allow you to initialize an account at a PDA using anything
but the canonical bump. Instead, it uses find_program_address
to derive the
PDA and subsequently performs the initialization.
seeds
and bump
constraints. This simply rederives the PDA and compares the
derived address with the address of the account passed in.
In this scenario, Anchor does allow you to specify the bump to use to derive
the PDA with bump = <some_bump>
. The intent here is not for you to use
arbitrary bumps, but rather to let you optimize your program. The iterative
nature of find_program_address
makes it expensive, so best practice is to
store the canonical bump in the PDA account’s data upon initializing a PDA,
allowing you to reference the bump stored when validating the PDA in subsequent
instructions.
When you specify the bump to use, Anchor uses create_program_address
with the
provided bump instead of find_program_address
. This pattern of storing the
bump in the account data ensures that your program always uses the canonical
bump without degrading performance.
bump
constraint, Anchor will still use
find_program_address
to derive the PDA using the canonical bump. As a
consequence, your instruction will incur a variable amount of compute budget.
Programs that are already at risk of exceeding their compute budget should use
this with care since there is a chance that the program’s budget may be
occasionally and unpredictably exceeded.
On the other hand, if you only need to verify the address of a PDA passed in
without initializing an account, you’ll be forced to either let Anchor derive
the canonical bump or expose your program to unecessary risks. In that case,
please use the canonical bump despite the slight mark against performance.
starter
branch of
this repository.
Notice that there are two instructions on the program and a single test in the
tests
directory.
The instructions on the program are:
create_user_insecure
claim_insecure
create_user_insecure
instruction simply creates a new account at a PDA
derived using the signer’s public key and a passed-in bump.
The claim_insecure
instruction mints 10 tokens to the user and then marks the
account’s rewards as claimed so that they can’t claim again.
However, the program doesn’t explicitly check that the PDAs in question are
using the canonical bump.
Have a look at the program to understand what it does before proceeding.
user
PDA to use the
canonical bump, an attacker can create multiple accounts per wallet and claim
more rewards than should be allowed.
The test in the tests
directory creates a new keypair called attacker
to
represent an attacker. It then loops through all possible bumps and calls
create_user_insecure
and claim_insecure
. By the end, the test expects that
the attacker has been able to claim rewards multiple times and has earned more
than the 10 tokens allotted per user.
anchor test
to see that this test passes, showing that the attacker is
successful. Since the test calles the instructions for every valid bump, it
takes a bit to run, so be patient.
create_user_secure
claim_secure
UserSecure
. This new type will add the canonical bump as a field on
the struct.
create_user_secure
instruction simply needs to set the auth
, bump
and
rewards_claimed
fields on the user
account data.
claim_secure
instruction needs to mint 10 tokens to the user and set the
user
account’s rewards_claimed
field to true
.
solution
branch of
the same repository.