Practical Tips in Developing NEO Smart Contracts

Jing0825
5 min readSep 14, 2018

--

This tutorial shares some practical tips for your reference. One of the biggest challenges of developing C# NEO smart contracts is about the NeoVM supported language features, which has more than the official document tells in practice. There are also some useful tips for storage interaction and randomness generation. Enjoy hacking.

Type Conversions

The essential type supported by NeoVM is byte array (Byte[]), then the commonly used Boolean, String and BigInteger. There are other integer types such as Int8, Int64, UInt16, long, ulong, etc. which can be implicitly converted to BigInteger. Float is not supported.

So let’s just focus on the conversions between Byte[], Boolean, String, and BigInteger. Note: Some conversion are not defined officially, in this case, I’m trying to make the best reasonable implementation.

Byte[] to Boolean

Though looks like the easiest one, it actually has no direct conversion. Official instruction only mentions that False equals to int 0. So let’s assume True equals all other values, while empty byte array also equals to False. So we define the following function:

public static bool BytesToBool(byte[] data) => data[0] != 0;

Then we get the following result:

bool b0 = Bytes2Bool(new byte[0]); //False
bool b1 = Bytes2Bool(new byte[1]{0}); //False
bool b2 = Bytes2Bool(new byte[1]{1}); //True
bool b3 = Bytes2Bool(new byte[2]{0,2}); //False
bool b4 = Bytes2Bool(new byte[3]{3,2,5}); //True

Byte[] to String

This one is directly provided by Neo.SmartContract.Framework.Helper

public static string BytesToByte(byte[] data) => data.AsString();

Byte[] to BigInteger

public static BigInteger BytesToBigInteger(byte[] data) => data.AsBigInteger();

Boolean to Byte[]

This one also requires manual conversion.

public static byte[] Bool2Bytes(bool val) => val? (new byte[1]{1}): (new byte[1]{0});

String to Byte[]

public static byte[] StringToByteArray(String str) => str.AsByteArray();

BigInteger to Byte[]

public static byte[] BigIntegerToByteArray(BigInteger bigInteger) => bigInteger.AsByteArray();

Byte to Byte[]

You may think the following snippet good:

public static byte[] Byte2Bytes(byte b) => new byte[1] { b };//WRONG IMPLEMENTATION!!!

it passes the compiling but returns unexpected value in most times. This is because allocating byte array by variables is not supported. So just avoid this conversion.

Operators and Keywords

As mentioned in the official document, the majority of C# operators and keywords are supported by NeoVM. Here is the supplementary:

Bool: AND, OR and NOT

The operators of “&&”,“||” and “!” are supported.

bool b = true;
bool a = false;
Runtime.Notify(!b, b && a, b || a);// Message of false, false, true

Keyword: “ref” and “out”

The keywords of “ref” or “out” are C# features to allow the local variables passed into the function as references. These keywords are not supported in Neo smart contract.

Keyword: “try-catch”, “throw”, “finally”

The exception handling keywords are not supported.

Byte Array: Concatenation and SubArray

//Concatenation
public static byte[] JoinByteArrays(byte[] ba1, byte[] ba2) => ba1.Concat(ba2);
//Get Byte array's subarray
public static byte[] SubBytes(byte[] data, int start, int length) => Helper.Range(data, start, length);

Keyword “This” in Parameter

Sometimes you want to define an extension to the types to make the logic more condensed and intuitive. NeoVM supports the keyword “This”. The following sample code shows how to use it.

// Write a static class for the extentions of byte array
public static class ByteArrayExts{
// Return The subarray
public static byte[] Sub(this byte[] bytes, int start, int len){
return Helper.Range(bytes, start, len);
}
// Return the reversed bytearray
public static byte[] Reverse(this byte[] bytes){
byte[] ret = new byte[0];
for(int i = bytes.Length -1 ; i>=0 ; i--){
ret = ret.Concat(bytes.Sub(i,1));
}
return ret;
}
}

To use the above functions:

byte[] ba0 = {1,31,41,111};
byte[] ba1 = {12,6,254,0,231};
//Calls the Reverse and Sub functions with only one line.
Runtime.Notify(ba0, ba1, ba0.Reverse(), ba1.Sub(1,2));
//Call the extension functions multiple times in a row.
Runtime.Notify(ba1.Sub(0,3).Reverse());

Byte Array: Modify the Value

NeoVM does not support variable byte-wise manipulation. So we need to split the subarrays, modify some parts, and then concat them back. The following function should be put into the above ByteArrayExts class.

public static class ByteArrayExts{
//... previous functions ...

public static byte[] Modify(this byte[] bytes, int start, byte[] newBytes){
byte[] part1 = bytes.Sub(0,start);
int endIndex = newBytes.Length + start;
if(endIndex < bytes.Length){
byte[] part2 = bytes.Sub(endIndex, bytes.Length-endIndex);
return part1.Concat(newBytes).Concat(part2);
}
else{
return part1.Concat(newBytes);
}
}
}

Usage:

byte[] orig = new byte[5]{1,2,3,4,5};
byte[] newValue = new byte[2]{6,7};
//Replace the 3rd and 4th elements of orig byte array.
byte[] ret = orig.Modify(2, newValue);//return {1,2,6,7,5};

Storage

The class Storage/StorageMap are the only ways to interact with your smart contract’s on-chain persistent information. The basic CRUD operations are:

//Create and Update: 1GAS/KB
Storage.Put(Storage.CurrentContext, key, value);
//Read: 0.1GAS/time
Storage.Get(Storage.CurrentContext, key);
//Delete: 0.1GAS/time
Storage.Delete(Storage.CurrentContext, key);

There are some tips when using the above functions:

  1. Check if the value is unchanged before calling Storage.Put(). This will save 0.9GAS if it’s unchanged.
  2. Check if the new value is empty before calling Storage.Put(). If empty, use Storage.Delete() instead. This will also save 0.9GAS.
byte[] orig = Storage.Get(Storage.CurrentContext, key);
if (orig == value) return;//Don't invoke Put if value is unchanged.

if (value.Length == 0){//Use Delete rather than Put if the new value is empty.
Storage.Delete(Storage.CurrentContext, key);
}
else{
Storage.Put(Storage.CurrentContext, key, value);
}

3. Design your data structure with estimated length to be close but less than n KB. Because the function to write 2Bytes costs the same with that of 900Bytes. If necessary, you can even combine some items.


BigInteger[] userIDs = //....Every ID takes constantly 32 Bytes.
int i = 0;
BigInteger batch = 0;
while( i< userIDs.Length){
byte[] record = new byte[0];
for(int j = 0; j< 31;j++){//31x32 = 992 Bytes.
int index = i + j;
if( index == userIDs.Length ) return;
else{
record=record.Concat(userIDs[index].AsByteArray());
}
}
//This cost only 1GAS rather than 31GAS.
Storage.Put(Storage.CurrentContext, batch.AsByteArray(), record);
batch = batch + 1;
++i;
}

Randomness

Generating random values is a challenge for smart contracts.

First off, the seed has to be blockchain related deterministic value. Otherwise, it cannot be consented to by bookkeepers. Most Dapps would select blockhash as the seed.But with this approach, different users invoke the same SC function during the same block would return the same result.In Fabio Cardoso’s article, a new algorithm is introduced to utilize both blockhash and transactionID.

For some highly sensitive Dapps, professional users may argue that blockhashes can be Intervened by bookkeepers by transactions reordering. In this case, the algorithm of Asymmentropy is provided by generalkim00 and maxpown3r. The process is a little complicated so please just read their smart contract source code of a lottery example HERE to learn.

Summary

Thanks for reading this tutorial. I’ll continue updating it when we come up with more tips in here while developing smart contracts. Thank dprat0821 for help in the discussions. Thank Fabio, generalkim00, and maxpown3r for the great ideas.

My team is working on a game to carve people’s deep-heart words onto NEO blockchain. Thanks for any comments and suggestions.

NEO Donation Address: AKJEavjHZ3v96kxh7nWKpt4nVCj7VtirCg

--

--