DeFi Integration Capstone
Build one optional Anchor capstone that routes token settlement through a program-controlled vault, enforces a clear authority model, and proves the full flow with failure-path tests.
This page should not be treated like the main teaching spine.
It is an optional capstone.
That means it assumes the rest of the Anchor section already feels normal:
- PDA design
- token account wiring
- CPI
- signer seeds
- state transitions
- failure-path testing
If those still feel shaky, go back and strengthen them first.
The One Project
Build a vault-routed settlement module.
The flow is simple:
- a payer deposits tokens into a program-controlled vault
- the program records a settlement agreement on-chain
- an authorized settlement step releases tokens to a recipient
- an optional fee cut is routed to a treasury token account
- the settlement cannot be executed twice
That is the only project in this capstone.
No menu. No branching options.
Why This Is The Right Capstone
This project forces you to combine the exact ideas the section was building toward:
- PDA state
- PDA authority
- token CPI
- explicit settlement states
- one real trust boundary
It also feels closer to a real protocol module than the earlier training projects.
You are no longer just teaching one token mint or one escrow.
You are teaching controlled settlement logic.
What The Program Must Guarantee
At minimum, your program should enforce these rules on-chain:
- only the intended payer can fund the vault
- only the expected recipient can receive settlement proceeds
- settlement can only happen once
- fee routing, if present, must use the exact expected treasury account
- token accounts must match the intended mint
- PDA signer seeds must match the vault authority derivation rule exactly
If those rules are not enforced on-chain, the capstone is weak.
The Core Architecture
Use four main pieces:
- a
SettlementPDA for business state - a
vault-authorityPDA tied to that settlement - a vault token account owned by the vault-authority PDA
- one settlement instruction that performs the token CPI and updates state
This should look familiar by now.
That is intentional.
A capstone should combine the earlier patterns, not invent a totally different mental model.
Suggested Settlement State
Keep the first version compact.
#[account]
#[derive(InitSpace)]
pub struct Settlement {
pub payer: Pubkey,
pub recipient: Pubkey,
pub treasury: Pubkey,
pub mint: Pubkey,
pub amount: u64,
pub fee_bps: u16,
pub status: SettlementStatus,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)]
pub enum SettlementStatus {
Initialized,
Funded,
Settled,
Cancelled,
}Why this is enough:
payerdefines who supplies the tokensrecipientdefines who should receive the main proceedstreasurydefines where the fee goesmintfixes the asset being settledamountfixes the gross settlement amountfee_bpslets you compute one protocol fee deterministicallystatusprevents replay and invalid transitions
That is already a real authority model.
PDA Rules
Use deterministic rules and keep them boring.
Settlement PDA
seeds = [b"settlement", payer.key().as_ref(), settlement_id.as_ref()]Vault authority PDA
seeds = [b"vault-authority", settlement.key().as_ref()]Those two formulas are enough.
Do not get clever here.
The harder the logic becomes, the more important predictable seeds become.
Step 1: Initialize The Settlement
The initialize instruction should only write the agreement.
It should not move tokens yet.
That separation matters.
#[derive(Accounts)]
#[instruction(settlement_id: [u8; 8])]
pub struct InitializeSettlement<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub recipient: UncheckedAccount<'info>,
pub treasury: UncheckedAccount<'info>,
#[account(
init,
payer = payer,
space = 8 + Settlement::INIT_SPACE,
seeds = [b"settlement", payer.key().as_ref(), &settlement_id],
bump,
)]
pub settlement: Account<'info, Settlement>,
pub system_program: Program<'info, System>,
}The handler should only store the state terms and mark the settlement as Initialized.
That is enough for the first step.
Step 2: Fund The Vault
The payer should move tokens into a vault token account owned by the vault-authority PDA.
That is the same custody pattern you already saw in escrow.
The important point is not novelty.
The important point is reuse of a correct authority pattern.
At the funding step, validate all of these:
- settlement status is
Initialized - payer matches the stored settlement payer
- payer token account matches the expected mint
- vault token account is derived for the vault-authority PDA and the correct mint
Then use transfer_checked to move the amount into the vault.
After the transfer, set the settlement status to Funded.
Step 3: Settle Through The Vault Authority PDA
This is the heart of the capstone.
The settlement instruction should:
- require the settlement to be
Funded - compute the fee amount from
fee_bps - compute the recipient amount as
amount - fee - transfer the fee from the vault to the treasury token account
- transfer the remainder from the vault to the recipient token account
- mark the settlement as
Settled
The critical CPI pattern looks like this:
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault-authority",
settlement.key().as_ref(),
&[ctx.bumps.vault_authority],
]];
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
)
.with_signer(signer_seeds);That is the same signer rule you already learned.
Now it is doing real settlement work.
Step 4: Make The Fee Rule Explicit
Do not hide the fee logic in the client.
Do not treat treasury routing as a UI detail.
If the protocol takes a fee, the program should compute and enforce it.
A clear pattern is:
let fee_amount = settlement
.amount
.checked_mul(settlement.fee_bps as u64)
.ok_or(ErrorCode::Overflow)?
/ 10_000;
let recipient_amount = settlement
.amount
.checked_sub(fee_amount)
.ok_or(ErrorCode::Overflow)?;That makes the settlement math visible and testable.
Step 5: Block Replay By Updating State Last
This is a small but important discipline point.
You should update the final status only after the CPI steps succeed.
settlement.status = SettlementStatus::Settled;That preserves a clean rule:
- unfinished CPI means unfinished settlement
- finished CPI means final state transition
That rule makes both reasoning and testing cleaner.
Required Failure Paths
A capstone is not complete if it only works once on the happy path.
At minimum, test all of these:
- funding succeeds for the correct payer
- funding fails for the wrong payer
- settlement succeeds for the correct state and accounts
- settlement fails if the vault authority seeds are wrong
- settlement fails if the mint is wrong
- settlement fails if the treasury token account is not the expected one
- settlement fails on second execution because the status is no longer
Funded
That is the minimum serious matrix.
Risk Review
Before you call this capstone done, answer these clearly:
- who can move tokens at each step?
- which PDA actually controls the vault?
- what prevents double settlement?
- what guarantees the fee goes to the intended treasury?
- what guarantees the wrong mint cannot slip into the flow?
If your answers are vague, the implementation is still weak.
What A Good Submission Looks Like
A good capstone is not the one with the most features.
It is the one where:
- the account model is easy to explain
- the authority model is easy to explain
- the seed formulas are stable
- the fee logic is explicit
- the failure-path tests prove the trust boundary
That is the real standard.
If you cannot explain exactly why the vault authority PDA is allowed to sign, do not call the design finished.
That is the center of the whole module.
One Good Extension
If the core capstone works, add exactly one improvement:
- add an expiry timestamp that lets the payer cancel if settlement never occurs
That extension deepens the state machine without changing the core authority model.
What Comes Next
This is the end of the optional Anchor capstone path.
If you can build this cleanly and explain the authority model without hand-waving, the Anchor section has done its job.
Quick Check
Why should the fee calculation and treasury route live inside the program instead of only in the client?
What actually prevents the settlement from executing twice in the capstone design?