From 763089f660c3df1da4b8f6be3c5d134326616bcc Mon Sep 17 00:00:00 2001 From: 0xCipherCoder Date: Wed, 2 Oct 2024 13:53:22 +0530 Subject: [PATCH] Native onchain development - Updated program security lesson (#452) * Updated contents and code snippets as per guidelines * Updated content * Resolved comments as per suggestions * Updated word highlight fix --- .../program-security.md | 476 +++++++++--------- 1 file changed, 246 insertions(+), 230 deletions(-) diff --git a/content/courses/native-onchain-development/program-security.md b/content/courses/native-onchain-development/program-security.md index 1bbba4de8..61872f316 100644 --- a/content/courses/native-onchain-development/program-security.md +++ b/content/courses/native-onchain-development/program-security.md @@ -1,119 +1,125 @@ --- title: Create a Basic Program, Part 3 - Basic Security and Validation objectives: - - Explain the importance of "thinking like an attacker" - - Understand basic security practices - - Perform owner checks - - Perform signer checks - - Validate accounts passed into the program - - Perform basic data validation -description: "How to implement account checks and validate instruction data." + - Understand why "thinking like an attacker" is essential in securing Solana + programs. + - Learn and implement core security practices to protect your program. + - Perform owner and signer checks to verify account ownership and transaction + authenticity. + - Validate the accounts passed into your program to ensure they are what you + expect. + - Conduct basic data validation to prevent invalid or malicious input from + compromising your program. +description: + "Learn how to secure your Solana program with ownership, signer, and account + validation checks." --- ## Summary -- **Thinking like an attacker** means asking "How do I break this?" -- Perform **owner checks** to ensure that the provided account is owned by the - public key you expect, e.g. ensuring that an account you expect to be a PDA is - owned by `program_id` -- Perform **signer checks** to ensure that any account modification has been - signed by the right party or parties -- **Account validation** entails ensuring that provided accounts are the - accounts you expect them to be, e.g. deriving PDAs with the expected seeds to - make sure the address matches the provided account -- **Data validation** entails ensuring that any provided data meets the criteria - required by the program +- **Thinking like an attacker** is about shifting your mindset to proactively + identify potential security gaps by asking, "How do I break this?" +- **Owner checks** ensure that an account is controlled by the expected public + key, such as verifying that a PDA (Program Derived Address) is owned by the + program. +- **Signer checks** confirm that the right parties have signed the transaction, + allowing for safe modifications to accounts. +- **Account validation** is used to ensure that the accounts passed into your + program match your expectations, like checking the correctness of a PDA's + derivation. +- **Data validation** verifies that the instruction data provided to your + program adheres to specific rules or constraints, ensuring it doesn't lead to + unintended behavior. ## Lesson -In the last two lessons we worked through building a Movie Review program -together. The end result is pretty cool! It's exciting to get something working -in a new development environment. - -Proper program development, however, doesn't end at "get it working." It's -important to think through the possible failure points in your code to mitigate -them. Failure points are where undesirable behavior in your code could -potentially occur. Whether the undesirable behavior happens due to users -interacting with your program in unexpected ways or bad actors intentionally -trying to exploit your program, anticipating failure points is essential to -secure program development. +In the previous lessons +[deserialize instruction data](/content/courses/native-onchain-development/deserialize-instruction-data.md) +and +[program state management](/content/courses/native-onchain-development/program-state-management.md), +we built a Movie Review program, and while getting it to function was exciting, +secure development doesn't stop at "just working." It's critical to understand +potential failure points and take proactive steps to secure your program against +both accidental misuse and intentional exploitation. Remember, **you have no control over the transactions that will be sent to your program once it's deployed**. You can only control how your program handles them. While this lesson is far from a comprehensive overview of program security, we'll cover some of the basic pitfalls to look out for. -### Think like an attacker - -[Neodyme](https://workshop.neodyme.io/) gave a presentation at Breakpoint 2021 -entitled "Think Like An Attacker: Bringing Smart Contracts to Their Break(ing) -Point." If there's one thing you take away from this lesson, it's that you -should think like an attacker. - -In this lesson, of course, we cannot cover everything that could possibly go -wrong with your programs. Ultimately, every program will have different security -risks associated with it. While understanding common pitfalls is _essential_ to -engineering good programs, it is _insufficient_ for deploying secure ones. To -have the broadest security coverage possible, you have to approach your code -with the right mindset. +### Think Like an Attacker -As Neodyme mentioned in their presentation, the right mindset requires moving -from the question "Is this broken?" to "How do I break this?" This is the first -and most essential step in understanding what your code _actually does_ as -opposed to what you wrote it to do. +A fundamental principle in secure programming is adopting an "attacker's +mindset." This means considering every possible angle someone might use to break +or exploit your program. -#### All programs can be broken +In their presentation at Breakpoint 2021, +[Neodyme](https://workshop.neodyme.io/) emphasized that secure program +development isn't just about identifying when something is broken; it's about +exploring how it can be broken. By asking, "How do I break this?" you shift from +simply testing expected functionality to uncovering potential weaknesses in the +implementation itself. -It's not a question of "if." +All programs, regardless of complexity, can be exploited. The goal isn't to +achieve absolute security (which is impossible) but to make it as difficult as +possible for malicious actors to exploit weaknesses. By adopting this mindset, +you're better prepared to identify and close gaps in your program's security. -Rather, it's a question of "how much effort and dedication would it take." +#### All Programs Can Be Broken -Our job as developers is to close as many holes as possible and increase the -effort and dedication required to break our code. For example, in the Movie -Review program we built together over the last two lessons, we wrote code to -create new accounts to store movie reviews. If we take a closer look at the -code, however, we'll notice how the program also facilitates a lot of -unintentional behavior we could easily catch by asking "How do I break this?" -We'll dig into some of these problems and how to fix them in this lesson, but -remember that memorizing a few pitfalls isn't sufficient. It's up to you to -change your mindset toward security. +Every program has vulnerabilities. The question isn't whether it can be broken, +but how much effort it takes. As developers, our goal is to close as many +security gaps as possible and increase the effort required to break our code. +For example, while our Movie Review program creates accounts to store reviews, +there may be unintentional behaviors that could be caught by thinking like an +attacker. In this lesson, we'll explore these issues and how to address them. ### Error handling Before we dive into some of the common security pitfalls and how to avoid them, -it's important to know how to use errors in your program. While your code can -handle some issues gracefully, other issues will require that your program stop -execution and return a program error. +it's important to know how to use errors in your program. Security issues in a +Solana program often requires terminating the execution with a meaningful error. +Not all errors are catastrophic, but some should result in stopping the program +and returning an appropriate error code to prevent further processing. -#### How to create errors +#### Creating Custom Errors -While the `solana_program` crate provides a `ProgramError` enum with a list of -generic errors we can use, it will often be useful to create your own. Your -custom errors will be able to provide more context and detail while you're -debugging your code. +Solana's +[`solana_program`](https://docs.rs/solana-program/latest/solana_program/) crate +provides a generic +[`ProgramError`](https://docs.rs/solana-program/latest/solana_program/program_error/enum.ProgramError.html) +enum for error handling. However, custom errors allow you to provide more +detailed, context-specific information that helps during debugging and testing. We can define our own errors by creating an enum type listing the errors we want to use. For example, the `NoteError` contains variants `Forbidden` and `InvalidLength`. The enum is made into a Rust `Error` type by using the `derive` -attribute macro to implement the `Error` trait from the `thiserror` library. -Each error type also has its own `#[error("...")]` notation. This lets you -provide an error message for each particular error type. +attribute macro to implement the `Error` trait from the +[`thiserror`](https://docs.rs/thiserror/latest/thiserror/) library. Each error +type also has its own `#[error("...")]` notation. This lets you provide an error +message for each particular error type. + +Here's an example of how you can define custom errors in your program: ```rust -use solana_program::{program_error::ProgramError}; +use solana_program::program_error::ProgramError; use thiserror::Error; -#[derive(Error)] +#[derive(Error, Debug)] pub enum NoteError { - #[error("Wrong note owner")] + #[error("Unauthorized access - You don't own this note.")] Forbidden, - #[error("Text is too long")] + #[error("Invalid note length - The text exceeds the allowed limit.")] InvalidLength, } ``` -#### How to return errors +In this example, we create custom errors for unauthorized access and invalid +data input (such as note length). Defining custom errors gives us greater +flexibility when debugging or explaining what went wrong during execution. + +#### Returning Errors The compiler expects errors returned by the program to be of type `ProgramError` from the `solana_program` crate. That means we won't be able to return our @@ -138,54 +144,66 @@ if pda != *note_pda.key { } ``` -### Basic security checks +This ensures the program gracefully handles errors and provides meaningful +feedback when things go wrong. -While these won't comprehensively secure your program, there are a few security -checks you can keep in mind to fill in some of the larger gaps in your code: +### Basic Security Checks -- Ownership checks - used to verify that an account is owned by the program -- Signer checks - used to verify that an account has signed a transaction -- General Account Validation - used to verify that an account is the expected - account -- Data Validation - used to verify the inputs provided by a user +To ensure your Solana program is resilient against common vulnerabilities, you +should incorporate key security checks. These are critical for detecting invalid +accounts or unauthorized transactions and preventing undesired behavior. #### Ownership checks -An ownership check verifies that an account is owned by the expected public key. -Let's use the note-taking app example that we've referenced in previous lessons. -In this app, users can create, update, and delete notes that are stored by the -program in PDA accounts. - -When a user invokes the `update` instruction, they also provide a `pda_account`. -We presume the provided `pda_account` is for the particular note they want to -update, but the user can input any instruction data they want. They could even -potentially send data which matches the data format of a note account but was -not also created by the note-taking program. This security vulnerability is one -potential way to introduce malicious code. +An ownership check verifies that an account is owned by the expected program. +For instance, if your program relies on PDAs (Program Derived Addresses), you +want to ensure that those PDAs are controlled by your program and not by an +external party. + +Let's use the note-taking app example that we've referenced in the +[deserialize instruction data](/content/courses/native-onchain-development/deserialize-instruction-data.md) +and +[program state management](/content/courses/native-onchain-development/program-state-management.md) +lessons. In this app, users can create, update, and delete notes that are stored +by the program in PDA accounts. + +When a user invokes the `update` instruction handler, they also provide a +`pda_account`. We presume the provided `pda_account` is for the particular note +they want to update, but the user can input any instruction data they want. They +could even potentially send data that matches the data format of a note account +but was not also created by the note-taking program. This security vulnerability +is one potential way to introduce malicious code. The simplest way to avoid this problem is to always check that the owner of an account is the public key you expect it to be. In this case, we expect the note account to be a PDA account owned by the program itself. When this is not the case, we can report it as an error accordingly. +Here's how you can perform an ownership check to verify that an account is owned +by the program: + ```rust if note_pda.owner != program_id { return Err(ProgramError::InvalidNoteAccount); } ``` -As a side note, using PDAs whenever possible is more secure than trusting -externally-owned accounts, even if they are owned by the transaction signer. The -only accounts that the program has complete control over are PDA accounts, -making them the most secure. +In this example, we check if the `note_pda` is owned by the program itself +(denoted by `program_id`). Ownership checks like these prevent unauthorized +entities from tampering with critical accounts. + + + +PDAs are often considered to be trusted stores of a program's state. Ensuring +the correct program owns the PDAs is a fundamental way to prevent malicious +behavior. -#### Signer checks +#### Signer Checks -A signer check simply verifies that the right parties have signed a transaction. -In the note-taking app, for example, we would want to verify that the note -creator signed the transaction before we process the `update` instruction. -Otherwise, anyone can update another user's notes by simply passing in the -user's public key as the initializer. +Signer checks confirm that a transaction has been signed by the correct parties. +In the note-taking app, for example, we want to verify that only the note +creator can update the note. Without this check, anyone could attempt to modify +another user's note by passing in their public key. ```rust if !initializer.is_signer { @@ -194,39 +212,48 @@ if !initializer.is_signer { } ``` -#### General account validation +By verifying that the initializer has signed the transaction, we ensure that +only the legitimate owner of the account can perform actions on it. -In addition to checking the signers and owners of accounts, it's important to -ensure that the provided accounts are what your code expects them to be. For -example, you would want to validate that a provided PDA account's address can be -derived with the expected seeds. This ensures that it is the account you expect -it to be. +#### Account Validation + +Account validation checks that the accounts passed into the program are correct +and valid. This is often done by deriving the expected account using known seeds +(for PDAs) and comparing it to the passed account. -In the note-taking app example, that would mean ensuring that you can derive a -matching PDA using the note creator's public key and the ID as seeds (that's -what we're assuming was used when creating the note). That way a user couldn't -accidentally pass in a PDA account for the wrong note or, more importantly, that -the user isn't passing in a PDA account that represents somebody else's note -entirely. +For instance, in the note-taking app, you can derive the expected PDA using the +creator's public key and note ID, and then validate that it matches the provided +account: ```rust -let (pda, bump_seed) = Pubkey::find_program_address(&[note_creator.key.as_ref(), id.as_bytes().as_ref(),], program_id); +let (expected_pda, bump_seed) = Pubkey::find_program_address( + &[ + note_creator.key.as_ref(), + id.as_bytes().as_ref(), + ], + program_id +); -if pda != *note_pda.key { +if expected_pda != *note_pda.key { msg!("Invalid seeds for PDA"); return Err(ProgramError::InvalidArgument) } ``` -### Data validation +This check prevents a user from accidentally (or maliciously) passing the wrong +PDA or one that belongs to someone else. By validating the PDA's derivation, you +ensure the program is acting on the correct account. -Similar to validating accounts, you should also validate any data provided by -the client. +### Data Validation -For example, you may have a game program where a user can allocate character -attribute points to various categories. You may have a maximum limit in each -category of 100, in which case you would want to verify that the existing -allocation of points plus the new allocation doesn't exceed the maximum. +Data validation ensures that the input provided to your program meets the +expected criteria. This is crucial for avoiding incorrect or malicious data that +could cause the program to behave unpredictably. + +For example, let's say your program allows users to allocate points to a +character's attributes, but each attribute has a maximum allowed value. Before +making any updates, you should check that the new allocation does not exceed the +defined limit: ```rust if character.agility + new_agility > 100 { @@ -235,8 +262,8 @@ if character.agility + new_agility > 100 { } ``` -Or, the character may have an allowance of attribute points they can allocate -and you want to make sure they don't exceed that allowance. +Similarly, you should check that the user is not exceeding their allowed number +of points: ```rust if attribute_allowance < new_agility { @@ -245,10 +272,9 @@ if attribute_allowance < new_agility { } ``` -Without these checks, program behavior would differ from what you expect. In -some cases, however, it's more than just an issue of undefined behavior. -Sometimes failure to validate data can result in security loopholes that are -financially devastating. +Without these validations, the program could end up in an undefined state or be +exploited by malicious actors, potentially causing financial loss or +inconsistent behavior. For example, imagine that the character referenced in these examples is an NFT. Further, imagine that the program allows the NFT to be staked to earn token @@ -260,45 +286,50 @@ stakers. #### Integer overflow and underflow -Rust integers have fixed sizes. This means they can only support a specific -range of numbers. An arithmetic operation that results in a higher or lower -value than what is supported by the range will cause the resulting value to wrap -around. For example, a `u8` only supports numbers 0-255, so the result of -addition that would be 256 would actually be 0, 257 would be 1, etc. +One of the common pitfalls when working with integers in Rust (and in Solana +programs) is handling integer overflow and underflow. Rust integers have fixed +sizes and can only hold values within a certain range. When a value exceeds that +range, it wraps around, leading to unexpected results. -This is always important to keep in mind, but especially so when dealing with -any code that represents true value, such as depositing and withdrawing tokens. +For example, with a `u8` (which holds values between 0 and 255), adding 1 to 255 +results in a value of 0 (overflow). To avoid this, you should use checked math +functions like +[`checked_add()`](https://doc.rust-lang.org/std/primitive.u8.html#method.checked_add) +and +[`checked_sub()`](https://doc.rust-lang.org/std/primitive.u8.html#method.checked_sub): To avoid integer overflow and underflow, either: 1. Have logic in place that ensures overflow or underflow _cannot_ happen or -2. Use checked math like `checked_add` instead of `+` +2. Use checked math like `checked_add()` instead of `+` + ```rust let first_int: u8 = 5; let second_int: u8 = 255; - let sum = first_int.checked_add(second_int); + let sum = first_int.checked_add(second_int) + .ok_or(ProgramError::ArithmeticOverflow)?; ``` ## Lab -Let's practice together with the Movie Review program we've worked on in -previous lessons. No worries if you're just jumping into this lesson without -having done the previous lesson - it should be possible to follow along either -way. +In this lab, we will build upon the Movie Review program that allows users to +store movie reviews in PDA accounts. If you haven't completed the previous +lessons +[deserialize instruction data](/content/courses/native-onchain-development/deserialize-instruction-data.md) +and +[program state management](/content/courses/native-onchain-development/program-state-management.md), +don't worry—this guide is self-contained. -As a refresher, the Movie Review program lets users store movie reviews in PDA -accounts. Last lesson, we finished implementing the basic functionality of -adding a movie review. Now, we'll add some security checks to the functionality -we've already created and add the ability to update a movie review in a secure -manner. - -Just as before, we'll be using [Solana Playground](https://beta.solpg.io/) to -write, build, and deploy our code. +The Movie Review program lets users add and update reviews in PDA accounts. In +previous lessons, we implemented basic functionality for adding reviews. Now, +we'll add security checks and implement an update feature in a secure manner. +We'll use [Solana Playground](https://beta.solpg.io/) to write, build, and +deploy our program. ### 1. Get the starter code To begin, you can find -[the movie review starter code](https://beta.solpg.io/62b552f3f6273245aca4f5c9). +[the movie review starter code](https://beta.solpg.io/62b552f3f6273245aca4f5c9). If you've been following along with the Movie Review labs, you'll notice that we've refactored our program. @@ -317,12 +348,12 @@ defining custom errors. The complete file structure is as follows: - **state.rs -** serialize and deserialize state - **error.rs -** custom program errors -In addition to some changes to file structure, we've updated a small amount of -code that will let this lab be more focused on security without having you write -unnecessary boiler plate. +In addition to some changes to the file structure, we've updated a small amount +of code that will let this lab be more focused on security without having you +write unnecessary boilerplate. Since we'll be allowing updates to movie reviews, we also changed `account_len` -in the `add_movie_review` function (now in `processor.rs`). Instead of +in the `add_movie_review()` function (now in `processor.rs`). Instead of calculating the size of the review and setting the account length to only as large as it needs to be, we're simply going to allocate 1000 bytes to each review account. This way, we don't have to worry about reallocating size or @@ -356,8 +387,7 @@ that checks the `is_initialized` field on the `MovieAccountState` struct. `MovieAccountState` has a known size and provides for some compiler optimizations. -```rust -// inside state.rs +```rust filename="state.rs" impl Sealed for MovieAccountState {} impl IsInitialized for MovieAccountState { @@ -367,27 +397,21 @@ impl IsInitialized for MovieAccountState { } ``` -Before moving on, make sure you have a solid grasp on the current state of the +Before moving on, make sure you have a solid grasp of the current state of the program. Look through the code and spend some time thinking through any spots that are confusing to you. It may be helpful to compare the starter code to the [solution code from the previous lesson](https://beta.solpg.io/62b23597f6273245aca4f5b4). ### 2. Custom Errors -Let's begin by writing our custom program errors. We'll need errors that we can -use in the following situations: - -- The update instruction has been invoked on an account that hasn't been - initialized yet -- The provided PDA doesn't match the expected or derived PDA -- The input data is larger than the program allows -- The rating provided does not fall in the 1-5 range +We'll define custom errors to handle cases like uninitialized accounts, invalid +PDA matches, exceeding data limits, and invalid ratings (ratings must be between +1 and 5). These errors will be added to the `error.rs` file: The starter code includes an empty `error.rs` file. Open that file and add errors for each of the above cases. -```rust -// inside error.rs +```rust filename="error.rs" use solana_program::{program_error::ProgramError}; use thiserror::Error; @@ -414,19 +438,16 @@ impl From for ProgramError { } ``` -Note that in addition to adding the error cases, we also added the -implementation that lets us convert our error into a `ProgramError` type as -needed. +Note that in addition to adding the error cases, we also added an implementation +that lets us convert our error into a `ProgramError` type as needed. -Before moving on, let's bring `ReviewError` into scope in the `processor.rs`. We -will be using these errors shortly when we add our security checks. +After adding the errors, import `ReviewError` in `processor.rs` to use them. -```rust -// inside processor.rs +```rust filename="processor.rs" use crate::error::ReviewError; ``` -### 3. Add security checks to `add_movie_review` +### 3. Add Security Checks to add_movie_review Now that we have errors to use, let's implement some security checks to our `add_movie_review` function. @@ -438,7 +459,7 @@ also a signer on the transaction. This ensures that you can't submit movie reviews impersonating somebody else. We'll put this check right after iterating through the accounts. -```rust +```rust filename="processor.rs" let account_info_iter = &mut accounts.iter(); let initializer = next_account_info(account_info_iter)?; @@ -455,11 +476,11 @@ if !initializer.is_signer { Next, let's make sure the `pda_account` passed in by the user is the `pda` we expect. Recall we derived the `pda` for a movie review using the `initializer` -and `title` as seeds. Within our instruction we'll derive the `pda` again and +and `title` as seeds. Within our instruction, we'll derive the `pda` again and then check if it matches the `pda_account`. If the addresses do not match, we'll return our custom `InvalidPDA` error. -```rust +```rust filename="processor.rs" // Derive PDA and check that it matches client let (pda, _bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), account_data.title.as_bytes().as_ref(),], program_id); @@ -477,7 +498,7 @@ We'll start by making sure `rating` falls within the 1 to 5 scale. If the rating provided by the user outside of this range, we'll return our custom `InvalidRating` error. -```rust +```rust filename="processor.rs" if rating > 5 || rating < 1 { msg!("Rating cannot be higher than 5"); return Err(ReviewError::InvalidRating.into()) @@ -488,7 +509,7 @@ Next, let's check that the content of the review does not exceed the 1000 bytes we've allocated for the account. If the size exceeds 1000 bytes, we'll return our custom `InvalidDataLength` error. -```rust +```rust filename="processor.rs" let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len()); if total_len > 1000 { msg!("Data length is larger than 1000 bytes"); @@ -496,20 +517,20 @@ if total_len > 1000 { } ``` -Lastly, let's checking if the account has already been initialized by calling -the `is_initialized` function we implemented for our `MovieAccountState`. If the +Lastly, let's check if the account has already been initialized by calling the +`is_initialized` function we implemented for our `MovieAccountState`. If the account already exists, then we will return an error. -```rust +```rust filename="processor.rs" if account_data.is_initialized() { msg!("Account already initialized"); return Err(ProgramError::AccountAlreadyInitialized); } ``` -All together, the `add_movie_review` function should look something like this: +Altogether, the `add_movie_review()` function should look something like this: -```rust +```rust filename="processor.rs" pub fn add_movie_review( program_id: &Pubkey, accounts: &[AccountInfo], @@ -592,17 +613,12 @@ pub fn add_movie_review( } ``` -### 4. Support movie review updates in `MovieInstruction` - -Now that `add_movie_review` is more secure, let's turn our attention to -supporting the ability to update a movie review. +### 4. Support Movie Review Updates in MovieInstruction -Let's begin by updating `instruction.rs`. We'll start by adding an -`UpdateMovieReview` variant to `MovieInstruction` that includes embedded data -for the new title, rating, and description. +Next, we'll modify `instruction.rs` to add support for updating movie reviews. +We'll introduce a new `UpdateMovieReview()` variant in `MovieInstruction`: -```rust -// inside instruction.rs +```rust filename="instruction.rs" pub enum MovieInstruction { AddMovieReview { title: String, @@ -618,13 +634,12 @@ pub enum MovieInstruction { ``` The payload struct can stay the same since aside from the variant type, the -instruction data is the same as what we used for `AddMovieReview`. +instruction data is the same as what we used for `AddMovieReview()`. -Lastly, in the `unpack` function we need to add `UpdateMovieReview` to the match -statement. +We'll also update the `unpack()` function to handle `UpdateMovieReview()`. -```rust -// inside instruction.rs +```rust filename="instruction.rs" +// Inside instruction.rs impl MovieInstruction { pub fn unpack(input: &[u8]) -> Result { let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?; @@ -644,38 +659,38 @@ impl MovieInstruction { } ``` -### 5. Define `update_movie_review` function +### 5. Define update_movie_review Function Now that we can unpack our `instruction_data` and determine which instruction of -the program to run, we can add `UpdateMovieReview` to the match statement in -the `process_instruction` function in the `processor.rs` file. +the program to run, we can add `UpdateMovieReview()` to the match statement in +the `process_instruction()` function in the `processor.rs` file. -```rust -// inside processor.rs +```rust filename="processor.rs" +// Inside processor.rs pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8] ) -> ProgramResult { - // unpack instruction data + // Unpack instruction data let instruction = MovieInstruction::unpack(instruction_data)?; match instruction { MovieInstruction::AddMovieReview { title, rating, description } => { add_movie_review(program_id, accounts, title, rating, description) }, - // add UpdateMovieReview to match against our new data structure + // Add UpdateMovieReview to match against our new data structure MovieInstruction::UpdateMovieReview { title, rating, description } => { - // make call to update function that we'll define next + // Make call to update function that we'll define next update_movie_review(program_id, accounts, title, rating, description) } } } ``` -Next, we can define the new `update_movie_review` function. The definition +Next, we can define the new `update_movie_review()` function. The definition should have the same parameters as the definition of `add_movie_review`. -```rust +```rust filename="processor.rs" pub fn update_movie_review( program_id: &Pubkey, accounts: &[AccountInfo], @@ -687,16 +702,16 @@ pub fn update_movie_review( } ``` -### 6. Implement `update_movie_review` function +### 6. Implement update_movie_review Function All that's left now is to fill in the logic for updating a movie review. Only let's make it secure from the start. -Just like the `add_movie_review` function, let's start by iterating through the -accounts. The only accounts we'll need are the first two: `initializer` and +Just like the `add_movie_review()` function, let's start by iterating through +the accounts. The only accounts we'll need are the first two: `initializer` and `pda_account`. -```rust +```rust filename="processor.rs" pub fn update_movie_review( program_id: &Pubkey, accounts: &[AccountInfo], @@ -722,7 +737,7 @@ Before we continue, let's implement some basic security checks. We'll start with an ownership check on for `pda_account` to verify that it is owned by our program. If it isn't, we'll return an `InvalidOwner` error. -```rust +```rust filename="processor.rs" if pda_account.owner != program_id { return Err(ProgramError::InvalidOwner) } @@ -736,7 +751,7 @@ data for a movie review, we want to ensure that the original `initializer` of the review has approved the changes by signing the transaction. If the `initializer` did not sign the transaction, we'll return an error. -```rust +```rust filename="processor.rs" if !initializer.is_signer { msg!("Missing required signature"); return Err(ProgramError::MissingRequiredSignature) @@ -748,9 +763,9 @@ if !initializer.is_signer { Next, let's check that the `pda_account` passed in by the user is the PDA we expect by deriving the PDA using `initializer` and `title` as seeds. If the addresses do not match, we'll return our custom `InvalidPDA` error. We'll -implement this the same way we did in the `add_movie_review` function. +implement this the same way we did in the `add_movie_review()` function. -```rust +```rust filename="processor.rs" // Derive PDA and check that it matches client let (pda, _bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(), account_data.title.as_bytes().as_ref(),], program_id); @@ -760,13 +775,13 @@ if pda != *pda_account.key { } ``` -#### Unpack `pda_account` and perform data validation +#### Unpack pda_account and Perform Data Validation Now that our code ensures we can trust the passed in accounts, let's unpack the `pda_account` and perform some data validation. We'll start by unpacking `pda_account` and assigning it to a mutable variable `account_data`. -```rust +```rust filename="processor.rs" msg!("unpacking state account"); let mut account_data = try_from_slice_unchecked::(&pda_account.data.borrow()).unwrap(); msg!("borrowed account data"); @@ -785,13 +800,13 @@ if !account_data.is_initialized() { ``` Next, we need to validate the `rating`, `title`, and `description` data just -like in the `add_movie_review` function. We want to limit the `rating` to a +like in the `add_movie_review()` function. We want to limit the `rating` to a scale of 1 to 5 and limit the overall size of the review to be fewer than 1000 -bytes. If the rating provided by the user outside of this range, then we'll +bytes. If the rating provided by the user is outside of this range, then we'll return our custom `InvalidRating` error. If the review is too long, then we'll return our custom `InvalidDataLength` error. -```rust +```rust filename="processor.rs" if rating > 5 || rating < 1 { msg!("Rating cannot be higher than 5"); return Err(ReviewError::InvalidRating.into()) @@ -810,7 +825,7 @@ Now that we've implemented all of the security checks, we can finally update the movie review account by updating `account_data` and re-serializing it. At that point, we can return `Ok` from our program. -```rust +```rust filename="processor.rs" account_data.rating = rating; account_data.description = description; @@ -819,11 +834,11 @@ account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?; Ok(()) ``` -All together, the `update_movie_review` function should look something like the -code snippet below. We've included some additional logging for clarity in +All together, the `update_movie_review()` function should look something like +the code snippet below. We've included some additional logging for clarity in debugging. -```rust +```rust filename="processor.rs" pub fn update_movie_review( program_id: &Pubkey, accounts: &[AccountInfo], @@ -900,7 +915,7 @@ pub fn update_movie_review( We're ready to build and upgrade our program! You can test your program by submitting a transaction with the right instruction data. For that, feel free to use this -[frontend](https://github.com/Unboxed-Software/solana-movie-frontend/tree/solution-update-reviews). +[frontend](https://github.com/solana-developers/movie-frontend/tree/solution-update-reviews). Remember, to make sure you're testing the right program you'll need to replace `MOVIE_REVIEW_PROGRAM_ID` with your program ID in `Form.tsx` and `MovieCoordinator.ts`. @@ -914,7 +929,7 @@ continuing. Now it's your turn to build something independently by building on top of the Student Intro program that you've used in previous lessons. If you haven't been -following along or haven't saved your code from before, feel free to use +following along or haven't saved your code before, feel free to use [this starter code](https://beta.solpg.io/62b11ce4f6273245aca4f5b2). The Student Intro program is a Solana Program that lets students introduce @@ -933,6 +948,7 @@ Note that your code may look slightly different than the solution code depending on the checks you implement and the errors you write. + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=3dfb98cc-7ba9-463d-8065-7bdb1c841d43)!