Skip to content

Developing Rust Smart Contracts

Introduction

This tutorial demonstrates how to develop Rust contracts on the Oasis Devnet.

By the end of this tutorial you will know how to:

  • Write a simple smart contract in Rust
  • Test and deploy your smart contract to the Oasis Devnet

We'll demonstrate these concepts using a simple counter contract, which maintains a count of the number of times its increment() method has been called and returns the value through its getCount() method.

If you don't want to go through each step, feel free to download the finished code:

git clone https://github.com/oasislabs/rust-contract-tutorial

Prerequisites

Oasis Contract Kit

This tutorial requires the Oasis Contract Kit, which has necessary tools and dependencies pre-installed and ready to go. Please follow these instructions to install Contract Kit.

Note

The commands in this tutorial should be run inside the Contract Kit environment.

Truffle

This tutorial uses the Truffle framework to develop and test smart contracts. If you have never used Truffle before we recommend you review this Quickstart guide. Truffle is included as part of Contract Kit.

Rust

This tutorial assumes a decent understanding of the Rust programming language. To learn more about Rust, visit the Rust book. The Rust compiler is included as part of Contract Kit.

Step 1: Create a New Project

To create a new project for the counter contract, run the following commands:

# Create a new directory for our project
mkdir my_counter && cd my_counter

# Configure an empty Truffle project in the current directory
truffle unbox oasislabs/oasis-box

Note

The Truffle box oasislabs/oasis-box is a special project template allowing Truffle to support the full spectrum of Oasis Devnet features, including both Solidity and Rust compilation as well as confidential smart contracts.

It's suggested to start new Truffle projects using this command (rather than truffle init) to ensure compatibility with the Oasis Devnet.

Our project has the following structure:

Directory Description
/contracts Contract source code. Solidity contracts have a .sol extension. Rust contracts are named lib.rs and have a separate directory per contract (described below). You can mix Solidity and Rust in a single project.
/migrations Scripts for deploying contracts. Executed when you run truffle migrate
/tests Code for testing contracts. Executed when you run truffle test

You'll notice these directories are empty because we haven't created any contracts or tests yet. We'll do this in the next step.

Step 2: Write Rust Smart Contract

Let's create a new Rust smart contract called MyCounter which will implement our counter. We can do this using the truffle create command as follows:

truffle create rust-contract MyCounter

This creates an empty Rust smart contract in directory /contracts/my-counter (the command converts camel case contract names into hyphen-separated folder names). This command is similar to the truffle create contract command for creating new Solidity contracts.

For developers familiar with Rust

A Rust smart contract is a library crate managed as a Cargo project. The truffle create command above creates the necessary boilerplate for compiling the library as an Oasis smart contract.

In the newly created directory you'll see the following files, which are common to every Rust smart contract:

File Description
src/lib.rs Contract source code.
Cargo.toml Build configuration. You can add additional dependencies to this file.

The initial contract source code at src/lib.rs is shown below:

#[owasm_abi_derive::contract]
trait MyCounter {
    fn constructor(&mut self) {}
}

Our smart contract is defined by a trait called MyCounter. The name of the trait defines the contract name. You can choose any name for your contract. We'll use this name in Step 3 when we define migrations and tests.

The #[owasm_abi_derive::contract] attribute tells Rust to automatically generate the code and definitions needed for our contract to run. Every Rust smart contract must include this attribute.

Smart contract methods can be added by implementing regular Rust functions in the trait. The code above defines a single function called constructor, which is a special method that is called automatically when the contract is deployed. You can add any necessary initialization code to this method.

Aside from the special syntax just described, a Rust smart contract is a standard Rust library, meaning you can develop as you would any Rust program. For example you can create additional source files in the project and add external libraries as dependencies. Any code defined outside the trait can be called by the contract but is not part of the contract's public interface (i.e. can't be called externally).

Defining Contract Methods and State

Adding methods and state to a Rust contract is quite simple. Here are the necessary additions to our counter contract:

#![feature(int_to_from_bytes)]

static COUNTER_KEY: H256 = H256([
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
]);

#[owasm_abi_derive::contract]
trait MyCounter {
    fn constructor(&mut self) {
        let count: u32 = 0;
        owasm_ethereum::set_bytes(&COUNTER_KEY, &count.to_le_bytes());
    }

    #[constant]
    fn getCount(&mut self) -> u32 {
        let mut count_bytes: [u8; 4] = Default::default();
        count_bytes.copy_from_slice(&owasm_ethereum::get_bytes(&COUNTER_KEY).unwrap());
        u32::from_le_bytes(count_bytes)
    }

    fn increment(&mut self) {
        let count = self.getCount() + 1;
        owasm_ethereum::set_bytes(&COUNTER_KEY, &count.to_le_bytes());
    }
}

Defining Contract State

Note about contract state

In Rust smart contracts-as with all contracts on Oasis-state is modeled as a Map from H256 to Vec<u8> key-pairs. (In Solidity, the compiler automatically translates storage variables into key value pairs and so you don't notice this detail.)

To access contract state you must interact with this key-value map using the provided owasm_ethereum library. If you store data in a local Rust variable it will not be persisted on the blockchain.

To store our count, we declare a static variable COUNTER_KEY as H256 type. This variable represents the address in the contract's state that will store our count value. In the code above we specified this to be the first address (0x1) but it can be any valid H256 value that's not used for other storage.

In the constructor method we call owasm_ethereum::set_bytes to write an initial value of zero to the counter address. Note that whenever we write to read from storage, we need translate our storage to from Vec<u8> byte vectors.

Defining Contract Methods

We define two new methods: getCount and increment, which read and write the current count using owasm_ethereum::get_bytes and owasm_ethereum::set_bytes respectively. By adding these methods inside the trait we ensure the methods are publicly accessible when the contract is deployed.

The getCount method has a return type of u32, indicating that this method returns a value to callers. The #[constant] attribute specifies that this method does not modify state, allowing it to be called in a read-only setting (e.g. using eth_call instead of eth_sendTransaction). This is similar to view or pure functions in Solidity.

Interacting with the blockchain

In this code we demonstrated how a smart contract can read and write its own state. The owasm_ethereum library contains many additional methods for interacting with the blockchain, including methods for calling other smart contracts. A full list of methods is available here.

Step 3: Deploy and Test on Devnet

We're now ready to test this contract.

Deploying the Smart Contract

Before we can deploy, we need to tell Truffle how to access our Devnet wallet.

Modify the truffle-config.js file to add the mnemonic for your Oasis Devnet wallet.

Now, we need to write a Truffle migration. Migration scripts allow you to stage your deployments and customize how your smart contract is deployed. For more details see Truffle's Migrations page.

To add a migration script for our contract, create a new file migrations/2_counter.js with the following contents:

const MyCounter = artifacts.require("MyCounter");

module.exports = function(deployer) {
  deployer.deploy(MyCounter);
};

This is a very simple migration script, which uses Truffle's deployer to deploy the contract. For more complex projects you could define additional steps. For example, if your constructor function requires arguments you can define them in this file.

It's time to deploy our contract!

truffle migrate --network oasis

You should see the following output:

Starting migrations...
======================
> Network name:    'oasis'
> Network id:      42261
> Block gas limit: 16000023


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x503b7b0793a5c4f05d31dc3794bc9a9f95b8062ee5a8e253da61f48e53f8733d
   > Blocks: 0            Seconds: 0
   > contract address:    0x485c49FdBd6D71b5a76D808a8db54A9DCF4f3BAb
   > account:             0xB8b3666d8fEa887D97Ab54f571B8E5020c5c8b58
   > balance:             99.93151344
   > gas used:            87222
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00174444 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00174444 ETH


2_deploy_contracts.js
=====================

   Deploying 'MyCounter'
   ---------------------
   > transaction hash:    0x8f3feea8d962566b93fa5fcb703459ca16382a8e71d83abb143d8745320bcd05
   > Blocks: 0            Seconds: 0
   > contract address:    0x4800596bfe47A7a1a92C19fEc285C4D8408779eb
   > account:             0xB8b3666d8fEa887D97Ab54f571B8E5020c5c8b58
   > balance:             99.92127046
   > gas used:            470489
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00940978 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00940978 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.01115422 ETH

Congratulations! You've succesfully built and deployed a Rust smart contract to the Oasis Devnet.

Writing Tests

Rust contracts can be tested in Truffle in the same way as Solidity contracts.

Create a new file test/test-counter.js with the following content:

const MyCounter = artifacts.require("MyCounter");
const Web3 = require("web3");
const web3 = new Web3(MyCounter.web3.currentProvider);

contract("MyCounter", (accounts) => {

  const instance = new web3.eth.Contract(MyCounter.abi, MyCounter.address, {
    from: accounts[0]
  });

  it("should have a count of zero", async () => {
    const count = await instance.methods.getCount().call();

    assert.equal(count, 0);
  });

  it("should increment by one", async () => {
    await instance.methods.increment().send();
    const count = await instance.methods.getCount().call();

    assert.equal(count, 1);
  });
});

This test verifies our contract works correctly by calling the increment method and ensuring the count increases by one. For more information on writing tests, see Truffle's Testing page.

Notice we didn't need to do anything to allow our Rust smart contract methods to be called by Javascript—the necessary interface definitions are generated automatically by the Oasis Contract Kit build tools.

To run this test on the Oasis Devnet:

truffle test --network oasis

The tests above should yield the following output:

Contract: MyCounter
    ✓ should have a count of zero (214ms)
    ✓ should increment by one (937ms)


2 passing (1s)

Yay! You now know how to write and run tests for your Rust contract.

Next Steps

If you're ready for a more complex smart contract, you can download the ERC20 contract in Rust. See if you can modify this contract to create a custom ERC20 token and deploy it to the Devnet!