Safety First
If you look into the history of "hacks" and "bugs" that happen in the blockchain world, a lot of it is associated with some kind of "unsafe" code.
We need to keep our blockchain logic safe, and Rust is designed to handle it well.
Errors
When talking about handling safe math, we will start to introduce and use errors.
Do Not Panic!
If there is only one thing you remember after this whole tutorial, it should be this fact:
You cannot panic inside the runtime.
As a runtime developer, you are building logic in the low level parts of your blockchain.
A smart contract system must be able to handle malicious developers, but this comes at a performance cost.
When you program directly in the runtime, you get the highest performance possible, but you are also expected to be a competent developer and a good actor.
In short, if you introduce a panic in your code, you make your blockchain vulnerable to DDoS attacks.
But there is no reason you would ever need to panic because Rust has a great error handling system that we take advantage of in FRAME.
Pallet Errors
All of our callable functions use the DispatchResult
type. This means that we can always propagate up any errors that our Pallet runs into, and handle them properly, versus needing to panic.
The DispatchResult
type expects either Ok(())
or Err(DispatchError)
.
The DispatchError
type has a few variants that you can easily construct / use.
For example, if you want to be a little lazy, you can simply return a &'static str
:
#![allow(unused)] fn main() { fn always_error() -> DispatchResult { return Err("this function always errors".into()) } }
But the better option is to return a custom Pallet Error:
#![allow(unused)] fn main() { fn custom_error() -> DispatchResult { return Err(Error::<T>::CustomPalletError.into()) } }
Notice in both of these cases we had to call into()
to convert our input type into the DispatchError
type.
To create CustomPalletError
or whatever error you want, you simply add a new variants to the enum Error<T>
type.
#![allow(unused)] fn main() { #[pallet::error] pub enum Error<T> { /// This is a description for the error. /// /// This description can be shown to the user in UIs, so make it descriptive. CustomPalletError, } }
We will show you the common ergonomic ways to use Pallet Errors going forward.
Math
Unsafe Math
The basic math operators in Rust are unsafe.
Imagine our CountForKitties
was already at the limit of u32::MAX
. What would happen if we tried to call mint
one more time?
We would get an overflow!
In tests u32::MAX + 1
will actually trigger a panic, but in a release
build, this overflow will happen silently...
And this would be really bad. Now our count would be back to 0, and if we had any logic which depended on this count being accurate, that logic would be broken.
In blockchain systems, these can literally be billion dollar bugs, so let's look at how we can do math safely.
Checked Math
The first choice for doing safe math is to use checked_*
APIs, for example checked_add
.
The checked math APIs will check if there are any underflows or overflows, and return None
in those cases. Otherwise, if the math operation is calculated without error, it returns Some(result)
.
Here is a verbose way you could handle checked math in a Pallet:
#![allow(unused)] fn main() { let final_result: u32 = match value_a.checked_add(value_b) { Some(result) => result, None => return Err(Error::<T>::CustomPalletError.into()), }; }
You can see how we can directly assign the u32
value to final_result
, otherwise it will return an error.
We can also do this as a one-liner, which is more ergonomic and preferred:
#![allow(unused)] fn main() { let final_result: u32 = value_a.checked_add(value_b).ok_or(Error::<T>::CustomPalletError)?; }
This is exactly how you should be writing all the safe math inside your Pallet.
Note that we didn't need to call .into()
in this case, because ?
already does this!
Saturating Math
The other option for safe math is to use saturating_*
APIs, for example saturating_add
.
This option is useful because it is safe and does NOT return an Option
.
Instead, it performs the math operations and keeps the value at the numerical limits, rather than overflowing. For example:
#![allow(unused)] fn main() { let value_a: u32 = 1; let value_b: u32 = u32::MAX; let result: u32 = value_a.saturating_add(value_b); assert!(result == u32::MAX); }
This generally is NOT the preferred API to use because usually you want to handle situations where an overflow would occur. Overflows and underflows usually indicate something "bad" is happening.
However, there are times where you need to do math inside of functions where you cannot return a Result, and for that, saturating math might make sense.
There are also times where you might want to perform the operation no matter that an underflow / overflow would occur. For example, imagine you made a function slash
which slashes the balance of a malicious user. Your slash function may have some input parameter amount
which says how much we should slash from the user.
In a situation like this, it would make sense to use saturating_sub
because we definitely want to slash as much as we can, even if we intended to slash more. The alternative would be returning an error, and not slashing anything!
Anyway, every bone in your body should generally prefer to use the checked_*
APIs, and handle all errors explicitly, but this is yet another tool in your pocket when it makes sense to use it.
Your Turn
We covered a lot in this section, but the concepts here are super important.
Feel free to read this section again right now, and again at the end of the tutorial.
Now that you know how to ergonomically do safe math, update your Pallet to handle the mint
logic safely and return a custom Pallet Error if an overflow would occur.
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; impl<T: Config> Pallet<T> { pub fn mint(owner: T::AccountId) -> DispatchResult { let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0); /* 🚧 TODO 🚧: Update this logic to use safe math. */ let new_count = current_count + 1; CountForKitties::<T>::set(Some(new_count)); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, } #[pallet::error] pub enum Error<T> { /* 🚧 TODO 🚧: - Introduce a new error `TooManyKitties`. */ } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; Self::mint(who)?; Ok(()) } } } }
#![allow(unused)] fn main() { use super::*; use frame::prelude::*; impl<T: Config> Pallet<T> { pub fn mint(owner: T::AccountId) -> DispatchResult { let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0); let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?; CountForKitties::<T>::set(Some(new_count)); Self::deposit_event(Event::<T>::Created { owner }); Ok(()) } } }
#![allow(unused)] #![cfg_attr(not(feature = "std"), no_std)] fn main() { mod impls; mod tests; use frame::prelude::*; pub use pallet::*; #[frame::pallet(dev_mode)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet<T>(core::marker::PhantomData<T>); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; } #[pallet::storage] pub(super) type CountForKitties<T: Config> = StorageValue<Value = u32>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { Created { owner: T::AccountId }, } #[pallet::error] pub enum Error<T> { TooManyKitties, } #[pallet::call] impl<T: Config> Pallet<T> { pub fn create_kitty(origin: OriginFor<T>) -> DispatchResult { let who = ensure_signed(origin)?; Self::mint(who)?; Ok(()) } } } }
#![allow(unused)] fn main() { // Tests for the Kitties Pallet. // // Normally this file would be split into two parts: `mock.rs` and `tests.rs`. // The `mock.rs` file would contain all the setup code for our `TestRuntime`. // Then `tests.rs` would only have the tests for our pallet. // However, to minimize the project, these have been combined into this single file. // // Learn more about creating tests for Pallets: // https://paritytech.github.io/polkadot-sdk/master/polkadot_sdk_docs/guides/your_first_pallet/index.html // This flag tells rust to only run this file when running `cargo test`. #![cfg(test)] use crate as pallet_kitties; use crate::*; use frame::deps::sp_io; use frame::runtime::prelude::*; use frame::testing_prelude::*; use frame::traits::fungible::*; type Balance = u64; type Block = frame_system::mocking::MockBlock<TestRuntime>; // In our "test runtime", we represent a user `AccountId` with a `u64`. // This is just a simplification so that we don't need to generate a bunch of proper cryptographic // public keys when writing tests. It is just easier to say "user 1 transfers to user 2". // We create the constants `ALICE` and `BOB` to make it clear when we are representing users below. const ALICE: u64 = 1; const BOB: u64 = 2; // Our blockchain tests only need 3 Pallets: // 1. System: Which is included with every FRAME runtime. // 2. PalletBalances: Which is manages your blockchain's native currency. (i.e. DOT on Polkadot) // 3. PalletKitties: The pallet you are building in this tutorial! construct_runtime! { pub struct TestRuntime { System: frame_system, PalletBalances: pallet_balances, PalletKitties: pallet_kitties, } } // Normally `System` would have many more configurations, but you can see that we use some macro // magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for TestRuntime { type Block = Block; type AccountData = pallet_balances::AccountData<Balance>; } // Normally `pallet_balances` would have many more configurations, but you can see that we use some // macro magic to automatically configure most of the pallet for a "default test configuration". #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] impl pallet_balances::Config for TestRuntime { type AccountStore = System; type Balance = Balance; } // This is the configuration of our Pallet! If you make changes to the pallet's `trait Config`, you // will also need to update this configuration to represent that. impl pallet_kitties::Config for TestRuntime { type RuntimeEvent = RuntimeEvent; } // We need to run most of our tests using this function: `new_test_ext().execute_with(|| { ... });` // It simulates the blockchain database backend for our tests. // If you forget to include this and try to access your Pallet storage, you will get an error like: // "`get_version_1` called outside of an Externalities-provided environment." pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::<TestRuntime>::default() .build_storage() .unwrap() .into() } #[test] fn starting_template_is_sane() { new_test_ext().execute_with(|| { let event = Event::<TestRuntime>::Created { owner: ALICE }; let _runtime_event: RuntimeEvent = event.into(); let _call = Call::<TestRuntime>::create_kitty {}; let result = PalletKitties::create_kitty(RuntimeOrigin::signed(BOB)); assert_ok!(result); }); } #[test] fn system_and_balances_work() { // This test will just sanity check that we can access `System` and `PalletBalances`. new_test_ext().execute_with(|| { // We often need to set `System` to block 1 so that we can see events. System::set_block_number(1); // We often need to add some balance to a user to test features which needs tokens. assert_ok!(PalletBalances::mint_into(&ALICE, 100)); assert_ok!(PalletBalances::mint_into(&BOB, 100)); }); } #[test] fn create_kitty_checks_signed() { new_test_ext().execute_with(|| { // The `create_kitty` extrinsic should work when being called by a user. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // The `create_kitty` extrinsic should fail when being called by an unsigned message. assert_noop!(PalletKitties::create_kitty(RuntimeOrigin::none()), DispatchError::BadOrigin); }) } #[test] fn create_kitty_emits_event() { new_test_ext().execute_with(|| { // We need to set block number to 1 to view events. System::set_block_number(1); // Execute our call, and ensure it is successful. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Assert the last event by our blockchain is the `Created` event with the correct owner. System::assert_last_event(Event::<TestRuntime>::Created { owner: 1 }.into()); }) } #[test] fn count_for_kitties_created_correctly() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `None`. assert_eq!(CountForKitties::<TestRuntime>::get(), None); // You can `set` the value using an `Option<u32>`. CountForKitties::<TestRuntime>::set(Some(1337u32)); // You can `put` the value directly with a `u32`. CountForKitties::<TestRuntime>::put(1337u32); // Check that the value is now in storage. assert_eq!(CountForKitties::<TestRuntime>::get(), Some(1337u32)); }) } #[test] fn mint_increments_count_for_kitty() { new_test_ext().execute_with(|| { // Querying storage before anything is set will return `None`. assert_eq!(CountForKitties::<TestRuntime>::get(), None); // Call `create_kitty` which will call `mint`. assert_ok!(PalletKitties::create_kitty(RuntimeOrigin::signed(ALICE))); // Now the storage should be `Some(1)` assert_eq!(CountForKitties::<TestRuntime>::get(), Some(1)); }) } #[test] fn mint_errors_when_overflow() { new_test_ext().execute_with(|| { // Set the count to the largest value possible. CountForKitties::<TestRuntime>::set(Some(u32::MAX)); // `create_kitty` should not succeed because of safe math. assert_noop!( PalletKitties::create_kitty(RuntimeOrigin::signed(1)), Error::<TestRuntime>::TooManyKitties ); }) } }
diff --git a/src/impls.rs b/src/impls.rs
index 9739330..c550bc8 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -4,6 +4,7 @@ use frame::prelude::*;
impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId) -> DispatchResult {
let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0);
+ /* 🚧 TODO 🚧: Update this logic to use safe math. */
let new_count = current_count + 1;
CountForKitties::<T>::set(Some(new_count));
Self::deposit_event(Event::<T>::Created { owner });
diff --git a/src/lib.rs b/src/lib.rs
index 90242f6..8edcc9b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -28,7 +28,11 @@ pub mod pallet {
}
#[pallet::error]
- pub enum Error<T> {}
+ pub enum Error<T> {
+ /* 🚧 TODO 🚧:
+ - Introduce a new error `TooManyKitties`.
+ */
+ }
#[pallet::call]
impl<T: Config> Pallet<T> {
diff --git a/src/impls.rs b/src/impls.rs
index c550bc8..7277e36 100644
--- a/src/impls.rs
+++ b/src/impls.rs
@@ -4,8 +4,7 @@ use frame::prelude::*;
impl<T: Config> Pallet<T> {
pub fn mint(owner: T::AccountId) -> DispatchResult {
let current_count: u32 = CountForKitties::<T>::get().unwrap_or(0);
- /* 🚧 TODO 🚧: Update this logic to use safe math. */
- let new_count = current_count + 1;
+ let new_count = current_count.checked_add(1).ok_or(Error::<T>::TooManyKitties)?;
CountForKitties::<T>::set(Some(new_count));
Self::deposit_event(Event::<T>::Created { owner });
Ok(())
diff --git a/src/lib.rs b/src/lib.rs
index 8edcc9b..2c11f46 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,9 +29,7 @@ pub mod pallet {
#[pallet::error]
pub enum Error<T> {
- /* 🚧 TODO 🚧:
- - Introduce a new error `TooManyKitties`.
- */
+ TooManyKitties,
}
#[pallet::call]
diff --git a/src/tests.rs b/src/tests.rs
index 6de2fcc..fd8e478 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -143,3 +143,16 @@ fn mint_increments_count_for_kitty() {
assert_eq!(CountForKitties::<TestRuntime>::get(), Some(1));
})
}
+
+#[test]
+fn mint_errors_when_overflow() {
+ new_test_ext().execute_with(|| {
+ // Set the count to the largest value possible.
+ CountForKitties::<TestRuntime>::set(Some(u32::MAX));
+ // `create_kitty` should not succeed because of safe math.
+ assert_noop!(
+ PalletKitties::create_kitty(RuntimeOrigin::signed(1)),
+ Error::<TestRuntime>::TooManyKitties
+ );
+ })
+}