DEV Community

Tobi Ayinmiro
Tobi Ayinmiro

Posted on

The CPI Mental Model I Wish I Had on Day 71

Cross-Program Invocation isn't magic. It's just a function call with a guest list.

When I first came across Cross-Program Invocations (CPIs) in Solana, I thought they were some special feature that required a completely different way of thinking. Every example introduced CpiContext, account structs, and signer seeds all at once, and I couldn't tell which pieces were essential and which were just Anchor boilerplate.

After spending the last few days building programs that call the System Program, sign for PDAs, and even call another Anchor program, I realized that the whole idea is much simpler than I originally thought.

My mental model is this:

A CPI is just a function call with three things: the program you're calling, the accounts that program expects, and who is authorized to sign.

Once that clicked, every CPI example started looking almost identical.

The Mental Model

Every Cross-Program Invocation has three moving parts.

1. The program being called

Every CPI targets another on-chain program. That might be the System Program, the Token-2022 Program, or another program you wrote yourself. Anchor identifies that program by its program ID.

2. The accounts that program needs

Programs cannot magically access accounts. Your program has to pass every required account to the program you're calling.

If the called program expects a from account, a to account, and a system program account, you must provide all three.

This is why CPIs often feel like filling out a checklist rather than writing complicated logic.

3. The signer

This was the piece that confused me the most.

Sometimes the signer is simply the wallet that signed the transaction.

Other times, the signer is a Program Derived Address (PDA). Since PDAs don't have private keys, the runtime lets the program "sign" for them—but only if the signer seeds exactly match the PDA's seeds.

That means the seeds used in with_signer() must match the seeds declared in your #[account(seeds = ..., bump)] constraint. If they don't match, the runtime rejects the CPI.

A Small Example

One of the first examples that made this pattern click was calling another Anchor program.

pub fn bump(ctx: Context<Bump>) -> Result<()> {
    let cpi_ctx = CpiContext::new(
        ctx.accounts.counter_program.key(),
        Increment {
            tally: ctx.accounts.tally.to_account_info(),
        },
    );

    cpi::increment(cpi_ctx)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Even though this looked unfamiliar at first, it's really just following the same three-part pattern.

  • The program being called is counter_program.
  • The account it needs is tally.
  • No PDA is signing here, so a regular CpiContext::new() is enough.

Earlier in the week I used the exact same pattern to call the System Program to transfer SOL. The only difference was the destination program and the accounts passed into the context.

The structure stayed exactly the same.

What Tripped Me Up

The biggest lesson from building a PDA-backed vault wasn't actually writing the transfer it was understanding why with_signer() exists.

When withdrawing from my vault, the vault account itself had to authorize the transfer. But the vault is a PDA, and PDAs don't have private keys.

The solution was:

.with_signer(signer_seeds)
Enter fullscreen mode Exit fullscreen mode

where signer_seeds matched the PDA declaration:

#[account(
    seeds = [b"vault", user.key().as_ref()],
    bump,
)]
Enter fullscreen mode Exit fullscreen mode

That finally made the idea click.

The program isn't inventing a signature. It's proving to the runtime that it controls the PDA by supplying the exact seeds that generated it.

Once I understood that, PDA signing stopped feeling mysterious.

Final Thoughts

If I could go back to Day 71, I wouldn't start by memorizing CpiContext or invoke_signed.

I'd start with the question:

Who am I calling, what accounts does that program need, and who is allowed to sign?

Everything else is just implementation details.

Whether you're calling the System Program, Token-2022, or another Anchor program, that same mental model keeps showing up.

This post draws on my work from Days 73-74 of #100DaysOfSolana, covering PDA-signed transfers and Cross-Program Invocations between Anchor programs.

Top comments (0)