Program derived addresses (PDAs) are used in virtually all Solana smart contracts. While excellent resources (e.g., the Solana Cookbook) exist for understanding PDAs, there is one important caveat deserving special attention:
PDAs of a Solana program can have the same seeds with multiple valid bumps, giving multiple different addresses (quote from tweet by @armaniferrante)
This has a crucial security implication: PDAs can be faked (by providing different bump seeds) if their bump seeds are not validated.
In other words, please note the following:
Here is an example:
The output:
Let’s first understand how PDAs are computed. The source code of Pubkey::find_program_address is shown below:
It takes two user-supplied inputs:
and it returns
Specifically, it calls try_find_program_address, in which bump_seed is introduced:
In the above, line 479, bump_seed is a u8 variable with a value ranging between 0 to 255 (std::u8::MAX).
On lines 480–491, in a loop from 255 to 0, bump_seed is appended to the input seeds to call Pubkey::create_program_address (line 484):
In create_program_address, it basically performs a set of hash operations over the input seeds and program_id to compute a key (line 579 below):
It then verifies if the computed key is a valid PDA or not, i.e., by checking if the key lies on the ed25519 elliptic curve or not (line 581 bytes_are_curve_point)
If the computed key lies on the ed25519 elliptic curve, then it is a valid public key, thus not a valid PDA and an error will be returned (line 582).
(Assuming inputs are random) There is a ~50% chance that the computed key lies on the curve, so create_program_address will return error and the loop in try_find_program_address will continue with a new bump_seed.
When create_program_address returns a valid PDA, the corresponding bump_seed in try_find_program_address will also be returned.
Now, it should be clear that given the same seeds and program_id:
An attacker may create a fake PDA with the same seeds and program_id but a different bump seed from the intended PDA.
Consider an example below:
The deposit_account is a PDA that may be faked because its bump seed could be arbitrary, i.e., validated with an external data (bump).
If the faked PDA can pass an instruction’s internal checks, then it could be used to camouflage the intended PDA, e.g., transfer funds based on the fake PDA’s state rather than the intended state.
Sec3 Pro is the premier security analysis service for Solana smart contracts.
To detect such unvalidated PDA vulnerabilities. The tool first locates all the user-supplied PDAs in the smart smart including those declared with Anchor macros. The tool then verifies if the bump seed of every PDA is properly validated. E.g., with an equality constraint relating the bump seed to a bump returned by Pubkey::find_program_address.
If any PDA bump seed is not validated or it can be controlled by an external input, then the tool flags a potential vulnerability. The following shows a screenshot of BumpSeedNotValidated issue reported by sec3 Pro in the previous sample code. sec3 Pro is available at https://pro.sec3.dev.
There are two common ways to validate PDA bump seeds:
For Approach 2, in Anchor, use bump=<stored_valid_bump>. For example, bump=multisig.nonce where nonce is the valid bump stored in the multisig account when the account is created:
sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with GitHub to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.
To learn more about sec3, please visit https://www.sec3.dev