Traces and validates Cross-Program Invocation chains across Solana programs, detecting unsafe CPI patterns, verifying program ownership at each hop, and flagging reentrancy vectors through nested invocations
curl -s https://parity.cx/api/skills/cpi-validator | shcopyYou are tracing and validating every Cross-Program Invocation chain in a Solana program. CPI is how Solana programs compose, but each hop introduces trust boundaries. An unverified program ID at any hop means an attacker can substitute a malicious program that mimics the expected interface. This skill builds a complete CPI call graph and validates safety at every edge.
| Source | Path | Contains |
|--------|------|----------|
| Vulnerability Rules | programs/parity/src/context_engine.rs > VULNERABILITY_RULES | insecure-cpi pattern with detection hints |
| Curated Audit Findings | programs/parity/src/context_engine.rs > CURATED_AUDIT_FINDINGS | Real CPI vulnerabilities from OtterSec, Sec3, Neodyme including reentrancy-via-callback |
| Framework Patterns | programs/parity/src/context_engine.rs > ANCHOR_PATTERNS | Correct CPI invocation pattern with typed Program accounts |
| Security Audit Skill | skills/security-audit/SKILL.md | CPI security checks (Step 5) and reentrancy checks (Step 10) |
GitHub base URL: https://github.com/paritydotcx/Skills/blob/main/
Scan the program for every CPI call site:
Anchor-style CPI:
CpiContext::new(program, accounts) — unsigned CPICpiContext::new_with_signer(program, accounts, signer_seeds) — PDA-signed CPItoken::transfer, token::mint_to, token::burn and other anchor_spl helpersNative-style CPI:
invoke(&instruction, &account_infos) — unsignedinvoke_signed(&instruction, &account_infos, &signer_seeds) — signedFor each CPI call, record:
For every CPI target, verify the program ID is validated:
Safe: Typed Program account
// Anchor verifies program ID at deserialization
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,Unsafe: Raw AccountInfo as program
// VULNERABLE: any program can be substituted
pub target_program: AccountInfo<'info>,
// then used in:
invoke(&ix, &[...target_program.to_account_info()])?;Unsafe: Unchecked UncheckedAccount
// VULNERABLE: explicitly unchecked, no program ID validation
/// CHECK: trusted
pub external_program: UncheckedAccount<'info>,For each unsafe CPI target, check whether there is a manual program ID check before the CPI call:
// Manual check (acceptable but less safe than typed)
require!(
ctx.accounts.target_program.key() == expected_program::ID,
ErrorCode::InvalidProgram
);Finding if violated:
insecure-cpiTrace which accounts are passed from the calling program to the CPI target:
mut in the caller's context that are passed to CPI can be modified by the target program. Verify the caller expects this.Finding if applicable:
Check the checks-effects-interactions pattern:
If state is mutated after a CPI call, the target program could potentially callback into the originating program (directly or through a chain), reading state that hasn't been updated yet.
// VULNERABLE: state updated after CPI
token::transfer(cpi_ctx, amount)?;
ctx.accounts.vault.balance -= amount; // read by reentering call shows old balance
// SECURE: checks-effects-interactions
ctx.accounts.vault.balance -= amount; // update first
token::transfer(cpi_ctx, amount)?; // then CPIMap every instruction to determine if it follows checks-effects-interactions.
Finding if violated:
If the program chains CPI calls (program A calls B which calls C):
max_depthFinding if applicable:
When using CpiContext::new_with_signer or invoke_signed:
Reference skills/pda-helper/SKILL.md for detailed PDA signer validation patterns.
Finding if violated:
| Severity | Penalty |
|----------|---------|
| Critical | -25 |
| High | -15 |
| Medium | -8 |
| Info | -3 |
{
"score": 55,
"cpi_map": {
"deposit": {
"calls": [
{
"target": "Token Program",
"verified": true,
"method": "typed_program",
"operation": "transfer",
"line": 67,
"accounts_passed": ["user_token", "vault_token", "authority"],
"signer_type": "user"
}
],
"state_before_cpi": true,
"reentrancy_safe": true
},
"swap": {
"calls": [
{
"target": "Unknown",
"verified": false,
"method": "raw_accountinfo",
"line": 134
}
],
"state_before_cpi": false,
"reentrancy_safe": false
}
},
"findings": [...],
"summary": "8 CPI calls across 4 instructions. 1 critical (unverified program ID in swap), 1 critical (state after CPI in swap). deposit and withdraw are safe."
}