Build Flipper Contract
Learn ink! fundamentals by building a simple Flipper contract - your first step into WebAssembly smart contract development on Mandala Testnet.
Overview
The Flipper contract is the "Hello World" of ink! smart contracts. It demonstrates core concepts including state storage, functions, events, and testing. This contract maintains a boolean value that can be toggled between true
and false
.
Contract Concepts
What You'll Learn
- Storage management - How to store state on-chain
- Constructor patterns - Different ways to initialize contracts
- Public functions - Creating callable contract methods
- Events - Emitting events for off-chain monitoring
- Unit testing - Testing contract logic
- Error handling - Managing contract errors
Understanding the Contract Code
Basic Contract Structure
Open lib.rs
to see the complete flipper contract:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[ink::contract]
mod flipper {
/// Defines the storage of your contract.
/// Add new fields to the below struct in order
/// to add new static storage fields to your contract.
#[ink(storage)]
pub struct Flipper {
/// Stores a single `bool` value on the storage.
value: bool,
}
impl Flipper {
/// Constructor that initializes the `bool` value to the given `init_value`.
#[ink(constructor)]
pub fn new(init_value: bool) -> Self {
Self { value: init_value }
}
/// Constructor that initializes the `bool` value to `false`.
///
/// Constructors can delegate to other constructors.
#[ink(constructor)]
pub fn default() -> Self {
Self::new(Default::default())
}
/// A message that can be called on instantiated contracts.
/// This one flips the value of the stored `bool` from `true`
/// to `false` and vice versa.
#[ink(message)]
pub fn flip(&mut self) {
self.value = !self.value;
}
/// Simply returns the current value of our `bool`.
#[ink(message)]
pub fn get(&self) -> bool {
self.value
}
}
/// Unit tests in Rust are normally defined within such a `#[cfg(test)]`
/// module and test functions are marked with a `#[test]` attribute.
/// The below code is technically just normal Rust code.
#[cfg(test)]
mod tests {
/// Imports all the definitions from the outer scope so we can use them here.
use super::*;
/// We test if the default constructor does its job.
#[ink::test]
fn default_works() {
let flipper = Flipper::default();
assert_eq!(flipper.get(), false);
}
/// We test a simple use case of our contract.
#[ink::test]
fn it_works() {
let mut flipper = Flipper::new(false);
assert_eq!(flipper.get(), false);
flipper.flip();
assert_eq!(flipper.get(), true);
}
}
/// This is how you'd write end-to-end (E2E) or integration tests for ink! contracts.
///
/// When running these you need to make sure that you:
/// - Compile the tests with the `e2e-tests` feature flag enabled (`--features e2e-tests`)
/// - Are running a Substrate node which contains `pallet-contracts` in the background
#[cfg(all(test, feature = "e2e-tests"))]
mod e2e_tests {
/// Imports all the definitions from the outer scope so we can use them here.
use super::*;
/// A helper function used for calling contract messages.
use ink_e2e::ContractsBackend;
/// The End-to-End test `Result` type.
type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// We test that we can upload and instantiate the contract using its default constructor.
#[ink_e2e::test]
async fn default_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
// Given
let mut constructor = FlipperRef::default();
// When
let contract = client
.instantiate("flipper", &ink_e2e::alice(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let call_builder = contract.call_builder::<Flipper>();
// Then
let get = call_builder.get();
let get_result = client.call(&ink_e2e::alice(), &get).dry_run().await?;
assert!(matches!(get_result.return_value(), false));
Ok(())
}
/// We test that we can read and write a value from the on-chain contract.
#[ink_e2e::test]
async fn it_works(mut client: ink_e2e::Client<C, E>) -> E2EResult<()> {
// Given
let mut constructor = FlipperRef::new(false);
let contract = client
.instantiate("flipper", &ink_e2e::bob(), &mut constructor)
.submit()
.await
.expect("instantiate failed");
let mut call_builder = contract.call_builder::<Flipper>();
let get = call_builder.get();
let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?;
assert!(matches!(get_result.return_value(), false));
// When
let flip = call_builder.flip();
let _flip_result = client
.call(&ink_e2e::bob(), &flip)
.submit()
.await
.expect("flip failed");
// Then
let get = call_builder.get();
let get_result = client.call(&ink_e2e::bob(), &get).dry_run().await?;
assert!(matches!(get_result.return_value(), true));
Ok(())
}
}
}
Contract Anatomy
Key Components:
Storage (#[ink(storage)]
):
#[ink(storage)]
pub struct Flipper {
value: bool, // Single boolean storage field
}
Constructors (#[ink(constructor)]
):
// Constructor with parameter
#[ink(constructor)]
pub fn new(init_value: bool) -> Self {
Self { value: init_value }
}
// Default constructor
#[ink(constructor)]
pub fn default() -> Self {
Self::new(Default::default())
}
Messages (#[ink(message)]
):
// Mutable message (changes state)
#[ink(message)]
pub fn flip(&mut self) {
self.value = !self.value;
}
// Read-only message
#[ink(message)]
pub fn get(&self) -> bool {
self.value
}
Enhanced Flipper Contract
Adding Events
Let's enhance the contract with events for better monitoring:
#![cfg_attr(not(feature = "std"), no_std)]
#[ink::contract]
mod flipper {
/// Defines the storage of your contract.
#[ink(storage)]
pub struct Flipper {
value: bool,
}
/// Event emitted when the value is flipped.
#[ink(event)]
pub struct Flipped {
#[ink(topic)]
from: bool,
#[ink(topic)]
to: bool,
}
/// Errors that can occur upon calling this contract.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// The same value was provided.
SameValue,
}
/// A type alias for the contract's result type.
pub type Result<T> = core::result::Result<T, Error>;
impl Flipper {
/// Constructor that initializes the `bool` value to the given `init_value`.
#[ink(constructor)]
pub fn new(init_value: bool) -> Self {
Self { value: init_value }
}
/// Constructor that initializes the `bool` value to `false`.
#[ink(constructor)]
pub fn default() -> Self {
Self::new(Default::default())
}
/// Flips the value of the stored `bool` from `true` to `false` and vice versa.
#[ink(message)]
pub fn flip(&mut self) -> Result<()> {
let old_value = self.value;
self.value = !self.value;
// Emit event
self.env().emit_event(Flipped {
from: old_value,
to: self.value,
});
Ok(())
}
/// Flips the value only if it's different from the provided value.
#[ink(message)]
pub fn flip_to(&mut self, value: bool) -> Result<()> {
if self.value == value {
return Err(Error::SameValue);
}
let old_value = self.value;
self.value = value;
self.env().emit_event(Flipped {
from: old_value,
to: self.value,
});
Ok(())
}
/// Simply returns the current value of our `bool`.
#[ink(message)]
pub fn get(&self) -> bool {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn default_works() {
let flipper = Flipper::default();
assert_eq!(flipper.get(), false);
}
#[ink::test]
fn it_works() {
let mut flipper = Flipper::new(false);
assert_eq!(flipper.get(), false);
assert!(flipper.flip().is_ok());
assert_eq!(flipper.get(), true);
}
#[ink::test]
fn flip_to_works() {
let mut flipper = Flipper::new(false);
// Should work - different value
assert!(flipper.flip_to(true).is_ok());
assert_eq!(flipper.get(), true);
// Should fail - same value
assert_eq!(flipper.flip_to(true), Err(Error::SameValue));
}
#[ink::test]
fn multiple_flips() {
let mut flipper = Flipper::new(false);
for i in 0..10 {
let expected = i % 2 == 1;
flipper.flip().unwrap();
assert_eq!(flipper.get(), expected);
}
}
}
}
Understanding Contract Metadata
Analyzing flipper.json
The metadata file contains crucial information for interacting with your contract:
{
"source": {
"hash": "0xdf1bf38c13f781e4f436d4c60f066425b92c802255f05c64c5acddb236e79f89",
"language": "ink! 5.1.1",
"compiler": "rustc 1.87.0",
"build_info": {
"build_mode": "Debug",
"cargo_contract_version": "5.0.3",
"rust_toolchain": "stable-aarch64-apple-darwin",
"wasm_opt_settings": {
"keep_debug_symbols": false,
"optimization_passes": "Z"
}
}
},
"contract": {
"name": "flipper",
"version": "0.1.0",
"authors": [
"[your_name] <[your_email]>"
]
},
"image": null,
"spec": {
"constructors": [
{
"args": [
{
"label": "init_value",
"type": {
"displayName": [
"bool"
],
"type": 0
}
}
],
"default": false,
"docs": [
"Constructor that initializes the `bool` value to the given `init_value`."
],
"label": "new",
"payable": false,
"returnType": {
"displayName": [
"ink_primitives",
"ConstructorResult"
],
"type": 2
},
"selector": "0x9bae9d5e"
},
{
"args": [],
"default": false,
"docs": [
"Constructor that initializes the `bool` value to `false`.",
"",
"Constructors can delegate to other constructors."
],
"label": "default",
"payable": false,
"returnType": {
"displayName": [
"ink_primitives",
"ConstructorResult"
],
"type": 2
},
"selector": "0xed4b9d1b"
}
],
"docs": [],
"environment": {
"accountId": {
"displayName": [
"AccountId"
],
"type": 6
},
"balance": {
"displayName": [
"Balance"
],
"type": 9
},
"blockNumber": {
"displayName": [
"BlockNumber"
],
"type": 12
},
"chainExtension": {
"displayName": [
"ChainExtension"
],
"type": 13
},
"hash": {
"displayName": [
"Hash"
],
"type": 10
},
"maxEventTopics": 4,
"staticBufferSize": 16384,
"timestamp": {
"displayName": [
"Timestamp"
],
"type": 11
}
},
"events": [],
"lang_error": {
"displayName": [
"ink",
"LangError"
],
"type": 4
},
"messages": [
{
"args": [],
"default": false,
"docs": [
" A message that can be called on instantiated contracts.",
" This one flips the value of the stored `bool` from `true`",
" to `false` and vice versa."
],
"label": "flip",
"mutates": true,
"payable": false,
"returnType": {
"displayName": [
"ink",
"MessageResult"
],
"type": 2
},
"selector": "0x633aa551"
},
{
"args": [],
"default": false,
"docs": [
" Simply returns the current value of our `bool`."
],
"label": "get",
"mutates": false,
"payable": false,
"returnType": {
"displayName": [
"ink",
"MessageResult"
],
"type": 5
},
"selector": "0x2f865bd9"
}
]
},
"storage": {
"root": {
"layout": {
"struct": {
"fields": [
{
"layout": {
"leaf": {
"key": "0x00000000",
"ty": 0
}
},
"name": "value"
}
],
"name": "Flipper"
}
},
"root_key": "0x00000000",
"ty": 1
}
},
"types": [
{
"id": 0,
"type": {
"def": {
"primitive": "bool"
}
}
},
{
"id": 1,
"type": {
"def": {
"composite": {
"fields": [
{
"name": "value",
"type": 0,
"typeName": "<bool as::ink::storage::traits::AutoStorableHint<::ink::storage\n::traits::ManualKey<2054318728u32, ()>,>>::Type"
}
]
}
},
"path": [
"flipper",
"flipper",
"Flipper"
]
}
},
{
"id": 2,
"type": {
"def": {
"variant": {
"variants": [
{
"fields": [
{
"type": 3
}
],
"index": 0,
"name": "Ok"
},
{
"fields": [
{
"type": 4
}
],
"index": 1,
"name": "Err"
}
]
}
},
"params": [
{
"name": "T",
"type": 3
},
{
"name": "E",
"type": 4
}
],
"path": [
"Result"
]
}
},
{
"id": 3,
"type": {
"def": {
"tuple": []
}
}
},
{
"id": 4,
"type": {
"def": {
"variant": {
"variants": [
{
"index": 1,
"name": "CouldNotReadInput"
}
]
}
},
"path": [
"ink_primitives",
"LangError"
]
}
},
{
"id": 5,
"type": {
"def": {
"variant": {
"variants": [
{
"fields": [
{
"type": 0
}
],
"index": 0,
"name": "Ok"
},
{
"fields": [
{
"type": 4
}
],
"index": 1,
"name": "Err"
}
]
}
},
"params": [
{
"name": "T",
"type": 0
},
{
"name": "E",
"type": 4
}
],
"path": [
"Result"
]
}
},
{
"id": 6,
"type": {
"def": {
"composite": {
"fields": [
{
"type": 7,
"typeName": "[u8; 32]"
}
]
}
},
"path": [
"ink_primitives",
"types",
"AccountId"
]
}
},
{
"id": 7,
"type": {
"def": {
"array": {
"len": 32,
"type": 8
}
}
}
},
{
"id": 8,
"type": {
"def": {
"primitive": "u8"
}
}
},
{
"id": 9,
"type": {
"def": {
"primitive": "u128"
}
}
},
{
"id": 10,
"type": {
"def": {
"composite": {
"fields": [
{
"type": 7,
"typeName": "[u8; 32]"
}
]
}
},
"path": [
"ink_primitives",
"types",
"Hash"
]
}
},
{
"id": 11,
"type": {
"def": {
"primitive": "u64"
}
}
},
{
"id": 12,
"type": {
"def": {
"primitive": "u32"
}
}
},
{
"id": 13,
"type": {
"def": {
"variant": {}
},
"path": [
"ink_env",
"types",
"NoChainExtension"
]
}
}
],
"version": 5
}
Key Metadata Elements
Constructors: Available ways to instantiate the contract
Messages: Callable functions with their signatures
Events: Emitted events with their parameters
Types: Type definitions used in the contract
Advanced Testing
Integration Testing
Create more comprehensive tests:
#[cfg(test)]
mod tests {
use super::*;
use ink::env::test;
#[ink::test]
fn test_event_emission() {
let mut flipper = Flipper::new(false);
// Flip and check event
flipper.flip().unwrap();
let emitted_events = test::recorded_events().collect::<Vec<_>>();
assert_eq!(emitted_events.len(), 1);
// Decode and verify event data
let decoded_event = <Flipped as scale::Decode>::decode(&mut &emitted_events[0].data[..])
.expect("Invalid event data");
assert_eq!(decoded_event.from, false);
assert_eq!(decoded_event.to, true);
}
#[ink::test]
fn test_error_handling() {
let mut flipper = Flipper::new(true);
// Should fail when trying to set same value
let result = flipper.flip_to(true);
assert_eq!(result, Err(Error::SameValue));
// State should remain unchanged
assert_eq!(flipper.get(), true);
}
#[ink::test]
fn test_constructor_variations() {
// Test parameterized constructor
let flipper1 = Flipper::new(true);
assert_eq!(flipper1.get(), true);
// Test default constructor
let flipper2 = Flipper::default();
assert_eq!(flipper2.get(), false);
}
}
End-to-End Testing
Test the complete contract lifecycle:
#[ink::test]
fn test_complete_lifecycle() {
// Deploy with initial state
let mut flipper = Flipper::new(false);
assert_eq!(flipper.get(), false);
// Perform multiple operations
let operations = [true, false, true, true, false];
for &target_value in &operations {
if flipper.get() != target_value {
flipper.flip().unwrap();
}
assert_eq!(flipper.get(), target_value);
}
// Test error condition
let current_value = flipper.get();
let result = flipper.flip_to(current_value);
assert_eq!(result, Err(Error::SameValue));
}
Code Quality and Best Practices
Documentation
Add comprehensive documentation:
/// A simple contract that maintains a boolean flag.
///
/// This contract demonstrates basic ink! concepts including:
/// - State storage
/// - Constructor patterns
/// - Public message functions
/// - Event emission
/// - Error handling
#[ink::contract]
mod flipper {
/// The contract's storage structure.
///
/// Contains a single boolean value that can be toggled.
#[ink(storage)]
pub struct Flipper {
/// The boolean value stored by this contract.
value: bool,
}
/// Event emitted when the stored value changes.
#[ink(event)]
pub struct Flipped {
/// The previous value before the flip.
#[ink(topic)]
from: bool,
/// The new value after the flip.
#[ink(topic)]
to: bool,
}
}
Error Handling Best Practices
Implement comprehensive error handling:
/// All possible errors returned by contract functions.
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Attempted to set the same value that's already stored.
SameValue,
/// Custom error for future extensions.
CustomError(u32),
}
impl Flipper {
/// Flips to a specific value with validation.
///
/// # Arguments
/// * `value` - The target boolean value
///
/// # Errors
/// Returns `Error::SameValue` if the provided value matches the current state.
#[ink(message)]
pub fn flip_to(&mut self, value: bool) -> Result<()> {
if self.value == value {
return Err(Error::SameValue);
}
let old_value = self.value;
self.value = value;
self.env().emit_event(Flipped {
from: old_value,
to: self.value,
});
Ok(())
}
}
Performance Optimization
Storage Optimization
ink! charges for storage usage, so optimize your storage layout:
#[ink(storage)]
pub struct OptimizedFlipper {
// Pack multiple booleans into a single storage slot
flags: u8, // Can store 8 boolean flags
metadata: [u8; 32], // Fixed-size arrays are efficient
}
impl OptimizedFlipper {
#[ink(message)]
pub fn get_flag(&self, index: u8) -> Result<bool> {
if index >= 8 {
return Err(Error::IndexOutOfBounds);
}
Ok((self.flags >> index) & 1 == 1)
}
#[ink(message)]
pub fn set_flag(&mut self, index: u8, value: bool) -> Result<()> {
if index >= 8 {
return Err(Error::IndexOutOfBounds);
}
if value {
self.flags |= 1 << index;
} else {
self.flags &= !(1 << index);
}
Ok(())
}
}
Next Steps
With your Flipper contract complete:
- Deploy to Testnet - Deploy your contract to the live network
- Set up EVM development - Compare with Solidity contracts
Resources for Further Learning:
Your Flipper contract demonstrates the fundamentals of ink! development and serves as a foundation for building more sophisticated smart contracts!