diff --git a/content/courses/program-security/reinitialization-attacks.md b/content/courses/program-security/reinitialization-attacks.md index ae148a74a..715e6cfe8 100644 --- a/content/courses/program-security/reinitialization-attacks.md +++ b/content/courses/program-security/reinitialization-attacks.md @@ -2,10 +2,10 @@ title: Reinitialization Attacks objectives: - Explain security risks associated with a reinitialization vulnerability - - Use long-form Rust check if an account has already been initialized - - Using Anchor’s `init` constraint to initialize accounts, which automatically + - Using Anchor's `init` constraint to initialize accounts, which automatically sets an account discriminator that is checked to prevent the reinitialization of an account + - Use native Rust to check if an account has already been initialized description: "Understand the security risks of account reinitialized attacks being used to override data, and how to prevent them." @@ -13,83 +13,73 @@ description: ## Summary -- Use an account discriminator or initialization flag to check whether an - account has already been initialized to prevent an account from being - reinitialized and overriding existing account data. -- To prevent account reinitialization in plain Rust, initialize accounts with an - `is_initialized` flag and check if it has already been set to true when - initializing an account +- **Prevent Account Reinitialization:** Use an account discriminator or + initialization flag to prevent an account from being reinitialized and + overwriting existing data. +- **Anchor Approach:** Simplify this by using Anchor's `init` constraint to + create an account via a CPI to the system program, automatically setting its + discriminator. +- **Native Rust Approach:** In native Rust, set an is_initialized flag during + account initialization and check it before reinitializing: + ```rust if account.is_initialized { return Err(ProgramError::AccountAlreadyInitialized.into()); } ``` -- To simplify this, use Anchor’s `init` constraint to create an account via a - CPI to the system program and sets its discriminator ## Lesson -Initialization refers to setting the data of a new account for the first time. -When initializing a new account, you should implement a way to check if the -account has already been initialized. Without an appropriate check, an existing -account could be reinitialized and have existing data overwritten. - -Note that initializing an account and creating an account are two separate -instructions. Creating an account requires invoking the `create_account` -instruction on the System Program which specifies the space required for the -account, the rent in lamports allocated to the account, and the program owner of -the account. Initialization is an instruction that sets the data of a newly -created account. Creating and initializing an account can be combined into a -single transaction. +Initialization sets the data of a new account for the first time. It's essential +to check if an account has already been initialized to prevent overwriting +existing data. Note that creating and initializing an account are separate +actions. Creating an account involves invoking the `create_account` instruction +handler on the System Program, which allocates space, rent in lamports, and +assigns the program owner. Initialization sets the account data. These steps can +be combined into a single transaction. -#### Missing Initialization Check +### Missing Initialization Check -In the example below, there are no checks on the `user` account. The -`initialize` instruction deserializes the data of the `user` account as a `User` -account type, sets the `authority` field, and serializes the updated account -data to the `user` account. - -Without checks on the `user` account, the same account could be passed into the -`initialize` instruction a second time by another party to overwrite the -existing `authority` stored on the account data. +In the example below, there's no check on the `user` account. The `initialize` +instruction handler sets the `authority` field on the `User` account type and +serializes the data. Without checks, an attacker could reinitialize the account, +overwriting the existing `authority`. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] -pub mod initialization_insecure { +pub mod initialization_insecure { use super::*; pub fn initialize(ctx: Context) -> Result<()> { - let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap(); - user.authority = ctx.accounts.authority.key(); - user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?; + ctx.accounts.user.authority = ctx.accounts.authority.key(); Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { - #[account(mut)] - user: AccountInfo<'info>, #[account(mut)] - authority: Signer<'info>, + pub user: Account<'info, User>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, } -#[derive(BorshSerialize, BorshDeserialize)] +#[account] +#[derive(InitSpace)] pub struct User { - authority: Pubkey, + pub authority: Pubkey, } ``` -#### Add `is_initialized` check +### Add is_initialized Check -One approach to fix this is to add an additional `is_initialized` field to the -`User` account type and use it as a flag to check if an account has already been -initialized. +To fix this, add an `is_initialized` field to the User account type and check it +before reinitializing: ```rust if user.is_initialized { @@ -97,15 +87,13 @@ if user.is_initialized { } ``` -By including a check within the `initialize` instruction, the `user` account -would only be initialized if the `is_initialized` field has not yet been set to -true. If the `is_initialized` field was already set, the transaction would fail, -thereby avoiding the scenario where an attacker could replace the account -authority with their own public key. +This ensures the `user` account is only initialized once. If `is_initialized` is +true, the transaction fails, preventing an attacker from changing the account +authority. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; +use anchor_lang::solana_program::program_error::ProgramError; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -114,140 +102,137 @@ pub mod initialization_secure { use super::*; pub fn initialize(ctx: Context) -> Result<()> { - let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap(); + let user = &mut ctx.accounts.user; + if user.is_initialized { return Err(ProgramError::AccountAlreadyInitialized.into()); } - user.authority = ctx.accounts.authority.key(); user.is_initialized = true; + user.authority = ctx.accounts.authority.key(); - user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { - #[account(mut)] - user: AccountInfo<'info>, #[account(mut)] - authority: Signer<'info>, + pub user: Account<'info, User>, + #[account(mut)] + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, } -#[derive(BorshSerialize, BorshDeserialize)] +#[account] +#[derive(InitSpace)] pub struct User { - is_initialized: bool, - authority: Pubkey, + pub is_initialized: bool, + pub authority: Pubkey, } ``` -#### Use Anchor’s `init` constraint - -Anchor provides an `init` constraint that can be used with the `#[account(...)]` -attribute to initialize an account. The `init` constraint creates the account -via a CPI to the system program and sets the account discriminator. +### Use Anchor's init Constraint -The `init` constraint must be used in combination with the `payer` and `space` -constraints. The `payer` specifies the account paying for the initialization of -the new account. The `space` specifies the amount of space the new account -requires, which determines the amount of lamports that must be allocated to the -account. The first 8 bytes of data is set as a discriminator that Anchor -automatically adds to identify the account type. - -Most importantly for this lesson, the `init` constraint ensures that this -instruction can only be called once per account, so you can set the initial -state of the account in the instruction logic and not have to worry about an -attacker trying to reinitialize the account. +[Anchor's `init` constraint](https://www.anchor-lang.com/docs/account-constraints), +used with the `#[account(...)]` attribute, initializes an account, sets the +account discriminator, and ensures that the instruction handler can only be +called once per account. The `init` constraint must be used with `payer` and +`space` constraints to specify the account paying for initialization and the +amount of space required. ```rust use anchor_lang::prelude::*; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +const DISCRIMINATOR_SIZE: usize = 8; + #[program] pub mod initialization_recommended { use super::*; - pub fn initialize(_ctx: Context) -> Result<()> { + pub fn initialize(ctx: Context) -> Result<()> { msg!("GM"); + ctx.accounts.user.authority = ctx.accounts.authority.key(); Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { - #[account(init, payer = authority, space = 8+32)] - user: Account<'info, User>, + #[account( + init, + payer = authority, + space = DISCRIMINATOR_SIZE + User::INIT_SPACE + )] + pub user: Account<'info, User>, #[account(mut)] - authority: Signer<'info>, - system_program: Program<'info, System>, + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, } #[account] +#[derive(InitSpace)] pub struct User { - authority: Pubkey, + pub authority: Pubkey, } ``` -#### Anchor’s `init_if_needed` constraint +#### Anchor's init_if_needed Constraint -It’s worth noting that Anchor has an `init_if_needed` constraint. This -constraint should be used very cautiously. In fact, it is blocked behind a -feature flag so that you are forced to be intentional about using it. + -The `init_if_needed` constraint does the same thing as the `init` constraint, -only if the account has already been initialized the instruction will still run. - -Given this, it’s \***\*\*\*\***extremely\***\*\*\*\*** important that when you -use this constraint you include checks to avoid resetting the account to its -initial state. - -For example, if the account stores an `authority` field that gets set in the -instruction using the `init_if_needed` constraint, you need checks that ensure -that no attacker could call the instruction after it has already been -initialized and have the `authority` field set again. +[Anchor's `init_if_needed` constraint](https://www.anchor-lang.com/docs/account-constraints), +guarded by a feature flag, should be used with caution.It initializes an account +only if it hasn't been initialized yet. If the account is already initialized, +the instruction handler will still execute, so +it's \***\*\*\*\***extremely\***\*\*\*\*** important to include checks in your +instruction handler to prevent resetting the account to its initial state. + -In most cases, it’s safer to have a separate instruction for initializing -account data. +For example, if the `authority` field is set in the instruction handler, ensure +that your instruction handler includes checks to prevent an attacker from +reinitializing it after it's already been set. Typically, it's safer to have a +separate instruction handler for initializing account data. ## Lab -For this lab we’ll create a simple program that does nothing but initialize -accounts. We’ll include two instructions: +In this lab, we'll create a simple Solana program with two instruction handlers: -- `insecure_initialization` - initializes an account that can be reinitialized -- `recommended_initialization` - initialize an account using Anchor’s `init` - constraint +- `insecure_initialization` - Initializes an account without checks, allowing + reinitialization. +- `recommended_initialization` - Initializes an account using Anchor's `init` + constraint, preventing reinitialization. -#### 1. Starter +### 1. Starter -To get started, download the starter code from the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-reinitialization-attacks/tree/starter). -The starter code includes a program with one instruction and the boilerplate -setup for the test file. +To get started, download the starter code from the +[`starter` branch of this repository](https://github.com/solana-developers/reinitialization-attacks/tree/starter). +The starter code includes a program with one instruction handler and the +boilerplate setup for the test file. -The `insecure_initialization` instruction initializes a new `user` account that -stores the public key of an `authority`. In this instruction, the account is -expected to be allocated client-side, then passed into the program instruction. -Once passed into the program, there are no checks to see if the `user` account's -initial state has already been set. This means the same account can be passed in -a second time to override the `authority` stored on an existing `user` account. +The `insecure_initialization` instruction handler initializes a new `user` +account that stores the public key of an `authority`. The account is expected to +be allocated client-side and then passed into the program instruction. However, +there are no checks to verify if the `user` account's initial state has already +been set. This means the same account can be passed in a second time, allowing +the `authority` to be overwritten. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; -declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +declare_id!("HLhxJzFYjtXCET4HxnSzv27SpXg16FWNDi2LvrNmSvzH"); #[program] pub mod initialization { use super::*; pub fn insecure_initialization(ctx: Context) -> Result<()> { - let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap(); - user.authority = ctx.accounts.authority.key(); - user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?; + let user = &mut ctx.accounts.user; + let mut user_data = User::try_from_slice(&user.data.borrow())?; + user_data.authority = ctx.accounts.authority.key(); + user_data.serialize(&mut *user.data.borrow_mut())?; Ok(()) } } @@ -255,119 +240,153 @@ pub mod initialization { #[derive(Accounts)] pub struct Unchecked<'info> { #[account(mut)] - /// CHECK: - user: UncheckedAccount<'info>, - authority: Signer<'info>, + /// CHECK: This account will be initialized in the instruction + pub user: UncheckedAccount<'info>, + pub authority: Signer<'info>, } -#[derive(BorshSerialize, BorshDeserialize)] +#[account] +#[derive(InitSpace)] pub struct User { - authority: Pubkey, + pub authority: Pubkey, } ``` -#### 2. Test `insecure_initialization` instruction +### 2. Test insecure_initialization Instruction Handler The test file includes the setup to create an account by invoking the system -program and then invokes the `insecure_initialization` instruction twice using -the same account. +program and then invokes the `insecure_initialization` instruction handler twice +using the same account. -Since there are no checks the verify that the account data has not already been -initialized, the `insecure_initialization` instruction will complete -successfully both times, despite the second invocation providing a _different_ -authority account. +Since there are no checks in the `insecure_initialization` instruction handler +to verify that the account data has not already been initialized, this +instruction handler will execute successfully both times, even with a +_different_ authority account. ```typescript import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; -import { expect } from "chai"; import { Initialization } from "../target/types/initialization"; +import { + Keypair, + LAMPORTS_PER_SOL, + SystemProgram, + Transaction, + SendTransactionError, +} from "@solana/web3.js"; +import { expect } from "chai"; +import { airdropIfRequired } from "@solana-developers/helpers"; -describe("initialization", () => { +describe("Initialization", () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.Initialization as Program; - const wallet = anchor.workspace.Initialization.provider.wallet; - const walletTwo = anchor.web3.Keypair.generate(); + const walletAuthority = provider.wallet as anchor.Wallet; + const secondWallet = Keypair.generate(); + + const insecureUserAccount = Keypair.generate(); + const recommendedUserAccount = Keypair.generate(); - const userInsecure = anchor.web3.Keypair.generate(); - const userRecommended = anchor.web3.Keypair.generate(); + const ACCOUNT_SPACE = 32; + const AIRDROP_AMOUNT = 1 * LAMPORTS_PER_SOL; + const MINIMUM_BALANCE_FOR_RENT_EXEMPTION = 1 * LAMPORTS_PER_SOL; before(async () => { - const tx = new anchor.web3.Transaction().add( - anchor.web3.SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: userInsecure.publicKey, - space: 32, - lamports: - await provider.connection.getMinimumBalanceForRentExemption(32), + try { + const rentExemptionAmount = + await provider.connection.getMinimumBalanceForRentExemption( + ACCOUNT_SPACE, + ); + + const createAccountInstruction = SystemProgram.createAccount({ + fromPubkey: walletAuthority.publicKey, + newAccountPubkey: insecureUserAccount.publicKey, + space: ACCOUNT_SPACE, + lamports: rentExemptionAmount, programId: program.programId, - }), - ); - - await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ - wallet.payer, - userInsecure, - ]); - - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - walletTwo.publicKey, - 1 * anchor.web3.LAMPORTS_PER_SOL, - ), - "confirmed", - ); + }); + + const transaction = new Transaction().add(createAccountInstruction); + + await anchor.web3.sendAndConfirmTransaction( + provider.connection, + transaction, + [walletAuthority.payer, insecureUserAccount], + ); + + await airdropIfRequired( + provider.connection, + secondWallet.publicKey, + AIRDROP_AMOUNT, + MINIMUM_BALANCE_FOR_RENT_EXEMPTION, + ); + } catch (error) { + console.error("Setup failed:", error); + throw error; + } }); - it("Insecure init", async () => { - await program.methods - .insecureInitialization() - .accounts({ - user: userInsecure.publicKey, - }) - .rpc(); + it("performs insecure initialization", async () => { + try { + await program.methods + .insecureInitialization() + .accounts({ + user: insecureUserAccount.publicKey, + authority: walletAuthority.publicKey, + }) + .signers([walletAuthority.payer]) + .rpc(); + } catch (error) { + console.error("Insecure initialization failed:", error); + throw error; + } }); - it("Re-invoke insecure init with different auth", async () => { - const tx = await program.methods - .insecureInitialization() - .accounts({ - user: userInsecure.publicKey, - authority: walletTwo.publicKey, - }) - .transaction(); - await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ - walletTwo, - ]); + it("re-invokes insecure initialization with different authority", async () => { + try { + const transaction = await program.methods + .insecureInitialization() + .accounts({ + user: insecureUserAccount.publicKey, + authority: secondWallet.publicKey, + }) + .signers([secondWallet]) + .transaction(); + + await anchor.web3.sendAndConfirmTransaction( + provider.connection, + transaction, + [secondWallet], + ); + } catch (error) { + console.error("Re-invocation of insecure initialization failed:", error); + throw error; + } }); }); ``` -Run `anchor test` to see that both transactions will complete successfully. +Run `anchor test` to verify that the `insecure_initialization` instruction +handler executes successfully in both invocations. ```bash -initialization - ✔ Insecure init (478ms) - ✔ Re-invoke insecure init with different auth (464ms) +Initialization + ✔ performs insecure initialization (420ms) + ✔ re-invokes insecure initialization with different authority (419ms) ``` -#### 3. Add `recommended_initialization` instruction +### 3. Add recommended_initialization Instruction Handler -Let's create a new instruction called `recommended_initialization` that fixes -this problem. Unlike the previous insecure instruction, this instruction should +Now, let's create a new instruction handler called `recommended_initialization` +that addresses the issue. Unlike the insecure instruction handler, this one will handle both the creation and initialization of the user's account using Anchor's `init` constraint. -This constraint instructs the program to create the account via a CPI to the -system program, so the account no longer needs to be created client-side. The -constraint also sets the account discriminator. Your instruction logic can then -set the account's initial state. - -By doing this, you ensure that any subsequent invocation of the same instruction -with the same user account will fail rather than reset the account's initial -state. +This constraint ensures the account is created via a CPI to the system program, +and the discriminator is set. This way, any subsequent invocation with the same +user account will fail, preventing reinitialization. ```rust use anchor_lang::prelude::*; @@ -378,7 +397,7 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod initialization { use super::*; - ... + ... pub fn recommended_initialization(ctx: Context) -> Result<()> { ctx.accounts.user.authority = ctx.accounts.authority.key(); Ok(()) @@ -387,92 +406,118 @@ pub mod initialization { #[derive(Accounts)] pub struct Checked<'info> { - #[account(init, payer = authority, space = 8+32)] + #[account( + init, + payer = authority, + space = DISCRIMINATOR_SIZE + User::INIT_SPACE + )] user: Account<'info, User>, #[account(mut)] authority: Signer<'info>, system_program: Program<'info, System>, } + +#[account] +#[derive(InitSpace)] +pub struct User { + pub authority: Pubkey, +} ``` -#### 4. Test `recommended_initialization` instruction +### 4. Test recommended_initialization Instruction Handler -To test the `recommended_initialization` instruction, we’ll invoke the -instruction twice just like before. This time, we expect the transaction to fail -when we try to initialize the same account a second time. +To test the `recommended_initialization` instruction handler, invoke it twice as +before. This time, the transaction should fail when attempting to initialize the +same account a second time. ```typescript -describe("initialization", () => { +describe("Initialization", () => { ... - it("Recommended init", async () => { - await program.methods - .recommendedInitialization() - .accounts({ - user: userRecommended.publicKey, - }) - .signers([userRecommended]) - .rpc() - }) - - it("Re-invoke recommended init with different auth, expect error", async () => { + it("performs recommended initialization", async () => { + try { + await program.methods + .recommendedInitialization() + .accounts({ + user: recommendedUserAccount.publicKey, + }) + .signers([recommendedUserAccount]) + .rpc(); + } catch (error) { + console.error("Recommended initialization failed:", error); + throw error; + } + }); + + it("fails to re-invoke recommended initialization with different authority", async () => { try { - // Add your test here. - const tx = await program.methods + const transaction = await program.methods .recommendedInitialization() .accounts({ - user: userRecommended.publicKey, - authority: walletTwo.publicKey, + user: recommendedUserAccount.publicKey, + authority: secondWallet.publicKey, }) - .transaction() - await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ - walletTwo, - userRecommended, - ]) - } catch (err) { - expect(err) - console.log(err) + .transaction(); + + await anchor.web3.sendAndConfirmTransaction( + provider.connection, + transaction, + [secondWallet, recommendedUserAccount], + { commitment: "confirmed" } + ); + + throw new Error("Re-invocation succeeded unexpectedly"); + } catch (error) { + if (error.message === "Re-invocation succeeded unexpectedly") { + throw error; + } + + if (error instanceof SendTransactionError) { + console.log("Transaction failed as expected"); + } else { + console.error("Unexpected error:", error); + } + console.log(error) + expect(error).to.exist; } - }) -}) + }); +}); ``` -Run `anchor test` and to see that the second transaction which tries to -initialize the same account twice will now return an error stating the account -address is already in use. +Run `anchor test` to confirm that the second transaction fails with an error +indicating the account is already in use. ```bash -'Program CpozUgSwe9FPLy9BLNhY2LTGqLUk1nirUkMMA5RmDw6t invoke [1]', +'Program HLhxJzFYjtXCET4HxnSzv27SpXg16FWNDi2LvrNmSvzH invoke [1]', 'Program log: Instruction: RecommendedInitialization', 'Program 11111111111111111111111111111111 invoke [2]', -'Allocate: account Address { address: EMvbwzrs4VTR7G1sNUJuQtvRX1EuvLhqs4PFqrtDcCGV, base: None } already in use', +'Allocate: account Address { address: FcW7tG71GKuRgxEbgFuuNQNV3HVSMmVyKATo74iCK4yi, base: None } already in use', 'Program 11111111111111111111111111111111 failed: custom program error: 0x0', -'Program CpozUgSwe9FPLy9BLNhY2LTGqLUk1nirUkMMA5RmDw6t consumed 4018 of 200000 compute units', -'Program CpozUgSwe9FPLy9BLNhY2LTGqLUk1nirUkMMA5RmDw6t failed: custom program error: 0x0' +'Program HLhxJzFYjtXCET4HxnSzv27SpXg16FWNDi2LvrNmSvzH consumed 3330 of 200000 compute units', +'Program HLhxJzFYjtXCET4HxnSzv27SpXg16FWNDi2LvrNmSvzH failed: custom program error: 0x0' ``` -If you use Anchor's `init` constraint, that's usually all you need to protect -against reinitialization attacks! Remember, just because the fix for these -security exploits is simple doesn't mean it isn't important. Every time your -initialize an account, make sure you're either using the `init` constraint or -have some other check in place to avoid resetting an existing account's initial -state. +Using Anchor's `init` constraint is usually sufficient to protect against +reinitialization attacks. While the fix for these security exploits is +straightforward, it is crucial. Every time you initialize an account, ensure +that you're either using the `init` constraint or implementing another check to +prevent resetting an existing account's initial state. -If you want to take a look at the final solution code you can find it on the -`solution` branch of -[this repository](https://github.com/Unboxed-Software/solana-reinitialization-attacks/tree/solution). +For the final solution code, refer to the +[`solution` branch of this repository](https://github.com/solana-developers/reinitialization-attacks/tree/solution). ## Challenge -Just as with other lessons in this unit, your opportunity to practice avoiding -this security exploit lies in auditing your own or other programs. +Your challenge is to audit your own or other programs to practice avoiding this +security exploit. -Take some time to review at least one program and ensure that instructions are -properly protected against reinitialization attacks. +Take some time to review at least one program and confirm that instruction +handlers are adequately protected against reinitialization attacks. -Remember, if you find a bug or exploit in somebody else's program, please alert -them! If you find one in your own program, be sure to patch it right away. +If you find a bug or exploit in another program, alert the developer. If you +find one in your own program, patch it immediately. + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=652c68aa-18d9-464c-9522-e531fd8738d5)!