4

Typically, when one creates a PDA, both the signing program and the owning program are the same. You have an account owned by Program A, such that:

  1. Only Program A can sign for this PDA, and
  2. Only Program A can manipulate the data on this PDA.

Is it possible to decouple this, swapping the word "Program A" for "Program B" in just one of the two numbered statements above?

1 Answer 1

6

TL;DR

Yes, it is possible.

This is how, for example, you can create system accounts capable of storing and transferring SOL with the normal system_instruction::transfer, but which can only be signed by the custom logic of a given program. An example of such behavior can be found in Marinade's Liquid Staking Program which oversees MSOL.

These Two Addresses Can Be Different:

1. The PDA's Owner

Every account, PDA or otherwise, has a specified program owner. For all intents and purposes, that program is essentially God over that account, and it can change the data as it pleases (within what the runtime allows, e.g. you cannot create lamports from nothing), regardless of signatures. And it's the only program that has this power. This is why, for example, when people initialize PDAs owned by their program, they find that their system_instruction::transfer attempts fail, and that they instead need to use try_borrow_mut_lamports. This happens because the account is owned by your program, not the system program, so the system program can't modify the data in that account, lamports included.

2. The PDA's Signing Program

Separate from the PDA owner is the program that can sign CPIs with that account. PDA signatures are verified using the caller's Program ID, not the account's owner. When the runtime verifies a PDA signature, it's just generating an address using the signing seeds and the caller's program ID, and verifying that the resultant address matches the one that was passed in.

After that, it is entirely up to a program's business logic to care about signatures in any capacity. If you make a PDA that can be signed by only by Program A, this only matters insofar as other programs care about signatures to inputs.

For example, there is no law of nature that says the to address must sign for a transfer of lamports in the system program. But without such a constraint, no user would be able to control their own SOL balance, which would be pretty pointless. You've probably come across similar design decisions in your own development. A common pattern you see everywhere is to have a PDA store an "authority" address, and require that "authority" sign to authorize changes to other data on that PDA.

It's worth mentioning that "signing program" is sort of an ad hoc term I am using, and I haven't really come across a canonical term for this concept.

The Main Requirement to Creating Accounts In General

The account being created must always sign for its own creation. Therefore only program (2), the PDA's signing program, can create a PDA.

When you create an account, the system instruction that processes it checks that account was a signer. This works for PDAs just fine, and is how Anchor initializes your PDAs under the hood (it makes your program sign a create_account CPI using the seeds you pass in).

This effectively means that if you want to create a PDA that only Program A can sign, then Program A must expose a means of creating the PDA in question. There's no way for another Program B to create it, nor for a user to create it with direct system_instruction calls, etc.

The Anchor Way to Decouple Owner and Signing Programs

    // ... inside a #[derive(Accounts)] struct
    #[account(
        seeds = [
            // ...
        ],
        // Only this program will be able to modify this PDA's data
        owner = system_program::System::id(),
        bump,
    )]
    pub sol_storage_pda: SystemAccount<'info>,

You can see where we're manually specifying an owner. Where are we specifying the signing program, then? Due to the previously described constraint on account creation, the signing program will simply be the program in which the above code resides.

Edit: There is also a seeds::program argument that you can use in your #[account] macro, allowing one to specify exactly which program is the expected signing program. This could be used to enforce that only a given program is allowed to CPI into another. Thanks to Henrye for pointing this out.

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.