Leaving info here for anyone else that might be wondering about this topic.
tldr: it would be quite hard to create an account in the space of another program's PDA space without using that program to do it.
Some quick notes on PDAs:
Program derived address allow programs to control specific
addresses, called program addresses, in such a way that no external
user can generate valid transactions with signatures for those
addresses.
Program derived address allow programs to programmatically sign for
program addresses that are present in instructions invoked via
Cross-Program Invocations.
Assuming that the goal is to create an account at a known PDA for another program, and that we know the seeds for that address, we still would not be able to create an account at that location.
Solana currently provides 2 ways to create an account through the SystemProgram:
- Using the SystemProgram::CreateAccount instruction
- Using the SystemProgram::CreateAccountWithSeed instruction
CreateAccount
Let's look at the first of these, the CreateAccount instruction.
/// Create a new account
///
/// # Account references
/// 0. `[WRITE, SIGNER]` Funding account
/// 1. `[WRITE, >>>> SIGNER <<<<]` New account
CreateAccount {
/// Number of lamports to transfer to the new account
lamports: u64,
/// Number of bytes of memory to allocate
space: u64,
/// Address of program that will own the new account
owner: Pubkey,
},
It requires a signature from the account being created. So either you'd need a private key or you'd need to be able to sign for it using invoke_signed
in an on-chain program.
However, a PDA address won't have a private key; it is bumped off the curve so the only option is to use invoke_signed
.
The issue is that you'll get a "signer privilege escalated" error if you try to sign for an account that doesn't belong to the calling program.
For example, the following will not work:
// Try to create an account using a PDA address for another program
let other_program = Pubkey::try_from("base58_key_here").unwrap();
let (__pda_address, __bump) =
// Works, but you get a totally different PDA than would be generated
// using "other_program"
//Pubkey::find_program_address(
// &[b"my_seeds_go_here"],
// &id().key());
// Does not work, results in "signer privilege escalated" error
Pubkey::find_program_address(
&[b"my_seeds_go_here"],
&other_program);
Then call invoke_signed, as follows:
// Example 1: ask system program to create a new account
invoke_signed(
&create_account(
&ctx.accounts.payer.key(),
&ctx.accounts.a.key(),
3000000,
165,
&ctx.accounts.token_program.key(),
),
&[
ctx.accounts.payer.to_account_info().clone(),
ctx.accounts.a.to_account_info().clone(),
],
&[
&[b"my_seeds_go_here", &[__bump][..]][..],
]
)?;
Or, with Anchor
// Example 2: ask system program to create a new account
let cpi_accounts = anchor_lang::system_program::CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.a.to_account_info(),
};
let cpi_context = anchor_lang::context::CpiContext::new(
ctx.accounts.system_program.to_account_info(),
cpi_accounts,
);
anchor_lang::system_program::create_account(
cpi_context.with_signer(
&[&[b"my_seeds_go_here", &[__bump][..]][..]]),
lamports,
anchor_spl::token::TokenAccount::LEN as u64,
&ctx.accounts.token_program.key(),
)?;
Once again, both these will result in signer privilege escalated
CreateAccountWithSeed
Similarly, the CreateAccountWithSeed instruction provides a method to create a new account.
Note that it does not require the direct signature of the account being created, but it does require a signature to a base account.
/// Create a new account at an address derived from a base pubkey and a seed
///
/// # Account references
/// 0. `[WRITE, SIGNER]` Funding account
/// 1. `[WRITE]` Created account
/// 2. `[SIGNER]` (optional) Base account; the account matching the base Pubkey below must be
/// provided as a signer, but may be the same as the funding account
/// and provided as account 0
CreateAccountWithSeed {
/// Base public key
base: Pubkey,
/// String of ASCII chars, no longer than `Pubkey::MAX_SEED_LEN`
seed: String,
/// Number of lamports to transfer to the new account
lamports: u64,
/// Number of bytes of memory to allocate
space: u64,
/// Owner program account address
owner: Pubkey,
},
In order to use this as an attack vector, you'd need to find a base account keypair + seeds that map to a PDA.
However, note that PDA's are specifically off the curve.