Skip to main content

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:

  1. Deploy to Testnet - Deploy your contract to the live network
  2. 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!