Tokenisation of assets with Stratis smart contracts

GarryPas
9 min readJan 16, 2019

--

Introduction

This article is a followup to my introduction to Stratis smart contract development a couple of weeks back. In that post I focused more on the end-to-end process of getting a very basic “hello world” smart contract onto the Stratis TestNet. In this article I’m going to look a writing something a little less trivial, and focus more on the coding aspects.

Getting Started

I’m going to be building this contract in Visual Studio 2017, however other text editors or IDEs will work fine if that’s how you prefer to develop. The example was done on a Mac, but should work on Windows and Linux too. While I’ve tried to include as much of the code as possible, I’ve tried to break it up into chunks so I can explain what each bit is doing. If I’ve missed something important please let me know and I can review it. The full code is given in Appendix A at the bottom of this page.

To begin with we’ll create two new projects. The first will be nothing more than a place to store our smart contract code, while the second will be a test project to test the code and ensure everything is working. The contract will be called Assets.cs and we will create a test file AssetsTests.cs to test the contract using a local Test Chain (we will use the StratisContractTester NuGet package I introduced in my previous article to facilitate this, however Stratis promise that their own component is in the works, so you may prefer this if it has been released by the time you are reading this).

Figure 1: Visual Studio with the contract and tests in place.

At this point your solution should look something like Figure 1.

Now the first thing we want to do is reference our contract from within our test. For this we add the following:

private static string ContractFile => 
@”..\..\..\..\Contract\Assets.cs”.Replace(@”\”,
System.IO.Path.DirectorySeparatorChar.ToString());

That replacement of the path separator ensures cross-platform compatibility, and the path may differ slightly on your machine.

Now go ahead and install the NuGet package, StratisContractTester; you can add an entry like the one highlighted in bold to your .csproj, into the item group where NuGet dependencies are defined:

<ItemGroup>
<PackageReference Include=”Microsoft.NET.Test.Sdk”
Version=”15.8.0" />
<PackageReference Include=”MSTest.TestAdapter” Version=”1.3.2" />
<PackageReference Include=”MSTest.TestFramework” Version=”1.3.2" />
<PackageReference Include=”Stratis.Patricia” Version=”1.0.3" />
<PackageReference Include=”StratisContractTester”
Version=”1.0.3" />

</ItemGroup>

Finally you will need to add several DLLs into your test project. The simplest way I found to do this was to git clone the Stratis project at the last release, build it, and copy the resulting DLLs into a lib folder, and reference the DLLs directly. For build servers you can automate this with bash scripts and the like. The DLLs I added are listed below:

NBitcoin.dll
Stratis.Bitcoin.dll
Stratis.Bitcoin.Features.SmartContracts.dll
Stratis.SmartContracts.dll
Stratis.SmartContracts.CLR.dll
Stratis.SmartContracts.CLR.Validation.dll
Stratis.SmartContracts.Core.dll
Stratis.SmartContracts.Networks.dll
Stratis.SmartContracts.RuntimeObserver.dll
Stratis.SmartContracts.Standards.dll

Writing the code

Lets suppose we want to create a smart contract that allows us to tokenise precious metals; We can add units of precious metals to the contract, and these can be transferred to other Stratis wallets, or traded between anyone who has a Stratis wallet address.

First we need to define the owner of the contract — this will be the authority responsible for managing the assets, who we’ve made the person who publishes the smart contract. This can be thought of as an “admin” type user. In the contract code, add:

public Assets(ISmartContractState smartContractState)
: base(smartContractState)
{
Owner = Message.Sender;
}
public Address Owner
{
get { return this.PersistentState.GetAddress("Owner"); }
private set { this.PersistentState.SetAddress("Owner", value); }
}

At this point we’re ready to add functionality. We’d like to test our code, and to do this we first need to compile, validate and publish our smart contract onto our test chain (defined by the StratisContractTester) prior to executing a method on our contract. We will do this now. Add fields for the StratisContractTester instance, and to store the results of each setup step (we only need the contract address from the publish step).

private ContractTester contractTester;
private ContractCompilationResult compilationResult = null;
private IEnumerable<ValidationResult> validationResult = null;
private string contractAddress;

We now define 3 helper methods in our test to get everything setup ready to start executing methods on our contract. This bit should be fairly self-explanatory.

private void Compile() 
{
compilationResult = contractTester.Compile(ContractFile);
if (!compilationResult.Success)
{
Assert.Inconclusive(“Failed to compile smart contract.”);
}
}

private void Validate()
{
validationResult = contractTester.Validate(compilationResult);
if (contractTester.Validate(compilationResult).Any())
{
Assert.Inconclusive(“Failed to validate smart contract.”);
}
}

private void Publish ()
{
contractAddress =
contractTester.PublishContract(compilationResult);
if (contractAddress == null)
{
Assert.Inconclusive(“Failed to publish smart contract. Probably a bug earlier in the flow.”);
}
}

Our first action once the contract is published will be to create a new class of token (gold, silver, copper etc), and “mint” a supply of that token, then check the balance is correct after the operation. Let’s define a test for such functionality:

The test code:

private void Mint(string asset, ulong amount)
{
var mintResult = contractTester.ExecuteMethod(contractAddress,
“Mint”, new object[] { asset, amount });
if (mintResult.ErrorMessage != null)
{
Console.WriteLine(mintResult.ErrorMessage);
Assert.Inconclusive(mintResult.ErrorMessage);
}
}
private ulong GetBalance(Address address, string asset)
{
var getBalanceResult =
contractTester.ExecuteMethod(contractAddress, "GetBalance",
new object[] { address, "gold" });
if (getBalanceResult.ErrorMessage != null)
{
Console.WriteLine(getBalanceResult.ErrorMessage);
Assert.Inconclusive(getBalanceResult.ErrorMessage);
}
return (ulong)getBalanceResult.Return;
}
[TestMethod]
public void MintsNewAssetsAndGetsUpdatedBalance()
{
Compile();
Validate();
Publish();
Mint("gold", 100UL);

var result = GetBalance(ContractTester.SenderAddress.ToAddress(),
"gold");
Assert.AreEqual(100UL, result);
}

As you can see we are executing a method Mint on our smart contract (thanks to the StratisContractTester helper we are calling this as the wallet who created the contract, so we will have the required access), then we are calling GetBalance to check the balance.

Now we need to implement the Mint and GetBalance methods on our smart contract:

private static string GetKey(Address address, string assetName)
{
return $"{address}[{assetName}]";
}
public ulong GetBalance(Address address, string assetName)
{
return this.PersistentState.GetUInt64(GetKey(address, assetName));
}
public void Mint(string assetName, ulong amount)
{
// Only the creator of the smart contract can mint assets
EnsureSenderIsOwner();

assetName = assetName.ToLower();

var balance = PersistentState.GetUInt64(GetKey(Owner, assetName));
balance += amount;
PersistentState.SetUInt64(GetKey(Owner, assetName), balance);
}
private void EnsureSenderIsOwner()
{
Assert(Message.Sender == Owner, $”You ({Message.Sender}) don’t have permission to do that. Owner is {Owner}.”);
}

We add an EnsureSenderIsOwner method so we can restrict certain actions (such as minting new tokens) to the owner of the contract.

Now that we have the ability to create classes of tokens, we’d like to enable the ability for wallet addresses to transfer tokens between one-another. To do this we add a Transfer function. First the test code:

private void Transfer(Address destination, string asset, 
ulong amount)
{
var transferResult = contractTester.ExecuteMethod(contractAddress,
“Transfer”, new object[] { destination, asset, amount });
if (transferResult.ErrorMessage != null)
{
Console.WriteLine(transferResult.ErrorMessage);
Assert.Inconclusive(transferResult.ErrorMessage);
}
}
[TestMethod]
public void TransfersAssetsToAnotherWallet()
{
const ulong amountToTransfer = 40UL;
Compile();
Validate();
Publish();
Mint(“gold”, 100UL);
var destination = new AddressGenerator()
.GenerateAddress(199349828, 1).ToAddress();
Transfer(destination, “gold”, amountToTransfer);

var result = GetBalance(destination, “gold”);
Assert.AreEqual(amountToTransfer, result);
}

Notice that we are re-using methods such as Compile that we created earlier. This avoids repeating the same code over and over. In practice you might prefer to move the calls to these functions to a TestInitialize, but for these examples it makes it a bit clearer to show the code in the test method body.

Now we implement the Transfer method in our smart contract:

public void Transfer(Address destination, string assetName,
ulong amount)
{
Assert(Message.Sender != destination,
"Sender and destination are the same");

var myKey = GetKey(Message.Sender, assetName);
var theirKey = GetKey(destination, assetName);

var myBalance = PersistentState.GetUInt64(myKey);

Assert(amount <= myBalance,
"Insufficient balance to complete transaction.");

myBalance -= amount;
var theirBalance = PersistentState.GetUInt64(theirKey);
theirBalance += amount;

PersistentState.SetUInt64(myKey, myBalance);
PersistentState.SetUInt64(theirKey, theirBalance);
}

We run our tests and ensure that they all pass. Importantly we check the balances, and check that our sender and destination are not equal (without this check we get an incorrect balance at the end of the transaction).

Discussion

Writing smart contracts in Stratis is a fairly trivial affair once the developer understands how to interact with the Stratis platform, and understands the smart contract paradigm. While the functionality available is fairly limited, as I covered in my previous article, the contracts are Turing-complete, and allow for all of the functionality you’d expect to be able to implement to be implemented.

The example given here allowed us to mint tokens, and perform basic functionality associated with trading (getting our balance and sending/receiving tokens).

We also added some basic security to prevent the unwashed publish from performing privileged actions. In reality we could take this further and allow groups of addresses to mint new tokens, or link particular wallets with particular assets.

Conclusions

Here we wrote a smart contract that essentially carries out the basic functions of many alt-coins. We wrote around 200 lines of code — simple C# that any enterprise developer (such as myself) would have few problems writing or maintaining. This is a key requirement for adoption as it lowers the barrier to entry.

More generally it is hard to understate the potential of smart contracts. Imagine the Royal Mint implementing a smart contract similar to the example shown here, allowing anyone to trade tokens backed by precious metals — this was exactly how the money we use today came into existence (tokens representing precious metals). While markets may have cooled on cryptocurrencies, the technology continues to advance at a rapid pace.

Hopefully my article will help anyone new to this technology get a better understanding, and has adequately demonstrated why I feel Stratis is one of the most exciting projects in crypto in 2019.

Appendix

Appendix A

AssetsTests.cs:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using StratisContractTester;
using System.Linq;
using System.Collections.Generic;
using Stratis.SmartContracts;
using Stratis.SmartContracts.CLR;
using Stratis.SmartContracts.CLR.Compilation;
using Stratis.SmartContracts.CLR.Validation;
using System;

namespace StratisContractTesterTests
{
[TestClass]
public class AssetsTests
{
private static string ContractFile => @"..\..\..\..\Contract\Assets.cs".Replace(@"\", System.IO.Path.DirectorySeparatorChar.ToString());

private ContractTester contractTester;
private ContractCompilationResult compilationResult = null;
private IEnumerable<ValidationResult> validationResult = null;
private string contractAddress;

[TestInitialize]
public void Initialize()
{
contractTester = new ContractTester();
}

private void Compile()
{
compilationResult = contractTester.Compile(ContractFile);
if (!compilationResult.Success)
{
Assert.Inconclusive("Failed to compile smart contract.");
}
}

private void Validate()
{
validationResult = contractTester.Validate(compilationResult);
if (contractTester.Validate(compilationResult).Any())
{
Assert.Inconclusive("Failed to validate smart contract.");
}
}

private void Publish ()
{
contractAddress = contractTester
.PublishContract(compilationResult);
if (contractAddress == null)
{
Assert.Inconclusive("Failed to publish smart contract. Probably a bug earlier in the flow.");
}
}

private void Mint(string asset, ulong amount)
{
var mintResult = contractTester.ExecuteMethod(
contractAddress,
"Mint",
new object[] { asset, amount });
if (mintResult.ErrorMessage != null)
{
Console.WriteLine(mintResult.ErrorMessage);
Assert.Inconclusive(mintResult.ErrorMessage);
}
}

private void Transfer(Address destination, string asset,
ulong amount)
{
var transferResult = contractTester.ExecuteMethod(
contractAddress,
"Transfer",
new object[] { destination, asset, amount });
if (transferResult.ErrorMessage != null)
{
Console.WriteLine(transferResult.ErrorMessage);
Assert.Inconclusive(transferResult.ErrorMessage);
}
}

private ulong GetBalance(Address address, string asset)
{
var getBalanceResult = contractTester.ExecuteMethod(
contractAddress,
"GetBalance",
new object[] { address, "gold" });
if (getBalanceResult.ErrorMessage != null)
{
Console.WriteLine(getBalanceResult.ErrorMessage);
Assert.Inconclusive(getBalanceResult.ErrorMessage);
}
return (ulong)getBalanceResult.Return;
}

[TestMethod]
public void SmartContactCanBeCompiled()
{
Compile();
Assert.IsTrue(compilationResult.Success);
}

[TestMethod]
public void SmartContactIsValidForStratisBlockchain()
{
Compile();
Validate();
Assert.IsFalse(validationResult.Any());
}

[TestMethod]
public void SmartContactCanBePublishedOnStratisBlockchain()
{
Compile();
Validate();
Publish();
Assert.IsNotNull(contractAddress);
}

[TestMethod]
public void MintsNewAssetsAndGetsUpdatedBalance()
{
Compile();
Validate();
Publish();
Mint("gold", 100UL);

var result = GetBalance(
ContractTester.SenderAddress.ToAddress(),
"gold");
Assert.AreEqual(100UL, result);
}

[TestMethod]
public void TransfersAssetsToAnotherWallet()
{
const ulong amountToTransfer = 40UL;
Compile();
Validate();
Publish();
Mint("gold", 100UL);
var destination = new AddressGenerator()
.GenerateAddress(199349828, 1)
.ToAddress();
Transfer(destination, "gold", amountToTransfer);

var result = GetBalance(destination, "gold");
Assert.AreEqual(amountToTransfer, result);
}
[TestMethod]
public void CannotTransferToSelf()
{
Compile();
Validate();
Publish();
Mint("gold", 100UL);

var result = contractTester.ExecuteMethod(
contractAddress,
"Transfer",
new object[] {
ContractTester.SenderAddress.ToAddress(), "gold", 1UL });

Assert.IsNotNull(result.ErrorMessage);

Assert.IsTrue(result.ErrorMessage.Value.Contains(
"Sender and destination are the same"));
Assert.AreEqual(100UL,
GetBalance(ContractTester.SenderAddress.ToAddress(),
"gold"));
}
}
}

Assets.cs

using Stratis.SmartContracts;

public class Assets : SmartContract
{
public Assets(ISmartContractState smartContractState)
: base(smartContractState)
{
Owner = Message.Sender;
}

public Address Owner
{
get { return PersistentState.GetAddress("Owner"); }
private set { PersistentState.SetAddress("Owner", value); }
}

public ulong GetBalance(Address address, string assetName)
{
return PersistentState.GetUInt64(GetKey(address, assetName));
}

public void Transfer(Address destination, string assetName,
ulong amount)
{
Assert(Message.Sender != destination,
"Sender and destination are the same");

var myKey = GetKey(Message.Sender, assetName);
var theirKey = GetKey(destination, assetName);

var myBalance = PersistentState.GetUInt64(myKey);

Assert(amount <= myBalance,
"Insufficient balance to complete transaction.");

myBalance -= amount;
var theirBalance = PersistentState.GetUInt64(theirKey);
theirBalance += amount;

PersistentState.SetUInt64(myKey, myBalance);
PersistentState.SetUInt64(theirKey, theirBalance);
}

public void Mint(string assetName, ulong amount)
{
// Only the creator of the smart contract can mint assets
EnsureSenderIsOwner();

assetName = assetName.ToLower();

var balance = PersistentState.GetUInt64(GetKey(Owner,
assetName));
balance += amount;
PersistentState.SetUInt64(GetKey(Owner, assetName), balance);
}

private void EnsureSenderIsOwner()
{
Assert(Message.Sender == Owner, $"You ({Message.Sender}) don't have permission to do that. Owner is {Owner}.");
}

private static string GetKey(Address address, string assetName)
{
return $"{address}[{assetName}]";
}
}

--

--

GarryPas

Software engineering consultant specialising in NodeJS, DotNET and Java. Primarily backend these days.