Arc 11 covered Days 71–77 of Epoch 3, and it was all about Cross-Program Invocations.
In Arc 9, we wrote our first Solana program.
In Arc 10, we gave that program a more useful state model with Program Derived Addresses.
But our programs were still mostly working alone.
They could read and update their own accounts, enforce their own constraints, and respond to instructions sent by a client. They could not directly change state owned by another program or bypass the rules that program enforced.
That is an important part of Solana’s security model.
The System Program owns the rules for creating accounts and transferring lamports.
Token-2022 owns the rules for mints, token accounts, supply, and mint authorities.
If our program needs one of those capabilities, it calls the program that owns the operation.
That call is a Cross-Program Invocation, or CPI.
The Web2 comparison is a service-to-service API call. One service sends a request through another service’s public interface, and the receiving service applies its own rules.
A CPI works in a similar way, with one important difference: the outer and inner instructions execute as part of the same Solana transaction. If the inner call fails, the state changes made by the outer instruction are rolled back too.
That combination of clear program boundaries and atomic execution is what makes Solana programs composable.
Our first CPI called the System Program
The arc began with the smallest useful CPI we could build.
Our Anchor program accepted a sender, a recipient, and an amount of SOL to transfer.
But the program did not edit the sender’s balance directly.
Instead, it called the System Program’s transfer instruction.
That distinction matters.
Accounts on Solana are owned by programs, and the owner program controls how their data may be changed. Our program could not simply reproduce the effect of a System Program transfer by adjusting balances itself.
It had to ask the System Program to perform the operation.
Every CPI needs three things:
- the program being called
- the accounts that program expects
- the authority required to approve the action
For the SOL transfer, the called program was the System Program.
The required accounts were the sender and recipient.
The authority came from the sender, who had signed the original transaction.
Our program assembled those pieces and made the inner call. The System Program then applied its own rules and either accepted or rejected the transfer.
That gave us the basic CPI model we used throughout the arc.
The caller requests an operation.
The callee validates the request.
The callee remains responsible for the state it owns.
Signer authority can flow into an inner call
The sender in that first exercise was a normal wallet signer.
The wallet signed the outer transaction sent by the client. When our program called the System Program, that signer privilege was also available to the inner instruction.
The CPI did not invent new authority.
It passed along authority that already existed in the transaction.
The same applies to writable accounts. A program cannot make an account writable during a CPI if the original transaction supplied it as read-only.
That gives us an important boundary:
A CPI cannot arbitrarily escalate account privileges.
Writable access must already have been granted by the transaction. Signer authority must come either from an existing signer or from a PDA controlled by the calling program.
That second form of authority became the major step forward later in the arc.
Token-2022 followed the same pattern
The next exercise moved from transferring SOL to minting tokens.
Our program called Token-2022’s mint_to instruction to increase a mint’s supply and deposit the new tokens into a destination token account.
The accounts were different, but the CPI model stayed the same.
The call needed:
- the Token-2022 program
- the mint
- the destination token account
- the mint authority
The mint authority was still a regular wallet signer, so its signature flowed from the outer transaction into the Token-2022 instruction.
Token-2022 then applied its own rules.
It checked that the mint and destination accounts were valid, that the supplied authority matched the mint’s recorded authority, and that the authority had signed.
Our program did not need to recreate any token logic.
It only needed to assemble a valid request.
That is the larger value of learning the CPI pattern.
Once we understand the combination of program, accounts, and authority, existing Solana programs become building blocks we can call from our own code.
The System Program already knows how to transfer SOL.
Token-2022 already knows how to manage token supply.
Our program can use those capabilities while each called program keeps control of its own rules.
A CPI still crosses a real program boundary
Anchor’s CPI helpers can make the interaction look similar to calling a local function.
But the called program does not simply trust the caller.
It receives an instruction, examines the supplied accounts, checks their privileges and relationships, and runs its own logic.
The caller decides what it wants to request.
The callee decides whether that request is valid.
For the System Program, that means enforcing the rules for lamport transfers.
For Token-2022, it means enforcing the rules for mints and token accounts.
For another Anchor program, it means applying that program’s account constraints and instruction logic.
The inner program is therefore more than a shared code library.
It is a separate on-chain authority with its own state and security boundary.
That is what makes composition useful. Programs can depend on one another without surrendering control of the accounts they own.
PDA signing gave the program its own authority
The first two CPIs used wallet signers.
That gave us a useful baseline: the user signed the outer transaction, and the inner program recognised the same authority.
The next challenge introduced a different situation.
We built a per-user SOL vault at a PDA.
Each user had a predictable vault address derived from the program’s seeds.
Depositing into the vault was straightforward. The user controlled the source account and signed the transaction, so that authority could flow into the inner transfer.
Withdrawing from the vault raised a harder question.
The vault was a PDA.
It had no private key and could not sign a transaction in the way a wallet could.
The answer was a PDA-signed CPI.
When the program made the inner call, it supplied the original seeds and canonical bump used to derive the vault.
The Solana runtime combined those values with the calling program’s ID and derived the address again.
If the result matched the vault account, the runtime treated that PDA as a signer for the inner invocation.
The program never obtained or stored a private key for the PDA.
It proved authority by reproducing the derivation that created the address.
That is the core of PDA signing.
A program can control an on-chain account or asset through a deterministic address, then authorise actions for it only when its own instruction logic allows those actions.
Signer seeds are not secrets
The phrase “signer seeds” can make the seeds sound like passwords.
They are not.
Anyone can inspect the program and see the seed scheme. The bump may also be visible in account data or derived again by a client.
That does not let another user or program impersonate the PDA.
The program ID is part of the derivation.
Another program could use the same seed values, but its own program ID would produce a different address.
The runtime only grants signer privilege when the seeds, bump, calling program ID, and account address all match.
The security comes from that relationship, along with the program logic that controls when the signer seeds are used.
This gives a program its own deterministic on-chain identity without introducing another private key that someone has to create, protect, and rotate.
A PDA can hold SOL, control a token mint, own a token account, or act as an authority for another program.
The program decides when that authority may be exercised.
Programs can call other custom programs
The first exercises called established Solana programs.
Day 74 took the next step: one custom Anchor program called another.
The first program owned a tally account and exposed an instruction that incremented it.
The second program invoked that instruction through a CPI.
The client sent a transaction to the caller program. The caller then invoked the counter program, which updated the account it owned.
The client never called the counter instruction directly, but the tally still increased.
That demonstrated an important boundary.
The caller program could not edit the counter program’s account itself.
It had to request the update through the counter program’s public instruction.
The counter program remained responsible for validating the accounts and deciding whether the update was allowed.
That is how larger on-chain systems can be assembled from smaller programs.
One program can provide a focused capability.
Another can use that capability as part of a wider workflow.
The callee remains in control of its state, while the caller gains access to its public behaviour.
The IDL becomes a contract between programs
To call another Anchor program, the caller needs to understand its public interface.
It needs to know:
- the program ID
- the available instructions
- the arguments those instructions accept
- the accounts they require
- the account and data types they expose
Anchor describes that interface in an IDL.
The caller can use the callee’s IDL to generate typed CPI bindings. It does not need to copy or compile the callee’s source code into its own program.
That is similar to using an API specification to call another service.
The caller depends on the published contract rather than the internal implementation.
The callee can refactor its code while keeping callers compatible, provided that its external instruction and account interface remains stable.
But changes to that interface can break integrations.
Renaming an instruction, changing its arguments, adding required accounts, or altering an account layout may affect programs and clients that already depend on it.
Once other programs compose with ours, the IDL is no longer only a development convenience.
It becomes part of the interface other software expects us to preserve.
The whole call stack remains atomic
A CPI does not create a separate transaction.
The inner instruction executes as part of the transaction that called the outer program.
Consider an instruction that:
- Updates one of its own accounts.
- Calls Token-2022 to mint tokens.
- Updates another account after the mint succeeds.
If Token-2022 rejects the mint, the entire transaction fails.
The state update performed before the CPI does not remain on-chain.
There is no partially completed result where the caller records that the operation succeeded but the token mint did not change.
Everything succeeds together or everything is rolled back.
Anyone who has written compensation logic or reconciliation jobs for distributed workflows will recognise why that matters.
It allows a Solana program to combine operations across several programs without having to repair state when one step in the middle fails.
It also means that an error deep in a chain of CPIs can cause every preceding change in the transaction to be reverted.
Breaking CPIs showed where integrations fail
Once the working paths were in place, we deliberately broke them.
We tested three common categories of failure:
- incorrect PDA signer seeds or bump
- missing or incorrect accounts
- the wrong program ID
Each failure exposed a different part of the contract between caller and callee.
Wrong signer seeds
For a PDA-signed CPI, the runtime derives the PDA again using the supplied seeds, bump, and calling program ID.
If any of those inputs are wrong, the result does not match the account expected to sign.
The inner instruction then sees that its required authority is missing.
A one-byte difference is enough to break the derivation.
When debugging, the key question is:
Do these exact seeds and this bump derive the exact PDA being supplied as the authority?
If they do not, the runtime cannot grant signer privilege.
Wrong accounts
Every instruction expects a particular set of accounts.
Those accounts may need to be:
- writable
- signers
- owned by a particular program
- derived from particular seeds
- linked through a stored relationship
- associated with a particular mint or authority
Passing the wrong account often produces an Anchor constraint error.
Those errors can identify both the account and the validation that failed.
The callee’s accounts struct is therefore more than setup code.
It is the contract the caller must satisfy.
When a CPI fails, comparing the supplied accounts against that contract is often the fastest route to the cause.
Wrong program ID
The caller must also invoke the correct program.
Typed program accounts help protect this boundary. Anchor can validate the supplied program account before attempting the CPI and reject an unexpected ID during account validation.
With a loosely typed program account, that protection may not exist.
The runtime can invoke the program it was given, even when the instruction data was intended for someone else.
The error then comes from the unintended program trying to interpret an instruction it does not recognise.
This is one reason typed accounts are valuable.
They turn a confusing downstream failure into a clearer validation failure at the program boundary.
Transaction logs reveal the call stack
CPI debugging becomes easier when we stop looking only at the final error code.
The transaction logs show the sequence of execution:
- the outer program begins
- it reaches the CPI
- the inner program is invoked
- the inner program validates its accounts
- an instruction succeeds or fails
- the result propagates back through the call stack
That sequence tells us which program was running when the failure occurred.
Most CPI bugs come from one of two places:
- The caller assembled the request incorrectly.
- The callee enforced a rule that the supplied request did not satisfy.
Useful questions include:
- Which program produced the error?
- Which inner instruction was being called?
- Did the caller provide every required account?
- Were the signer and writable privileges correct?
- Did the signer seeds derive the expected PDA?
- Did Anchor identify a failed constraint?
- Was the intended program actually invoked?
A CPI failure is usually the on-chain equivalent of an API request that did not match the receiving service’s contract.
The logs help us find the point where that mismatch occurred.
Writing the explanation forced us to choose the important part
The final two days moved from implementation to communication.
CPIs include enough moving parts that it is tempting to explain all of them at once:
- program ownership
- nested instructions
- account privileges
- wallet signers
- PDA signing
- IDLs
- atomicity
- transaction logs
That can produce an explanation that is technically complete but hard to follow.
The challenge instead asked us to choose one clear thesis.
One possible framing was:
A CPI is a function call with a guest list.
The called instruction needs the right program, the right accounts, and the right authority.
Another focused on the major conceptual jump:
Signer seeds are how a program proves control of its PDA.
The seeds are not secret credentials. They let the runtime reconstruct the PDA under the calling program’s ID and grant it signer privilege for one inner invocation.
The public explanation needed:
- one clear mental model
- one small example
- the three ingredients of a CPI
- one real error encountered during the week
- evidence from the working program
That evidence might be passing test output, a GitHub repository, a concise implementation excerpt, or a devnet transaction.
As in the previous arcs, publishing the work was part of the learning process.
Explaining where authority comes from, which program owns the operation, and why a broken CPI fails reveals gaps that successful tests alone may not expose.
What Arc 11 taught us
Arc 11 moved us from isolated programs to composed systems.
By the end of the arc, we had seen how to:
- call the System Program from an Anchor instruction
- transfer SOL through a CPI
- call Token-2022 to mint tokens
- supply the program, accounts, and authority required by a callee
- pass wallet signer authority into an inner instruction
- understand the limits on signer and writable privileges
- authorise an inner call with a PDA
- control on-chain assets without storing a private key
- call one custom Anchor program from another
- use an IDL as the contract between programs
- treat instruction and account changes as compatibility decisions
- break CPIs with incorrect seeds, accounts, and program IDs
- read transaction logs as a record of the program call stack
- explain program composition through a clear, concrete example
The enduring lesson is that Solana programs are interoperable pieces of infrastructure.
A program that needs to transfer SOL can call the System Program.
A program that needs to mint tokens can call Token-2022.
A program that needs behaviour exposed by another custom program can call that program through its public instruction interface.
Each callee keeps control of its accounts and applies its own authorisation rules.
The caller gains the capability without copying the implementation or bypassing the program that owns the state.
That is what CPIs make possible:
Solana programs can combine trusted on-chain capabilities into one atomic transaction.
Revisit the Arc 11 challenges
- Day 71: Call the System Program from your own program to transfer SOL
- Day 72: Use a CPI to mint tokens through Token-2022
- Day 73: Build a PDA-controlled SOL vault and sign a CPI with seeds
- Day 74: Call one custom Anchor program from another through its IDL
- Day 75: Break working CPIs and debug signer, account, and program errors
- Day 76: Write a clear public explanation of how Cross-Program Invocations work
- Day 77: Share your composed program with code, tests, and on-chain evidence
Top comments (0)