As an intermediate anchor developer building on Solana, it is very likely you would eventually come across a situation where it is more beneficial for your programs to interact with one another. Such composability in Solana is achieved through a mechanism called Cross-Program Invocations (CPIs).
Let us first break down what CPIs are, how you should use them and what you should keep in mind while using this functionality.
Cross-Program Invocation(CPIs)
The unique integrated architecture of Solana is what gives it the ability to process thousands of transactions per second while leveraging a globally decentralized network. This design gives room for a sort of modular structure, while acting as a single-chain blockchain that enables applications built on top of it inherit composability. This composable topology is what enable applications(and programs) to interact and build on top one another eliminating the need for complex on-chain activities such ass bridging and liquidity fragmentation.
CPIs are one of the internal mechanisms that enables a Solana program (or smart contract) to call another program on the blockchain during its execution. It is a powerful mechanism that gives different programs the ability to interact and perform actions like token transfers, data manipulation, or other operations defined by the source program.
when should you use CPIs?
You should use CPIs when you need a program to leverage another program to execute a functionality. for example, like performing a token transfer, using a CPI to leverage the Token Program will save us a lot of computational resource instead of re-implementing the token transfer functionality or in transferring an NFT from one account to another.
How would you use a CPI?
Basically using a CPI involve the following steps:
firstly identifying the programs you wish to interact with. We shall be using the Token program as an example.
Then define the necessary accounts that would be required by the CPI using Anchor's #[account] macros, and specify the constraints that define how they should be passed into the CPI as shown in the example below:
#[derive(Accounts)]
pub struct List<'info> {
#[account(mut)]
maker: Signer<'info>,
#[account(
seeds = [b"marketplace", marketplace.name.as_str().as_bytes()],
bump = marketplace.bump,
)]
marketplace: Box<Account<'info, Marketplace>>,
maker_mint: Box<InterfaceAccount<'info, Mint>>,
collection_mint: Box<InterfaceAccount<'info, Mint>>,
#[account(
mut,
associated_token::authority = maker,
associated_token::mint = maker_mint,
)]
maker_ata: Box<InterfaceAccount<'info, TokenAccount>>, //stores the maker on the heap
#[account(
init_if_needed,
payer = maker,
associated_token::mint = maker_mint,
associated_token::authority = listing,
)]
vault: Box<InterfaceAccount<'info, TokenAccount>>,
#[account(
init,
payer = maker,
space = 8 + Listing::INIT_SPACE,
seeds = [marketplace.key().as_ref(), maker_mint.key().as_ref()],
bump
)]
listing: Box<Account<'info, Listing>>,
#[account(
seeds = [
b"metadata",
metadata_program.key().as_ref(),
maker_mint.key().as_ref()
],
seeds::program = metadata_program.key(),
bump,
constraint = metadata.collection.as_ref().unwrap().key.as_ref() == collection_mint.key().as_ref(),
constraint = metadata.collection.as_ref().unwrap().verified == true,
)]
metadata: Box<Account<'info, MetadataAccount>>,
#[account(
seeds = [
b"metadata",
metadata_program.key().as_ref(),
maker_mint.key().as_ref(),
b"edition"
],
seeds::program = metadata_program.key(),
bump,
)]
master_edition: Box<Account<'info, MasterEditionAccount>>,
metadata_program: Program<'info, Metadata>,
associated_token_program: Program<'info, AssociatedToken>,
system_program: Program<'info, System>,
token_program: Interface<'info, TokenInterface>,
}
The next step would involve defining the instruction data, for example, defining the mechanism of the transfer from the maker_ata account to the vault account, establishing the maker account as the authority and the maker_mint account as the mint. The required accounts needed to deposit an NFT to a vaults were themaker_ata account, vault account, maker and maker_mint account
Then we create the CPI context
and invoke the token program transfer instruction
pub fn deposit_nft(&mut self) -> Result<()> {
let accounts = TransferChecked {
from: self.maker_ata.to_account_info(),
to: self.vault.to_account_info(),
authority: self.maker.to_account_info(),
mint: self.maker_mint.to_account_info(),
};
//create a Cpi context
let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), accounts);
// Invoke the token program’s transfer instruction
transfer_checked(cpi_ctx, 1, self.maker_mint.decimals)
}
}
The above example is also a form of an important benefit of CPIs known as Privilege Extensions which as the name implies "extends" the privileges of the caller to the callee. In our above example, the original authority maker account
"extends" the control of the authority to the Token Program
temporarily, enabling it to execute the transfer on its behalf for the duration of the invocation.
Program as Signers
It is very likely that in some cases, and this is most often the case, you would require that the program itself take authority over the assets. In the NFT marketplace example above, it is likely you would want a program to delist and even purchase an NFT on your behalf or a lending protocol program would need to manage deposited collateral and automated market maker programs need to manage the tokens put into their liquidity pools.
We can accomplish this using Program Derived Addresses (PDAs). We will talk about PDAs in subsequent articles, but simply defined: PDAs are special addresses that do not have public keys and therefore do not have an associated private key. They have two advantages to build hashmap-like structures on-chain and the allow programs to sign instructions without private keys.
PDAs combined with CPIs are very powerful features in anchor development as they allow us to securely manage assets and data by ensuring that only the owning program through the CPI can sign transactions that affect a particular account thus preventing unauthorized signings during inter-program communications.
To sign a transaction with a PDA in through the CPI Context, instead of the calling the CpiContext::new(cpi_program, accounts)
method, we would have to use the CpiContext::new_with_signer(cpi_program, accounts, seeds)
where the seeds argument are the seeds and the bump the PDA was created with as shown in the close_mint_vault
implementation below:
pub fn close_mint_vault(&mut self) -> Result<()> {
let seeds = &[
&self.marketplace.key().to_bytes()[..],
&self.maker_mint.key().to_bytes()[..],
&[self.listing.bump],
];
let signer_seeds = &[&seeds[..]];
let accounts = CloseAccount {
account: self.vault.to_account_info(),
destination: self.maker.to_account_info(),
authority: self.listing.to_account_info(),
};
//Sign with the pda
let cpi_ctx = CpiContext::new_with_signer(
self.token_program.to_account_info(),
accounts,
signer_seeds
);
close_account(cpi_ctx)
}
what happens here is that during execution, the Solana runtime will check whether hash(seeds, current_program_id) == account address
is true. If yes, that account'sis_signer
flag will be turned to true. This means a PDA derived from some program, may only be used to sign CPIs that originate from that particular program.
In summary, combining CPIs and PDAs enables us to leverage the powerful abstractive feature of Anchor while enabling us to reduce the number of accounts we need through PDAs and securing a composable interaction between different programs on our application.
Top comments (2)
Very well written man!👏
Thank you for reading. I am glad you enjoyed it