Skip to main content
Superteam Brasil

What You Built

+20 XP
14/14

What You Built

Version stamp — checked 2026-07-26. anchor-lang 1.1.2 · borsh 1.8.0 (resolved) · edition 2021 · Agave ≥ 3.1.10. No new code in this lesson; every claim below was compiled against that toolchain on that date.

Open your file from the last exercise. Not the reference below — yours. Read it once, top to bottom, before you read anything else on this page.

Here is the reference version, so you can check yours item for item:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

pub mod vault {
    use super::*;

    #[account]
    #[derive(InitSpace)]
    pub struct VaultState {
        pub owner: Pubkey,
        pub balance: u64,
        pub bump: u8,
    }

    #[error_code]
    pub enum VaultError {
        #[msg("Deposit would overflow the vault balance")]
        Overflow,
        #[msg("Withdrawal is larger than the vault balance")]
        InsufficientFunds,
        #[msg("Amount must be greater than zero")]
        ZeroAmount,
        #[msg("Signer is not the vault owner")]
        NotOwner,
    }

    impl VaultState {
        pub fn deposit(&mut self, amount: u64) -> Result<()> {
            require!(amount > 0, VaultError::ZeroAmount);
            self.balance = self
                .balance
                .checked_add(amount)
                .ok_or(VaultError::Overflow)?;
            Ok(())
        }

        pub fn withdraw(&mut self, amount: u64) -> Result<()> {
            require!(amount > 0, VaultError::ZeroAmount);
            self.balance = self
                .balance
                .checked_sub(amount)
                .ok_or(VaultError::InsufficientFunds)?;
            Ok(())
        }
    }
}

pub use vault::{VaultError, VaultState};

Forty-odd lines. Thirteen lessons. Every line came from somewhere specific, and being able to say where is the difference between having finished a course and being able to write the next file without one.

Line by line

LineWhat it actually isWhere you got it
use anchor_lang::prelude::*;One glob import supplies Pubkey, Result, require!, msg!. Anchor re-exports the Solana crates, which is why you never named a solana-* dependency.L1 · Your First Rust Build
declare_id!("Fg6Pa…")Not decoration. #[account] expands to an impl Owner whose body reads crate::ID. Delete the macro and the error appears at your struct: cannot find value ID in the crate root.L1, and paid for at L10
pub mod vault { use super::*; }A module is a namespace, not a file. use super::* is what makes the crate root's imports visible inside the module body.L13 · Build the Vault Core
#[account]A macro that writes trait impls you would otherwise hand-write: AnchorSerialize, AnchorDeserialize, Discriminator, Owner. The discriminator is the 8 bytes your Course 1 decoder sliced off before reading owner.L10 · Structs, Traits, and What #[derive] Really Does
#[derive(InitSpace)]Generates the associated const VaultState::INIT_SPACE. Here it is 41: 32 + 8 + 1. Course 3 spends it as space = 8 + VaultState::INIT_SPACE.L10
pub owner: Pubkey32 bytes. The same 32 bytes you read with getAddressDecoder() in Course 1, now on the writing side.L2 · C1 L2
pub balance: u64Lamports stop being exact in a JavaScript Number above 2⁵³. This is the type for which that is not a question.L2 · Types, Mutability, and Why the Compiler Argues
pub bump: u8One byte, and a stored bump rather than a recomputed one. It is the value the derivation in Course 1 found by counting down from 255 until the result fell off the curve.L2 · C1 L3
#[error_code]Turns a plain enum into program errors: attaches the #[msg] strings and generates the conversion into anchor_lang::error::Error, offsetting the codes by ERROR_CODE_OFFSET = 6000. Exactly one of these per program.L11 · Enums, Option, and Result
the four variantsOn the wire: Overflow = 6000, InsufficientFunds = 6001, ZeroAmount = 6002, NotOwner = 6003. Custom errors start at 6000 so they cannot collide with Anchor's own.L11
impl VaultStateA method is just a function whose first parameter is a form of self.L10
&mut selfOne mutable borrow, and no shared borrows alongside it — the same shape as one writable account at a time. self by value would consume the account.L6 · Borrowing, L5 · Moves
-> Result<()>Anchor's alias for Result<(), anchor_lang::error::Error>. The reason the function can fail without panicking.L12 · Error Plumbing with ?
require!(amount > 0, …)Guard first, mutate second. It expands to an early return Err(...), which is why it reads like an assertion and behaves like a branch.L12
.checked_add(…) / .checked_sub(…)Returns Option<u64>None instead of wrapping or panicking. This is lesson 4's add_lamports / sub_lamports, moved onto a method.L4 · Checked Lamport Math
.ok_or(VaultError::Overflow)?ok_or converts Option into Result; ? either unwraps the Ok or returns early, applying the From conversion #[error_code] generated. Two lessons meeting inside one expression.L11 (Option) + L12 (?)
Ok(())A tail expression: no return, no semicolon. Add the semicolon and the body evaluates to (), and the error is mismatched types.L3 · Functions, Expressions, and the Missing return
pub use vault::{VaultError, VaultState};A re-export. Without it the items exist and nothing outside the module can name them.L13

Three absences, which are also the point

.clone() appears zero times. Lesson 5 was explicit that cloning is a legitimate escape hatch and not a moral failing. It is also true that in a &mut self method you never reach for it, because you never moved anything: you borrowed the account, wrote through the borrow, and gave it back.

unwrap() appears zero times. Every tutorial you will read on the way to your first real program uses it. In a program, a panic is an aborted instruction with no code and no message — the caller learns that something failed and nothing else. Your file returns 6001 with the string "Withdrawal is larger than the vault balance". That difference is the whole of lessons 4, 11 and 12.

Lifetimes appear zero times, and lesson 8 was not wasted on you. VaultState owns all three of its fields — a Pubkey, a u64 and a u8 are all owned values — so nothing in the struct borrows and there is no lifetime to name. What lesson 8 bought you is the next file: Account<'info, VaultState> in Course 3 is a borrowed view over these bytes, and 'info is the same annotation you wrote by hand on discriminator(data: &'a [u8]) -> Option<&'a [u8]>. Graduates who skip lifetimes read Account<'info, Vault> as noise for years. Question 6 below is that payoff, asked before you meet it.

Copy the file out. Now.

Say this plainly, because the platform does not: no block type on this platform persists your source file. The build server graded a submission; it did not keep a copy you can come back to. Select everything in the editor, save it locally as vault_core.rs, and put it somewhere you will find it next week.

If you lose it, Course 3's first lesson ships a known-good reference copy — the file above — so nothing dead-ends. But it will be a vault core, not yours, and the first thing Course 3 does is replace that placeholder in declare_id! with the program id of your own deployment.

What Course 3 adds, and why none of it is in your file

Your file has no #[program] module and no #[derive(Accounts)] struct. Read it cold and it looks like a program someone abandoned two-thirds of the way in. It is not. It is the two-thirds you can check by reading, deliberately separated from the third you cannot.

Course 3 addsWhy it is not here
#[program] pub mod vault_program { … } with deposit and withdraw instruction handlersA handler is an entrypoint. An entrypoint needs a deployed program id and a real account list, and its body is two lines: validate, then call the method you already wrote.
#[derive(Accounts)] pub struct Deposit<'info> with #[account(mut, seeds = [b"vault", owner.key().as_ref()], bump = vault.bump)]Constraints validate accounts. Nothing in your file is an account — that is precisely why it can be reasoned about without a runtime. This is also where your stored bump field gets spent, and where NotOwner is finally returned.
init with space = 8 + VaultState::INIT_SPACE — 49 bytes, and the rent-exemption number that follows from itSpace and rent are properties of an account, not of a struct. Your struct only had to know its own size, and #[derive(InitSpace)] is how it knows.
A CPI to the System Program to move the actual lamports: CpiContext::new(System::id(), Transfer { … })Your balance field is bookkeeping. The lamports move in a separate call, and keeping the two apart is what lets a test check the arithmetic without a runtime.
LiteSVM tests that call withdraw and assert on the number that comes backThis is the one that matters. It is what catches a checked_add you meant to be checked_sub — the exact failure the build server cannot see, and the reason Course 2's grading stops at "it compiles".
A deploy, after which declare_id! stops being a placeholder and your program has an address on devnetYou had nothing to deploy. You had something to be correct.

One currency note for when you start reading Course 3's material against the rest of the internet: CpiContext::new(System::id(), …) is the current shape. Every CPI tutorial written before Anchor 1.0 passes an account info instead — CpiContext::new(ctx.accounts.system_program.to_account_info(), …) — and it does not compile against the toolchain you have been building on. It is the single most common stale snippet in Solana material, and now you know why your copy-paste failed.

Why the split is the right shape, not a teaching convenience

Look at what your file does not depend on: no accounts, no signers, no clock, no CPI, no runtime of any kind. That is what makes it the part a reviewer reads first and the part a test can exercise in microseconds. Real audit findings cluster at the seam between the two halves — a handler that forgets to compare vault.owner to the signer, a constraint that validates the wrong account — and you cannot see a seam until the two sides of it are separate things.

It also means Course 3 is enterable on its own. Anyone arriving without this course gets the reference vault_core.rs, because a course whose first lesson requires a file you do not have is a dead end, not a prerequisite. You are arriving with your own version, which is a better place to start from.

What you can say you can do

Not "you can now write Solana programs" — you cannot yet, and one more course is why. What you can say is narrower and true: you can model on-chain state in Rust, make its invariants unbreakable by construction, and return named errors instead of panicking. That is the part of program development that no framework does for you.

Question 1 of 7

You change `pub bump: u8` to `pub bump: u64` and rebuild against the verification harness. Predict what happens.

Answer every question to unlock the AI Partner for this lesson.

Your `withdraw` compiles whether it calls `checked_sub` or `checked_add`, and the build server cannot tell the difference. In 120 words or fewer: name the specific mechanism that would catch the swap, and say at what point in Course 3 it runs. Then name one other thing in your file that no compiler can check. This is not graded and awards no XP — it is here because writing the answer is what makes you keep it.

0/120

PreviousLesson Complete!

Discussion