Build a voting dApp with ink! and OpenBrush

Sacha LanskySacha Lansky
5 min read

So you want to build a decentralized smart contract application (dApp) on Polkadot but aren't sure where to start? Why not create something useful, like a voting dApp? In this post, I'll give an overview of a new workshop I put together to guide developers to create a multi-contract dApp in ink! — Polkadot’s native smart contract language.

Earlier this year I hosted the lead developer of BrushFam on Substrate Seminar to learn more about what BrushFam is working on. One of their staple products is OpenBrush, a smart contract library and playground to build dApps with ink!. BrushFam also provides several other tools you can learn more about in the Seminar recording here.

This inspired me to create a workshop to help other developers build dApps with ink! using OpenBrush. Enter the voting dApp workshop. The workshop steps through how you would go about creating a multi-contract dApp by extending a PSP22 contract (Polkadot's ERC20 analog) and building the basis for a voting dApp. You'll also learn how to deploy the dApp on Rococo, Polkadot's parachain community test network. Check out the full workshop here and the solution code.

How the workshop is structured

The workshop is designed to be engaging and easy to follow. I used a Docsify template and plugin to render each section on its separate page. Each coding section has a side panel containing boilerplate code and explanations.

Each coding section has a panel on the right that provides useful information for anyone going through the workshop.

In the first part of the workshop, you'll learn how to extend a PSP22 contract to encapsulate some staking logic and expose a function for another contract to call. The second part steps through how to create a contract that encapsulates the voting logic of the dApp. Here's a diagram showing the architecture:

Why OpenBrush ?

It would have been entirely possible to recreate the Voting dApp using ink! without OpenBrush. The main advantage of using OpenBrush for a dApp like this one is to reuse the PSP22 implementation to create an initial native token supply. As you’ll find out by completing the workshop, all we’re doing in the first part is using OpenBrush to extend the PSP22 contract to inherit some custom functionality. This is possible without needing to implement your own “standard-compatible” contract, saving many lines of code. Here's the resulting Staking contract code:

#![cfg_attr(not(feature = "std"), no_std)]
#![allow(incomplete_features)]
#![feature(specialization)]

#[openbrush::contract]
pub mod my_psp22 {
    // imports from openbrush
    use openbrush::{
        contracts::psp22::*,
        traits::Storage,
    };
    // imports from the Staking impl
    use dapp::impls::staking::Data as StakingData;

    #[ink(storage)]
    #[derive(Default, Storage)]
    pub struct Staking {
        #[storage_field]
        psp22: psp22::Data,
        #[storage_field]
        staking: StakingData,
    }

    // contains default implementation without any modifications
    impl PSP22 for Staking {}

    // contains the extended logic to the PSP22 contract
    impl dapp::Staking for Staking {}

    impl Staking {
        /// Mint a fixed supply of 42_000_000 Staking tokens to be used for Voting.
        ///
        /// Tokens are issued to the account instantiating this contract.
        #[ink(constructor)]
        pub fn new() -> Self {
            let mut instance = Self::default();
            instance
                ._mint_to(instance.env().caller(), 42_000_000 * 10u128.pow(18))
                .expect("Should mint");
            instance
        }
    }
}

Beyond using the PSP22 standard, I found that using OpenBrush allowed me to neatly organize my code. I liked how I had to separate traits, implementations and errors in separate sub-modules, forcing me to think in a very “Rusty” way. It also allowed me to keep the contract code very minimal, putting all functions and logic in their respective modules.

The pattern I learned was: “Ok, I want my contract to have three publically callable functions. I’ll create a trait in a separate sub-module for my ink! contract to implement, then I’ll write that trait’s implementation in its own sub-module. If there are any other types I’ll want to reuse across other contracts in this project, I’ll use a dedicated module; make sure all sub-modules are linked to the main contract code. And voila!

I asked Green, a core contributor to ink! and OpenBrush why he would choose to use OpenBrush and his response was:

“If you know how to use OpenBrush, it increases the development speed by several factors. It allows splitting the project into submodules with encapsulated logic and makes the review process/unit testing simpler.”

Extend the dApp

Rather than focusing on ways of implementing sophisticated staking or voting logic, the workshop focuses on learning the patterns for developing dApps with OpenBrush. It's designed to be extended and open for learners to explore ways to improve the staking and voting logic. Here are some ideas of ways someone could extend the dApp:

  • Implement weighted conviction voting to allow users to increase their voting power in a more dynamic way

  • Implement the ability to have thresholds for a valid proposal to pass

  • Make the token supply inflationary

  • Build a front end to interact with the voting contract

Try the workshop out and leave your comments here. The workshop code is open source, feel free to suggest updates to it or new sections you'd like to see added to it.

If you’re a smart contract developer coming from the EVM world, I hope this workshop helps kickstart your development journey on Polkadot. 🚀


What is OpenBrush?

OpenBrush is a library for Polkadot smart contract developers with useful tools aimed at accelerating development time for deploying dApps written in ink!. It provides implementations of Polkadot Standards or “PSPs” (like ERCs in Ethereum) along with useful contract extensions and Rust macros to provide things like function modifiers, storage layout decisions and an extension to Rust's trait system to help make your dApp more modular.

What's ink! ?

ink! is Polkadot’s smart contract language designed to write secure smart contracts in Rust. There’s more though. You can write contracts in ink! that can call into core app-chain business logic, making it the perfect language to use for dApps in a heterogenous multi-chain network. The ink! language has also recently gone through an audit by OpenZepplin and is being used in production on a number of Polkadot-connected chains.

0
Subscribe to my newsletter

Read articles from Sacha Lansky directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sacha Lansky
Sacha Lansky

Passionate about helping build a trust-free internet. Developer advocate for all things Substrate. Rust 🦀 .