Learn ink! by Example Part 1— ink! macros and securities
Why learn ink!
There are many blockchain smart contract languages, such as Solidity, Vyper, Yul, Cairo, Move, and ink!, etc., that support different virtual machines running on different blockchains, each with their own pros and cons,
When compared to Ethereum / EVM, the Polkadot blockchain is a growing ecosystem that is attracting increasing developer adoption with its unique architecture, WASM (Web Assembly) virtual machine, and ink! Smart contract language.
Parity Technologies, the company behind Polkadot led by Dr. Gavin Woods is building the Web 3.0 foundation. From the Substrate blockchain framework to ink!, its sharded design and tools enable parachains to operate at scale. In this article, Why we believe in Wasm as the base layer of decentralized application development, Parity shared its belief and offered ink! as a solution.
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on all devices, from servers to browsers, with great portability. It has already been endorsed by industry heavy weights and popular in the web2 world, with increasing indications that the Wasm VM is gaining traction as an EVM alternative. For example, Abitrum adopted Wasm for fraud proof, in parallel to the same smart contract code compiled to run on EVM.
ink! Is a Rust-based domain specific language (DSL) for writing Wasm based smart contracts for Substrate blockchains. Compared to Solidity, ink! Is a relatively new kid, yet with cool features and great potential. Being a community-focused project, it needs developers to share valuable learning experiences and input for continuous improvements. This article does exactly that, with objective learning and development experiences, evaluating and climbing the ink! learning curve.
Learning by doing in 3 stages
The project’s plan is to come up with an intuitive example, implement in ink!, and share learnings with the community. It is a technical showcase with simple yet decent complexity to show three chosen aspects, as compared to Solidity as a contrast.
The three aspects to be demonstrated are:
- ink! Smart contract and macros for code simplicity, reusability and readability.
- Upgradability of ink! based smart contracts.
- Performance benchmark and gas cost savings.
There are a lot of details to go over, so we’ll progress in corresponding three stages, with takeaways documented in a series of three articles, each covering respective areas. This is the first of the series, focusing on macros in ink! smart contract development.
The series might benefit several types of audiences:
- Developers wanting to check out alternative smart contract languages.
- Rust developers want to do blockchain based smart contract development.
- ink! novice wanting to learn more about declarative and procedure macros.
- Solidity / Vyper developers compare with other smart contract development paradigms.
- Network operators wanting to know more about smart contract upgrade practices.
- Web2 developers want to get a taste of coding smart contracts using their familiar programming languages such as Rust.
- EVM or compatible VM developers concerning performance and gas fees.
- Blockchain researchers surveying on vs off-chain computing and storage.
The use case: Order Food on the Blockchain
Current ink! document uses a very simplistic example called Flipper, which switches a boolean on and off. It lacks the sophistication to showcase the full potential of the ink! language. In this example, we came up with a contrived workflow example, where multiple parties: restaurants, couriers, consumers and dApp admin interact to register, order, prepare and deliver food on the chain.
We defined and implement this use case for multiple reasons:
- A more sophisticated business workflow with multiple parties leveraging blockchain to conduct transactions
- There are reusable business logic to be encapsulated and implemented in macros
- Demonstrate the end-to-end process of Wasm based smart contract upgrades, and best practices to best preserve states and to avoid hard fork
- Implementation best practices demonstrating techniques to improve performance, reduce binary footprint, and lower gas fees
The main flow of the Order Food on Blockchain example is:
1. Registration:
- Restaurants register on the dApp by providing their details such as name, address, and contact information.
- Consumers register on the dApp by providing their details such as name, address, and contact information.
2. Ordering:
- Consumers browse the restaurants and select the desired items from their menus.
- Consumers place an order by specifying the items, quantities, and delivery address.
- The order is stored on the blockchain, along with the restaurant and consumer information.
3. Preparation:
- Restaurants receive the order details and start preparing the food.
- Restaurants update the status of the order to indicate that the food is being prepared.
4. Delivery:
- Couriers are assigned to deliver the food based on their availability and proximity to the restaurant and consumer.
- Couriers update the status of the order to indicate that the food is out for delivery.
- Couriers deliver the food to the specified delivery address.
- Couriers update the status of the order to indicate that the food has been delivered.
5. Payment:
- Consumers make payment for the food using a supported cryptocurrency.
- The payment is stored on the blockchain, along with the order and consumer information.
- Restaurants receive the payment for the delivered food.
- Throughout this process, the dApp admin oversees the entire workflow and ensures that all parties are following the rules and guidelines set by the smart contract.
The specific code for implementing this example can be found on GitHub, where the open-source nature of the project allows for collaboration and improvement of the codebase. This example shows the capabilities of the ink! language in handling complex business workflows involving multiple parties and showcases best practices for smart contract upgrades, state preservation, and gas fee optimization. Here’s the main flow of this Order Food on Blockchain example, with specific code open sourced and available in GitHub.
Personas
- Consumers: These are individuals who want to buy food. They can submit orders, pay for the food and delivery fees using tokens supported by the blockchain network. They can also confirm delivery.
- Restaurants: These are businesses that offer food. They can add, update, or delete items from their menu, including dish names, descriptions, prices, and estimated preparation times. They can also confirm orders, prepare food, request delivery, and get paid.
- Couriers: These are individuals who deliver food to customers. They receive requests for delivery and get paid a service fee, which is a percentage of the order total, upon successful delivery.
- dApp manager: This is the administrator of the decentralized application. They handle tasks such as access control, listing restaurants, couriers, and customers, approving service fee rates, and changing ownership of the smart contract.
- dApp users: These are individuals who use the decentralized application. They can self-register with a wallet and provide their information. They can view all restaurants, food items, and couriers. They can also register to be service providers (restaurants or couriers) or submit orders as customers.
Workflow
- The example implements a mini workflow for a marketplace with service providers and consumers interacting with each other on the blockchain.
- Service providers can register or update their services, such as restaurant information, menus, and delivery rates.
- Consumers can browse restaurants and food, place orders, and select a courier for delivery.
- All participants in the marketplace have registered wallet addresses for sending and receiving payments.
- The decentralized application provides escrow functionality, holding the payment for an order until the food is picked up and delivered. The proceeds are then split, with the food subtotal going to the restaurant and the delivery charges going to the courier.
- We implemented the mini workflow as a state machine in the smart contract storage. Each state transition, such as order submission, confirmation, food preparation, delivery, and acceptance, is captured on-chain. The smart contract for each state transition or error emits events, triggering the next step in the state machine.
- This example is meant for learning and sharing, with a moderate level of complexity to show the implementation using ink! (a Rust-based smart contract language). In the real world, we can use blockchain for trustless transactions and enterprise-grade workflows.
- The open source community can further develop this example by adopting a workflow specification written by non-technical users or generated through a workflow DSL (domain-specific language) to smart contract compiler. This would allow businesses to conduct transactions on the blockchain using an intuitive frontend and tools. Enterprises of all sizes can leverage the open ledger as a trust layer, interacting and transacting transparently for increased efficiency, scalability, and cost-effectiveness.
ink! Implementation
The example provided in this text is implemented using the ink! smart contract language, which is built on top of the Rust programming language. Developers familiar with Rust will find it easy to read the open source code as ink! is a Rust-based DSL (Domain Specific Language).
For non-Rust based smart contract developers, it can still be a valuable exercise to experiment with the code and sandbox to get a sense of how ink! based smart contract development compared to other languages like Solidity. More information about the performance benchmark, binary size, and gas costs of this smart contract will be shared in part three of this series, along with various optimization techniques.
Here are some highlights of the data, services, security, and access control implemented in this ink! smart contract:
- Top-level data types, such as Food, Order, Delivery, Customer, Restaurant, Courier, and Manager, are implemented using openBrush’s Mapping feature.
- The smart contract defines and implements multiple service categories, including Restaurant services, Courier service, Customer services, Payment services, and Manager services.
- Data is persisted in the smart contract’s storage, while services are transactions that are added to blocks.
- The smart contract uses openBrush’s ownable feature to set and get owners with corresponding access control to create, read, update, and delete data. For example, only restaurant and courier owners can administer their respective services.
The entire codebase for this example is available on GitHub under the Apache 2 license.
Security & Access Control
OpenBrush is a smart contract development framework for ink! that provides several features to help developers build secure and reliable contracts. It is an open source project aimed to provide similar capabilities as OpenZeppelin for Solidity smart contract developers.
OpenBrush attempts to provide similar security and access control features like OpenZeppelin for ink! smart contract development: Ownable extensions and AccessControl with AccessControlEnumerable extension,
Ownable
Ownable is for implementing ownership in smart contracts. This is the most common and basic form of access control, where there’s an account that is the owner of a contract and can administer it. OpenBrush’s Ownable contract provides the following security features:
- A public modifier called onlyOwner that can restrict access to certain functions.
- A public function called transferOwnership that can transfer ownership of the contract to another address.
- A public function called isOwner that can check whether the current account is the owner of the contract.
- A public function called renounceOwnership that can renounce ownership of the contract.
The renounceOwnership function is a unique feature of OpenBrush’s Ownable contract. This function allows the owner of the contract to permanently give up ownership of the contract. This can be useful in situations where the owner no longer wants to be responsible for the contract.
Access Control
Role-Based Access Control (RBAC) offers flexibility in defining multiple roles. OpenZeppelin’s AccessControl library provides several features to help developers control access to their contracts.
- Roles: Developers can define roles for different users and groups of users.
- Permissions: Developers can assign permissions to roles, such as the ability to transfer tokens, create new contracts, or call specific functions.
- Policies: Developers can define policies that control how permissions are applied. For example, a policy might require that two approvers sign a transaction before it can be executed.
OpenBrush aims to provide a comprehensive set of security and access control features for ink! smart contract development, similar to what OpenZeppelin offers for Solidity smart contracts. It includes the Ownable contract for ownership management and the AccessControl library for role-based access control. Additionally, OpenBrush extends these features with unique functionalities such as the ability to renounce ownership, timed permissions, multi-factor authentication, and audit trails.
- Timed permissions: Developers can specify that permissions expire after a certain amount of time.
- Multi-factor authentication: Developers can require users to provide multiple forms of authentication, such as a password and a one-time code, before they can access a contract.
- Audit trails: Developers can track who has accessed their contracts and what they have done.
ink! macros
ink! macros provide a powerful tool for developers to encapsulate reusable logic and improve the simplicity, reusability, and readability of smart contract code. They can define custom syntax or code that can be reused within a smart contract, and they are translated into actual Rust code during compilation.
One advantage of ink! macros is their ability to handle a variable number of parameters, which allows for flexibility in processing different items or operations. This can be especially useful for tasks like printing or iterating through a set of items.
The reusability of ink! macros is important for adoption, as it allows developers to modularise related functionalities and share them across different projects. This can speed up smart contract development by providing easy access to mature code snippets.
Using ink! macros can also lead to better code quality and lower gas fees. By encapsulating logic in macros, developers can reduce repetition and inconsistency in their code, leading to cleaner and more maintainable contracts. Optimizing the implementation of macros can help reduce gas fees.
Another benefit of ink! macros are their ability to separate the layers of smart contract development and abstract business logic in dApps. This can make it easier to divide-and-conquer complexities and facilitate functional invocation among smart contracts, even across different chains.
In this example, to add another menu operation, for example, adding food pictures for each menu item, a developer can expand the same macro to support additional functionalities, making it easier to share among dApp developers to invoke the macro with increasing operations as a reusable component similar to code library. Therefore, this exercise comes up with specific macro implementations to illustrate its potentials not just at system level, but also encapsulating business logic in dApps, with multiple benefits:
- ink! macros, similar to “libraries”, can help to improve smart contract readability, reusability and maintainability reducing repetition and inconsistency
- Optimally implemented macros can reduce gas fees and improve code quality
- Lower ink! learning barrier and increase community adoption
- Flexibility to handle a variable number of parameters, which functions can’t
- Extensibility to add more functionalities to the macro
- Separation of smart contract dev layers to divide-and-conquer complexities
- Abstraction of business logic in dApps to facilitate functional invocation among smart contracts, even cross chains, following web3 design patterns.
Payment macro
This is a declarative macro to abstract payment functions. The code is called to pay restaurants and couriers respectively, both now being merged into the same transfer_from_contract_to_account macro defined in helpers.rs:
Crud_item macros
Procedure macro can be used to encapsulate business logic. Unlike declarative macros that match expressions with predefined patterns, procedure macros manipulate the Abstract Syntax Tree (AST) of the compiler to rewrite code. The code is auto-generated during compilation time.
There are various business items in this use case: customers, restaurants, couriers, etc. Each needs crud (create, read, update, delete) operations. Such business logic is similar. We implemented corresponding procedure macros to encapsulate reusable logic including specific data quality checks, identifier matches, and error handling. For example: create_item macro replaces create_customer, create_restaurant, create_courier functions. It can also be applicable or extendable to other appropriate dApp business items.
- create_item
- read_item
- update_item
- delete_item
- read_item_all
- read_item_from_id
/// create_item is a procedure macro to encapsulate reusable logic otherwise repeated in separate functions such as:
/// create_customer, create_restaurant, create_courier, etc.
/// It can also be applicable or extendable to other appropriate dApp business items
#[proc_macro_attribute]
pub fn create_item(attr: TokenStream, item: TokenStream) -> TokenStream {
...
}
In all *_service.rs files, add this line:
use crud_macro::{create_item, read_item, read_item_from_id, read_item_all,
update_item, delete_item};
Specifically, in the customer_service.rs file, the following create_item(Customer) macro invocation creates a customer entry. The expanded code from the create_item macro is included in the comments for learning purposes.
/// Function to create a customer
/// Use create_item procedure macro for Customer
#[ink(message)]
#[create_item(Customer)]
fn create_customer(&mut self, customer_name: String, customer_address: String, phone_number: String) -> Result<CustomerId, FoodOrderError> {
AccessControlImpl::grant_role(self, CUSTOMER, Some(Self::env().caller())).expect("Failed to grant Customer Role");
// **
// Comments below are current expanded code from the create_item macro
// included here for learning purpose. Output code changes as underlying macro changes.
// **
// ensure!(customer_name.len() > 0, FoodOrderError::InvalidNameLength);
// ensure!(customer_address.len() > 0, FoodOrderError::InvalidAddressLength);
// ensure!(phone_number.len() > 0, FoodOrderError::InvalidPhoneNumberLength);
// let customer_account = Self::env().caller();
// ensure!(!self.data::<Data>().customer_data.contains(&customer_account), FoodOrderError::AlreadyExist);
// let customer_id = self.data::<Data>().customer_id;
// let customer = Customer {
// customer_id,
// customer_account,
// customer_name,
// customer_address,
// phone_number,
// };
// self.data::<Data>().customer_id += 1;
// self.data::<Data>().customer_data.insert(&customer_account, &customer);
// self.data::<Data>().customer_accounts.insert(&customer_id, &customer_account);
// Ok(customer_id)
}
If there’s a need to change crud operations, the application developer only needs to modify the macro code instead of changing multiple functions for various item types, reducing the chances of errors and making maintenance easier. One can also extend the update_item macro to include additional logic to update food items. Such a macro can significantly reduce code redundancy and improve code reusability and simplicity.
Testing
This project leverages Chai as the testing framework for end-to-end testing with testing scripts, assets and artifacts in the Github repo. A testing script on a happy path shows the following output:
Here’s the expected output when running test from the docker image:
Deployment
Shiden Network is a multi-chain decentralized application layer on Kusama Network. Shibuya is the Shiden’s parachain testnet with EVM functionalities. We choose it for deployment as Shiden supports EVM, Wasm, and Layer2 solutions.
Contracts-UI is a web application for deploying Wasm smart contracts on Substrate chains. Polkadot.js is an effort to provide a collection of tools, utilities and libraries for interacting with the Polkadot network from JavaScript. It is generally recommended to use the more user friendly Contracts UI over the PolkadotJS app for ink! deployments and interactions.
Once the BlockchainFoorOrder smart contract is deployed, you can check the deployed contract on the shibuya blockexplorer. It should look like the screenshots below:
You can then interact with this deployed contract by manually reading or executing any listed messages.
We prepared this BlockchainFoodOrder Smart Contract Deployment and Interactions Guide to illustrate the step-by-step flow of deploying the contract followed by ordering food on the blockchain. Before you are trying to execute a transaction, you will need to get native tokens from a faucet.
To start interacting with the system, you can manually read or execute messages with parameters using different accounts that represent various roles such as restaurants, couriers, and consumers. I recommend installing the Sub Wallet as a browser extension and obtaining some free SBY tokens from a faucet since we deployed on the Shibuya testnet, which uses SBY tokens.
Once you have the necessary setup, you can run the example’s order food on the blockchain workflow by invoking any of the 31 messages that play different roles. Please note that reading messages does not require gas fees, but executing messages does. We have prepared a shared document with screenshots to guide you through the process of deploying the contract and step-by-step instructions for ordering food on the blockchain. Towards the end of the workflow, you will see the order status updated to “DeliveryAccepted” as shown in the screenshots below.
Recommendations and Next Steps
This project aims to provide a comprehensive learning experience for the ink! language and related tools by implementing an Order Food on Blockchain example. The project will focus on three specific areas: ink! macros, Wasm smart contract upgradability, and binary footprint/gas costs/performance benchmarks.
The Order Food on Blockchain example will showcase the capabilities of ink! smart contracts by allowing users to place food orders on a decentralized platform. The ink! macros will simplify the development process and enhance the readability of the code.
The project will also explore the concept of Wasm smart contract upgradability in part two, which allows for the seamless upgrading of smart contracts without disrupting the existing functionalities. We will implement this feature to ensure the scalability and adaptability of the Order Food on Blockchain platform.
Additionally in part three, the project will analyze the binary footprint, gas costs, and performance benchmarks of the ink! smart contracts. This analysis will provide insights into the efficiency and cost-effectiveness of the platform, ensuring optimal utilization of blockchain resources.
This project will serve as a learning opportunity for individuals interested in understanding ink! language, ink! macros, Wasm smart contract upgradability, and optimizing binary footprint, gas costs, and performance benchmarks in the context of a real-world use case.
ink! learning resources
To get started with understanding ink!, a good starting point is the “Guided Tutorial for Beginners” and the post “What is Parity’s ink!?” by Michi Müller. These resources provide a high-level understanding of ink!.
For advanced Rust developers, there is a GitHub markdown that covers ink! architecture and internals, which can be helpful for diving deeper into ink!.
For ink! 4.0 specifically, there is an announcement that outlines what’s included in the release.
Parity Technologies has ink! documentation in their GitHub repository, which covers ink! basics and provides some examples.
Substrate’s documentation site also has a smart contract tutorial that covers specific topics on how to use a smart contract template, store and retrieve values, and add functions to a smart contract.
If you’re looking for ink! projects, there is a curated list on GitHub that includes production and testnets, dApps, libraries and standards, DeFi, gaming, and block explorers.
For tools, OpenBrush has documented its ink! smart contract libraries, including ownable and access control. Contracts UI provides a frontend for contract deployment and interaction.
If you have specific questions, you can visit the Stack Exchange for Substrate and Polkadot, where there are over 400 questions tagged with ink!.
While ink!’s official documentation is a significant source to learn ink! as a smart contract language, it may lack sufficient coverage of macros within the context of ink!. However, there are resources available on Rust macros that can be helpful. The Rust Programming Language has a document that explains what macros are and the difference between macros and functions. The Little Book of Rust Macros is another excellent source specifically focused on Rust macros, although it may not cover macros in ink! smart contract language. These resources should provide a good starting point for learning and understanding ink! and its various aspects.
Recommendations
ink! is a promising smart contract language that provides an alternative option for developers familiar with Rust. However, there are several areas where improvements can be made to enhance the ink! ecosystem and attract more developers:
1. Learning: The learning curve for ink! and its associated tools can be steep, especially for developers not familiar with Rust. Having a well-organized learning center with in-depth resources and examples would greatly benefit the community and make the onboarding process easier.
2. Tooling: While there are some tools available for ink!, there is room for improvement in areas such as upgrade management, interoperability, gas estimation, performance benchmarks, and industry-specific smart contract macros and templates. Enhancing the tooling will make it easier for developers to adopt ink! for building dApps.
3. Ecosystem: The Solidity ecosystem is more mature compared to ink!, with a wider range of libraries, frameworks, tools, and job opportunities. Building a robust ecosystem around ink! would make it more attractive to developers as a viable option for smart contract development.
4. Enhancements: There are other smart contract languages and frameworks, such as Move and Arbitrum Nitro stack, that offer innovative features like security, auditability, and resource-oriented design. ink! can draw inspiration from these innovations beyond just the Substrate framework.
5. Adoptions: While a niche group of Rust developers has well received ink! implementing smart contracts on Polkadot/Kusama, expanding the community requires reaching out to target segments beyond this small group. We need more efforts to win incremental adoptions and grow the ink! community.
To address some challenges that may hinder adoption, the ink! project should focus on:
- Community: The open-source project is currently concentrated within Parity Technology and a few Dotsama parachain development shops. Expanding the community beyond this narrow group is crucial for growth.
- Communication: Clear and effective communication is essential to convince developers to adopt ink!. Issues like the Polkadot API failing to connect to the Substrate Contracts Node docker container should be documented with solutions to ensure a high level of quality and support.
- Support: Bugs affecting important features should be clearly communicated to the community along with any workarounds and expected fixes. Lack of clarity on technical solutions can discourage adoption.
- Branding: ink! and its related tech stack may not have enough visibility outside of the Dotsama community. The branding should be more distinct to avoid confusion with unrelated search results and increase awareness of ink! and its associated resources.
- Developer experiences: The ink! community is still smaller compared to more mature smart contract languages like Solidity. It should make efforts to catch up and provide a comparable developer experience with more content, discussion threads, tutorials, code repositories, and tools.
Despite these challenges, ink! is recognized for its technical merits and comparative advantages in smart contract development. With support from the community and continuous improvements, ink! has the potential to become a powerful tool in the web3 ecosystem.
Next steps
We have made the project open source under the Apache 2.0 license, allowing both web2 and web3 developer communities to access and contribute to it. We will split this article into three parts. The first part covers the use case and implementing ink! macros. Upcoming part 2 will focus on upgradability, and part 3 will benchmark the performance, measure binary footprints, and optimize gas costs in different ink! optimization variations.
Keep an eye out for the upcoming articles and provide feedback, suggestions, and contributions to the community!