Focus mode
If you’re going to build, you have to prepare to defend. In this lesson, we’ll cover basic pitfalls to look out for. This is far from a comprehensive overview of program security, but it will help you to think like an attacker and ask the important question: how do I break this?
Rust has a very powerful error handling system. You've already run into some of the rules in place and how the compiler forces you to handle unhappy paths.
Here's how we'd create custom errors for our note taking program:
use solana_program::{program_error::ProgramError};
use thiserror::Error;
#[derive(Error)]
pub enum NoteError {
#[error("Wrong note owner")]
Forbidden,
#[error("Text is too long")]
}
The derive macro attribute takes the error trait and applies it to NoteError
enum, giving it a default implementation to make them errors.
We'll then give each error type its own #[error("...")]
notation to provide an error message.
Returning custom errors
Errors returned by the program must be of type ProgramError
use impl
to convert our custom error and the ProgramError
type
In a Solana Program we can only return errors of type ProgramError
from the solana_program
crate. We can implement the From
trait to convert our custom error into a ProgramError
type.
impl From<NoteError> for ProgramError {
fn from(e: NoteError) -> Self {
ProgramError::Custom(e as u32)
}
}
Then when we need to actually return the error, we use the into()
method to convert the error into an instance of ProgramError
if pda != *note_pda.key {
return Err(NoteError::Forbidden.into());
}
There are a few basic security measures you can take to make your program more secure:
Generally speaking, you should always validate the inputs you receive from a user. This is especially important when you're dealing with user-provided data. Remember - programs don't store state. They don't know who their owner is and they won't check who is calling them unless you tell them to.
Ownership ChecksAn ownership check verifies that an account is owned by the expected program. Gotta make sure only you can hit it
A user can potentially send data which matches the data struct of an account but created by a different program.
if note_pda.owner != program_id {
return Err(ProgramError::InvalidNoteAccount);
}
Signer checks A signer check simply verifies that an account has signed a transaction
if !initializer.is_signer {
msg!("Missing required signature");
return Err(ProgramError::MissingRequiredSignature)
}
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.
Data validation When appropriate, you should also validate instruction data provided by the client.
For example, you may have a game program where a user can allocate character attribute points to various categories.
You may want to verify that the existing allocation of points plus the new allocation doesn't exceed a maximum
if character.agility + new_agility > 100 {
msg!("Attribute points cannot exceed 100");
return Err(AttributeError::TooHigh.into())
}
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.
if attribute_allowance > new_agility {
msg!("Trying to allocate more points than allowed");
return Err(AttributeError::ExceedsAllowance.into())
}
Integer overflow and underflow Rust integers have fixed sizes, meaning 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.
If you've ever heard of Nuclear Ghandi from the video game Civilization - this is what causes it. He's supposed to be a really chill and peaceful leader with a really low aggression stat. But the devs didn't validate that the stat wouldn't overflow, so it went from 0 to 255 and he became a nuclear warlord with max aggression instead. Oops.
To avoid integer overflow and underflow, either:
checked_add
instead of +
let first_int: u8 = 5;
let second_int: u8 = 255;
let sum = first_int.checked_add(second_int);
Think of all the programs out there that aren't taking even these basic security measures. Imagine the bug bounties 🥵🤑
Programs to Accelerate Your Progress in a Software Career
Join our 4-8 month intensive Patika+ bootcamps, start with the fundamentals and gain comprehensive knowledge to kickstart your software career!
You need to enroll in the course to be able to comment!