1

Let's say we have a series of programs (A -> B -> C) and each one uses PDAs for which the user public keys are used as the seeds.

If a normal user calls ProgramA I think this is safe because the account struct will likely have some validation where the PDA's seed is linked to the user who must be a Signer, something like this:

#[derive(Accounts)]
pub struct ChangeUserName<'info> {
    pub user: Signer<'info>,
    #[account(mut, seeds = [b"user-stats", user.key().as_ref()], bump = user_stats.bump)]
    pub user_stats: Account<'info, UserStats>,
}

But what if a malicious user, who knows the public key, skips ProgramA and calls ProgramB that will call ProgramC with some unintended effect?

If PDAs can sign for programs themselves then anyone can call them, so would the right thing to do be to specify the user as a Signer with every program? But I'm not sure because I thought the program is the signer so I wouldn't have to do that.

2 Answers 2

4

Every PDA is derived from one Program, whose programID is part of the seed.

So let's say PDAaUW is derived from ProgramA and a user wallet's public key. Then ONLY ProgramA has write access to the PDA. Privileges might be escalated when ProgramA calls ProgramB via CPI, but ONLY in the case of CPI in that sequence. When calling ProgramB directly, ProgramB cannot write to PDAaUW.

There might be another PDA, let's call it PDAbUW, which derived from ProgramB and some user wallet's seed. Then ONLY ProgramB can access PDAb for writing.

Maybe I'm just blind to what you see here, but I can't see a potential security loophole here.

If you still do, could you please elaborate an example how an abusive scenario could look like, given ProgramA, PDAaUW, ProgramB, PDAbUW, ProgramC, PDAcUW?

6
  • I'm probably missing a lot but my understanding is that CPIs mean a program can call another program and sign without a private key. If PDAa calls PDAb, and PDAb calls PDAc (and they all affect the signer), but only PDAa requires the user to sign then what stops anonymous client from calling PDAb directly? Calling PDAb requires only public seeds so anyone should be able to call it and cause unintended consequences for the original signer? Jul 26, 2022 at 10:49
  • 1
    You can add a check in PDAb that it can be called only by PDAa and PDAc can only be called by PDAb. Since the program id of all the programs remain the same, you can check if the CPI is made through a different program whose id matches programA or programB. Through this you can make sure that even if someone calls the programB directly, it would return an error. Jul 26, 2022 at 13:01
  • 1
    The way the question was worded, it's ProgramA, ProgramB, ProgramC than can call instructions, i.e. the Programs are the executable accounts. The PDA accounts are not executable, they are pure data accounts, so they cannot call anything. Each PDA is derived from one Program, and only that Program can sign (or quasi-sign, that is) for that PDA. Jul 26, 2022 at 14:44
  • 1
    Thanks for clarifying! I can definitely see how my question makes little sense now. Programs are executable, PDA accounts are not. Jul 26, 2022 at 19:19
  • 2
    Yes, it's not the PDA account that signs for anything. The PDA accounts just hold data (and/or SOL or tokens), and it is derived from one program. That program, and only that program, can then sign transactions that access the PDA for writing and/or sending funds out of the PDA. Jul 26, 2022 at 21:29
1

Yes, every program should have its own security checks. This means checking signers, validating public keys. Even if the program or function is just intended to be called via CPI, you still need to validate the inputs.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.