learn.sol
Anchor Programs • Defi Integration Challenge
Lesson 12 of 12

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:

  1. a payer deposits tokens into a program-controlled vault
  2. the program records a settlement agreement on-chain
  3. an authorized settlement step releases tokens to a recipient
  4. an optional fee cut is routed to a treasury token account
  5. 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:

  1. a Settlement PDA for business state
  2. a vault-authority PDA tied to that settlement
  3. a vault token account owned by the vault-authority PDA
  4. 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.

Settlement PDATap to reveal
The program state account that stores who is paying, who receives funds, which treasury is allowed, and what stage the settlement is in.
Settlement PDA
Treasury RouteTap to reveal
The explicit on-chain rule that sends the protocol fee to one expected treasury token account.
Treasury Route
Replay ProtectionTap to reveal
The state-machine rule that stops settlement from being executed a second time after the first success.
Replay Protection
Vault SettlementTap to reveal
A token flow where assets enter a program-controlled vault first and only leave when the program approves the release.
Vault Settlement

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:

  • payer defines who supplies the tokens
  • recipient defines who should receive the main proceeds
  • treasury defines where the fee goes
  • mint fixes the asset being settled
  • amount fixes the gross settlement amount
  • fee_bps lets you compute one protocol fee deterministically
  • status prevents 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:

  1. funding succeeds for the correct payer
  2. funding fails for the wrong payer
  3. settlement succeeds for the correct state and accounts
  4. settlement fails if the vault authority seeds are wrong
  5. settlement fails if the mint is wrong
  6. settlement fails if the treasury token account is not the expected one
  7. 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:

  1. who can move tokens at each step?
  2. which PDA actually controls the vault?
  3. what prevents double settlement?
  4. what guarantees the fee goes to the intended treasury?
  5. 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.

Do Not Hand-Wave Vault Authority

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

Quick Check
Single answer

Why should the fee calculation and treasury route live inside the program instead of only in the client?

Quick Check
Single answer

What actually prevents the settlement from executing twice in the capstone design?

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px