Skip to content

SolidVM

As described in the STRATO pluggable VM section, SolidVM is the STRATO custom implementation and extension of Solidity as an interpreted language. It provides faster execution speeds, and allows for the usage of many STRATO specific features such as addresses on private chains, X.509 identity features, and more.

By using SolidVM, you can unlock many useful features in your Solidity smart contracts. Below are some of the highlighted features that make SolidVM different and so powerful on STRATO:

  • X.509 Verified Identity Integration
  • Automatic Typechecker
  • Cross-shard contract communication via the account type
  • Unlimited data type lengths
  • Intuitive type-casting
  • String-type concatenation
  • Human-readable event logs in Cirrus
  • Gas limit disconnected with account balance - no need to worry about transaction costs

Most contracts written in Solidity for the EVM are directly compatible with SolidVM, however SolidVM does not support some features like assembly code or ABI related functionality simply because they are not compatible with it, since SolidVM is interpreted and not compiled. Most functionality though can be translated into SolidVM with minor code adjustments. A translated contract of course will not take advantage of all of SolidVM's features though until those are properly implemented, like interacting with other chains.

Using SolidVM for a Contract

To select which VM is used to process a contract, you will need to include a VM option in the metadata parameter of a transaction payload. A VM can be set to EVM or SolidVM. The EVM will be used by default if the VM is not specified.

We use a basic SimpleStorage contract:

contract SimpleStorage {
  string myString;
  uint myNumber;
  constructor(string _myString, uint _myNumber) {
    myString = _myString;
    myNumber = _myNumber;
  }
  function setString(string _s) {
    myString = _s;
  }
  function setNumber(uint _n) {
    myNumber = _n;
  }
}

Request

curl -X POST \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "txs": [
        {
          "payload": {
            "contract": "SimpleStorage",
            "src": "<contract-src>",
            "args": {
              "_storedData": 3
            }, 
            "metadata": {
              "VM": "SolidVM"
            }
          },
          "type": "CONTRACT"
        }
      ],
      "txParams": {
        "gasLimit": 32100000000,
        "gasPrice": 1
      }
    }' \
  "https://<strato_address>/strato/v2.3/transaction?resolve=true"

Info

When posting multiple contracts in one API call, STRATO will use the VM defined in the payload of the first contract.

To enable SolidVM on an application that uses the blockapps-rest JS SDK, include the VM property in the options parameter when making a call to create a contract.

Example:

For this example we assume the options object has already been configured, and the stratoUser has also already been created and a key has been obtained.

// read Solidity source
const simpleStorageSrc = fsUtil.get("SimpleStorage.sol");

const contractArgs = {
  name: 'SimpleStorage',
  source: simpleStorageSrc,
  args: {
    _myString: "Hello World",
    _myNumber: 10,
  },
}

const solidVMOptions = {
  config: {
    ...options,
    VM: 'SolidVM'
  }
}

// Use your STRATO identity to upload the contract
const contract = await rest.createContract(stratoUser, contractArgs, solidVMOptions)
Learn more about BlockApps Rest

X.509 Integration

Several built-in functions have been added which allow you to directly interface with an X.509 certificate associated with a given address.

Built-in Functions:

  • getUserCert(address addr) returns (mapping(string => string));
    • Gets the X.509 certificate registered to this address
    • The returned data type is a solidity mapping(string => string) where each key-value pair in the mapping is the respective key-value pair of the certificate.
  • parseCert(string certificate) returns (mapping(string => string));
    • Takes a PEM/DER encoded certificate and parses it as a mapping(string => string). Does not register the certificate with any address or require that the certificate be registered with an address.
  • verifyCert(string _cert, string _pubkey) returns (bool)
  • This function verifies the given certificate or certificate chain signature(s) with the provided public key (or chain of keys contained with the certficate chain.)
    • _cert : string
      • A DER encoded X.509 certificate
    • _pubkey : string
      • A DER encoded EC Public Key
  • verifySignature(string _mesgHash, string _signature, string _pubkey) returns (bool)
    • This function checks if the signature and message hash can be verified usign the provided public-key.
    • _mesgHash : string
      • A hex-encoded hash of a message signed with ECDSA - must be 64 chars long (32 bytes)
      • Example: "68410110452c1179af159f85d3a4ae72aed12101fcb55372bc97c5108ef6e4d7"
    • _signature : string
      • A DER encoded X.509 certificate
    • _pubkey : string
      • A DER encoded EC Public Key
  • verifyCertSignedBy(string _cert, string _pubkey) returns (bool)
    • This function checks if the certificate is signed by the public key. This contrasts with verifyCert which checks if the certificate is ultimatly signed by the root public key.
      • _cert : string
        • A DER encoded X.509 certificate
      • _pubkey : string
        • A DER encoded EC Public Key

Additional X.509 Transaction Properties

If the account creating a transaction has a certificate registered to its address, than the global tx variable will have 3 additional properties corresponding to the tx.origin certificate's properties.

  • tx.username : string
    • The tx.origin's registered Common Name
  • tx.organization : string
    • The tx.origin's registered Organization
  • tx.group : string
    • The tx.origin's registered Organizatinal Unit

Certificate Mapping Fields

When a certificate is parsed or retreived using parseCert or getUserCert, it returns a mapping with the following fields:

  • commonName
    • The certificates' Common Name (CN)
  • organization
    • The certificates' Organization (O)
  • organizationalUnit
    • The certificates' Organizational Unit (OU)
  • country
    • The certificates' Country (C)
  • publicKey
    • The certificates' DER encoded Public Key
  • certString
    • The DER encoded string of this X.509 certificate
  • expirationDate
    • The Unix timestamp of the expiration of the certificate and can be parsed as an int to be used in mathematical contexts.

Please see the full X.509 Documentation for a complete overview of using X.509 certificates in STRATO.

Default Storage Values

When a state variable is declared without an initial value and is unset in the contract constructor, its value will be automatically set to a default value for its type:

Type Default Value
int 0
bool false
string ""
bytes ""
address 0x0
account 0x0
contract 0x0
enum 0
array []
mapping Empty mapping
struct Each field has the default value of its type
---------- --------------
### Data Type Lengths
There are no fixed-length data types such as int8. SolidVM removes the necessity to specify the bit-length of a numeric data type, such as int8 or bytes16. Instead all these data types allow for arbitrary length. This prevents data overflow conditions when numbers become very large.

Equivalent SolidVM Numeric Types

EVM SolidVM
int<length> int
uint<length> uint
bytes<length> bytes
fixed<M>x<N> Unsupported

Tip

SolidVM will recognize numeric types - even if they have a specified length. SolidVM will ignore this specified length and treat it as the corresponding unfixed-length data type. This means that types like int8 or bytes16 will be treated as just int or bytes in SolidVM. This is useful when porting Contracts written for the EVM to SolidVM.

User Defined Types

User Defined types allows a way for creating a low cost abstraction over a basic value type. This can be considered as an alias with strict type requirements. As at compile time the type is typechecked then unwrapped to its original type during the optimization phase prior to runtime.

A user defined type is defined using type T is V at the file level, where T is the name of the newly introduced type and V is T's built-in underlying type. The function T.wrap(V value) is used to convert from the underlying type to the custom type. Conversely, the function T.unwrap(T type) is used to convert from the custom type to the underlying type.

Example

type MagicInt is int;

contract UserDefinedTypes {
    MagicInt myInt;
    int regularInt;

    constructor() {
        myInt = MagicInt.wrap(3); //creates defined type using wrap function
        //regularIntType = myInt; Will throw an error since myInt is of type MagicInt
        regularInt = MagicInt.unwrap(myInt); // turn userDefined type back into underlying type
    }
}

Account Data Type

The account data type is similar to the address data type. The account type takes the form of <address>:<chainId?>. With the introduction of Private Chains on STRATO, it is necessary to specify which chain an account exists on if it is on a Private Chain. To declare an account type, use the account constructor, which takes either an address and chain ID, or just an address. If the chain ID is not specified, the account chain ID used will be the current chain of the contract. See below on how to specify different chains relative to the current chain.

function account(address addr, uint chainId) returns (account);

function account(address addr, string chainId) returns (account);

function account(address addr) returns (account);

Example (from within SolidVM)

contract AccountExample {

  account myAccount;

  constructor() {
    address myAddress = address(0x1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b);
    int myChainId = 0xad23ad6df23ad23ad23ad6df23ad23;
    myAccount = account(myAddress, myChainId); // Create variable for this account on the correct private chain 
  }
}

Example (from an API call)

contract AccountExample {

  account myAccount;

  constructor(address _addr, int _chainId) {
    myAccount = account(_addr, _chainId); // Create variable for this account on the correct private chain 
  }
}
Transaction Parameters:

{ 
  "_addr": "1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b", // address does not need 0x prefix when being passed to the API
  "_chainId": "0xad23ad6df23ad23ad23ad6df23ad23" //chainId does need 0x prefix when being passed to the API
}

Alternatively, the chainId could be constructed from a string argument type and cast as an int from within the contract as follows:

contract AccountExample {

  account myAccount;

  constructor(address _addr, string _chainId) {
    int myChainId = int(_chainId);
    myAccount = account(_addr, myChainId); 
  }
}

Transaction Parameters:

{ 
  "_addr": "1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b", // address does not need 0x prefix when being passed to the API
  "_chainId": "ad23ad6df23ad23ad23ad6df23ad23" //since the chainId is being parsed as an int, it does not need 0x prefix when being passed to the API
}

Tip

Because most languages have a maximum integer size, it is not recommended to convert the chain ID to a base 10 int before sending it in a transaction API call. This is because the hexadecimal chain ID will translate to an extremely large base 10 number, usually resulting in an overflow or loss of precision in the sender's environment.

Example using a string Chain ID

contract MakeAccount {

    account myAccount;

    constructor(address _addr, string _chainId) {
        myAccount = account(_addr, _chainId);
    }
}

Transaction Parameters:

{ 
  "_addr": "1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b", // address does not need 0x prefix when being passed to the API
  "_chainId": "0xad23ad6df23ad23ad23ad6df23ad23" // since the chainId is being parsed as a string, it does need 0x prefix when being passed to the API
}

Warning

Account values cannot be passed as arguments to functions or constructors through direct API calls. However functions may call other functions or constructors with account values as parameters.

Example:

contract AccountTest {
  function f(account acc) returns (account) {
    return acc;
  }
  function g(address addr, uint chainId) returns (account) {
    account acc = account(addr, chainId);
    return f(acc);
  }
}

The g() function would be able to be called from the API by passing in the address and chainId arguments, however an API call to f() with an argument of <address>:<chainId> would fail.

Because of this limitation, it is always recommended to construct account types from separate address and chainId arguments using the account constructor.

Referencing Connected Chains

Contracts may reference accounts on other connected chains by using the following keywords in the account type constructor.

Named Connected Chain

You may reference a named connected chain in the account constructor by providing the connected chain's name in the chainId parameter. A chain is connected at chain creation time and is assigned a name. Once connected, the two chains can access the contracts of the other chain.

address myAddress = 0x1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b;
account myAccount = account(myAddress, "myConnectedDapp");

Connecting chains to each on other enables app developers to integrate the data and functionality of existing Dapps on the network. For example, you can integrate your application with an existing payment app or data oracle service, and read the data provided from those services.

Parent Chain

You may Reference the "parent" chain of the current chain by using the reserved "parent" keyword in the chainId parameter of the account constructor. A parent chain is the chain declared in the "parentChain" field when creating a new chain.

address myAddress = 0x1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b;
account myAccount = account(myAddress, "parent");

Other Chains

As a convenience, the "self" and "main" keywords are available that allow the respective chains to be referenced in the account type constructor.

self

  • References the chain ID of the current chain.
    address myAddress = 0x1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b;
    account myAccount = account(myAddress, "self");
    

main

  • References the Main Chain of the network.
    address myAddress = 0x1a2b3c4d5e7f1a2b3c4d5e7f1a2b3c4d5e7f1a2b;
    account myAccount = account(myAddress, "main");
    

For more information on connected chains, visit the Private Chain Documentation.

Warning

In STRATO < v7.6, any keyword referencing an ancestor of the current chain (parent, grandparent or ancestor) cannot be used in the constructor of a Governance contract of a private chain. This is because the chain creation and constructor run as an atomic process, therefore the chain will not have any relations until after the constructor has been completed and the chain is created. Thus referencing the related chain will throw an error.

Account Built-in Properties Aside from having all the properties of the contract that the account variable represents, an account type also has the built in chainId property. The chainId property is of type int. If the account is on the main chain, its value will be 0. This property can useful to help ensure contracts do or do not get deployed to private chains.

Example:

contract RequirePrivateChain {

    int privateData;

    constructor(int _privateData) {
        int curChainId = account(this, "self").chainId;
        require(curChainId != 0, "This contract must not be posted on the main chain");
        privateData = _privateData;
    }
}

The this member representing the current contract has an extra built-in property of chainId, representing the hex-encoded string of the current account's chain ID. Note that any other account type will return an int.

Example:

contract ThisChainID {

    string privateChainId;

    constructor() {
        privateChainId = this.chainId;
    }
}

Contract Types

A contract type is a type that references a specific type of contract at a given address. (The account type can also be used, however it is recommended to use the name of a contract directly.) A Contract type behaves similarly to an Object in an OOP language - it can be used to reference another contract to access its methods and members. While objects exist in memory, contract data exists on the blockchain.

Declare contract variables that reference other contracts by using the <contractName>(args...) constructor. The constructor takes either an address, or account as a single parameter. If a contract does not actually exist at the provided account/address, then any attempts to access its functions/members will result in an error.

Instantiate new contracts on the blockchain by using the new <contractName>(args...) keyword. This creates a whole new contract instance with a new address and state variables on the current chain. It uses the contract's constructor to create the contract. This is the parallel to creating a new object in an OOP language, except the new object is permantently stored on the blockchain, rather than in the program's memory.

Any referenced contract name must in the current contract's scope, so its code must be uploaded with the original contract's code bundle.

Example (create new contracts)

contract Foo {
  uint x;
  constructor(uint _x) {
    x = _x;
  }
  function doubleX() returns (uint) {
    return x * 2;
  }
}

contract Bar {
  Foo myFoo;
  uint y;
  constructor(uint _x) {
    y = _x;
    myFoo = new Foo(_x + 1);
  }
  function useFoo(uint _y) returns (uint) {
    return myFoo.doubleX() + y; // y = (foo.x * 2) + y
  }
}

Example (reference existing contracts from arguments)

In this example, the accessFoo function would be called with the account and chainId of an existing Foo contract instance.

contract Foo {
  uint x;
  constructor(uint _x) {
    x = _x;
  }
  function doubleX() returns (uint) {
    return x * 2;
  }
}

contract Bar {
  uint y;
  constructor(uint _y) {
    y = _y;
  }
  function accessFoo(address fooAddress, uint fooChain) returns (uint) {
    Foo tempFoo = Foo(account(fooAddress, fooChain)); // use address + chainId to generate an account type
    return tempFoo.doubleX() + y; // y = (foo.x * 2) + y
  }
}

Example (without specifying a Chain ID)

In the case when the referenced contract is on the same chain as the current contract, then the chainId may be omitted from the account constructor or an address type may be used.

contract Foo {
  uint x;
  constructor(uint _x) {
    x = _x;
  }
  function doubleX() returns (uint) {
    return x * 2;
  }
}

contract Bar {
  uint y;
  constructor(uint _y) {
    y = _y;
  }
  function useFoo(address fooAddress) returns (uint) {
    Foo tempFoo = Foo(fooAddress);
    return tempFoo.doubleX() + y; // y = (foo.x * 2) + y
  }
}

SolidVM enforces the following rule to enable data integrity on-chain:

  • Contracts cannot modify the state of other chains (such as modifying contract state variables or creating new contracts on other chains.)

Furthermore, it is impossible to create new private chains from within SolidVM.

Joining Contract Info

When a "host" contract needs to reference the data of another external contract, SolidVM contracts work in conjunction with the STRATO Cirrus feature to easily allow data from related contracts to be retrieved in a single query. This is not a direct feature of SolidVM, but of STRATO, so contracts of any version may use this functionality. This feature requires STRATO >= v7.5.

See Joining Multiple Contract Tables for more detailed instructions on this topic.

No Data Location Required for Variables

Standard Solidity allows for variables to have their data location set by the user using keywords like memory or storage as a decorator on the variable after its type. However within SolidVM, there is no need to declare the storage location of a variable since the language is interpreted. All local variables assigned to the value of a global variable will automatically create memory copies of the global value. This is allowed because there is no concept of gas in SolidVM, so programmers do not have to worry about the slight overhead of copying storage values into memory before modifying them.

Pushing to Memory Arrays

As an additional feature not available in standard Solidity, SolidVM allows arrays that are stored in memory to be pushed to. This is possible since SolidVM does not have a dedicated stack limit or heap size. SolidVM does not have to statically allocate memory to arrays during function calls.

Example:

The useStorageArr function could be called and the data appended to the array would be stored in the xs state variable:

contract PushMemArr {

    int[] xs;

    constructor(int[] _xs) {
        xs = _xs;
    }

    function pushMemArr(int[] _xs, int _x) {
        _xs.push(_x);
    }

    function useStorageArr() {
        pushMemArr(xs, 1); // xs = [...xs, 1]
    }
}

Extended Type Casting/Constructors

SolidVM allows basic types to be casted between each other more easily than standard Solidity. This can be achieved by using the syntax:

typeA oldValue = 5
typeB myType = typeB(oldValue);

Below is a list of supported type conversions outside of standard Solidity:

From To Notes Example
string int/uint The string literal value is interpreted as a base-16 number. string s = "12";
int x = int(s);
// x = 18 (base-10)
string address The string literal value is interpreted as a base-16 blockchain address. string s = "12";
address a = address(s);
// a = 0x000...12
int/uint string Converts a number to its base-10 string representation. int x = 12;
string s = int(x);
// s = "12"
int/uint address Interprets a number as a base-16 value. See account/address types. int x = 12;
address a = address(x);
// a = 0x000...0c

Code

<address>.code : string

This member will get the code collection from a particular address, in the future it will likely be modified to return the information from a code pointer. This is different from how this same code member works in EVM. In EVM this is bytecode specific and returns the bytecode at the address. Since SolidVM is an interpreted language it does not use the EVM bytecode. code will return a string of the collection of code at the address, rather than the collection of bytecode for the address. This also means that the typical usage of <address>.call(<different_address>.code) does not work.

Important

The code member is not implemented the same way in SolidVM as the similarly named code function in EVM. In the EVM this member is typically used with the the three call functions - call(), staticcall(), and delegatecall().

call, staticcall, and delegatecall are not currently implemented in SolidVM.

Codehash

<address>.codehash : string

This member is very similar to the regular .code function but it just gets the code hash of the address, this is useful for referencing particular code snippets. This will get the Keccak-256 hash of the code that is located at that address. This is useful when trying to verify that two accounts are dissimilar or for referencing the codehash for elsewhere in the current contract code.

Example:

contract Test {

    constructor(){}
}

contract CodeHashTest{

    string codeHashTest;

    constructor() public {
        Test t = new Test();
        codeHashTest = address(t).codehash;
    }
}
In this example, codeHashTest will contain the code hash generated for the contract t.

SolidVM and EVM Address Differences

While BlockApps keeps SolidVM up-to-date with the latest EVM Solidity standard, there are several differences in the languages' architecture, thus not all functionality can reach complete parity. There are several functions and members that cannot be translated to SolidVM with full equivalency. Several of these are apparent in the address member functions. These include the following:

  • <address>.code

    • Does not return EVM byte code, rather it returns the string of the code collection at the address.
    • Future development will reference code pointers instead of code collections.
  • <address>.call

    • This function is not in SolidVM yet.
  • <address>.staticcall

    • This function is not in SolidVM yet.
  • <address>.delegatecall

    • This function is not in SolidVM yet.

String Concatenation

Two strings may be joined together like many other programming languages:

string s = "Block" + "Apps" // "BlockApps"

Revert Statement

The revert function immediately rasies an exception in the SolidVM runtime, which can be optionally caught by the try/catch functionality. If the revert is not caught, than it will cause the transaction to be invalid and revert any changes made in this transaction. This is commonly used to prevent the update or modification of state variables when certain criteria is met.

Important

The revert function operates differently in SolidVM than standard Solidity. In standard Solidity, revert throws out state changes from the current function scope and any sub-calls, as well as raises an exception. In SolidVM, revert only raises an exception and state changes will only be reverted if the error is uncaught.

Function Signature

//    invoke revert without any message/parameters passed
revert();                   

revert(args);
//    revert("error message") i.e. Ordered Args 
//    revert({x:"Message"}) i.e. Named Args

Arguments:

The revert function can take arbitrary arguments which will be thrown with the revert error.

Example:

  1. Reverting without arguments
contract RevertUsage {

    uint a;

    constructor() {
        a = 1;
        setA(9);
    }

    function setA(uint modified) {
        a = modified;
        revert(); // revert without arguments
    } 
}

After calling setA, a will still be 1 since revert was called and uncaught in the constructor.

  1. Reverting Based on Named Arguments
contract RevertNamedArgs {

    uint a;

    constructor() {
        a = 1;
        setA(9);
    }

    function setA(uint modified) {
        a = modified;
        revert({x:"Cannot modify 'a'"}); // revert based on named arguments
    }
}

Try/Catch Statements

SolidVM allows contracts to catch errors that may potentially throw errors and handle them with grace. Previously any code that threw an error immediately caused a runtime error, halting and reverting the transaction without any way to handle errors.

SolidVM presents two ways to handle errors: a method more paradigmatic with traditional Solidity, as well as a custom implementation just for SolidVM that allows for more granular error catching.

The SolidVM Way

The SolidVM-style of error catching enables developers to catch errors using SolidVM-defined error types. It also allows an arbitrary block of code to be run inside the try block, rather than being limited to a single expression. A try/catch block can be used to catch any number of error types by chaining multiple catch blocks with different error types after each other.

Example

contract SolidVMCatch {
    uint public myNum = 5;
    constructor() public {
        try {
            myNum = 1 / 0;
            //... can put as many statements as you want here
        } catch DivideByZero {
            myNum = 3;
        }
    }
}

The Solidity Way

Standard Solidity provides generic error handling. The Solitdity try/catch behavior can be found on the Solidity Docs.

SolidVM will catch errors based on the same logic, like catching division by zero as a Panic error, or calling revert as generic Error. See the Error Type Appendix for a full list of error types and their codes.

Try/catch statements are defined by placing the code that might throw an error right after the try keyword. A code block can then be place afterwards to define what should occur in the event of successful code execution. catch blocks are placed after this to define the behavior based on the type of error thrown.

Example

contract Divisor {

  function doTheDivide() public returns (uint) {
      return (1 / 0);
  }
}
contract DoTheDivide {

  Divisor public d;
  uint public errCount = 0;

  constructor() public {
      d = new Divisor();
  }

  function tryTheDivide() returns (uint, bool) {
      try d.doTheDivide() returns (uint v) {
          return (v, true);
      } catch Error(string memory itsamessage) { 
          // This is executed in case
          // revert was called inside doTheDivide()
          // and a reason string was provided.
          errCount++;
          return (0, false);
      } catch Panic(uint errCode) {
          // This is executed in case of a panic,
          // i.e. a serious error like division by zero
          // or overflow. The error code can be used
          // to determine the kind of error.
          errCount++;
          return (errCode, false);
      } catch (bytes bigTest) {
          // This is executed in case revert() was used.
          errCount++;
          return (0, false);
      }
  }

}

In the above example, a call to tryTheDivide would catch the Panic error and return the error code of 12.

Error type Appendix

As a reference, these are error types for SolidVM:

  • Require
    • Error code: none
    • Thrown when a require function's condition is not satisfied.
    • Error classification: Error
  • Assert
    • Error code: none
    • Thrown when a assert function's condition is not satisfied.
    • Error classification: Error
  • TypeError
    • Error code: 1
    • Reason: Thrown when a type error occurs, such as assigning a value of the wrong type to a variable. These errors typically happen at contract upload time.
    • Error classification: Panic
  • InternalError
    • Error code: 2
    • Reason: Thrown when an internal error occurs in the VM execution.
    • Error classification: Panic
  • InvalidArguments
    • Error code: 3
    • Thrown when an invalid number of arguments are given to a function.
    • Error classification: Panic
  • IndexOutOfBounds
    • Error code: 4
    • Thrown when an invalid index of an array is accessed.
    • Error classification: Panic
  • TODO
    • Error code: 5
    • Thrown when a feature/operation is unimplemented in SolidVM.
    • Error classification: Panic
  • MissingField
    • Error code: 6
    • Thrown when a symbol or element is missing from a statement or expression.
    • Error classification: Panic
  • MissingType
    • Error code: 7
    • Thrown when a symbol is declared as a non-existent type.
    • Error classification: Panic
  • DuplicateDefinition - 8
    • Thrown when a symbol is defined/declared multiple times
    • Error classification: Panic
  • ArityMismatch
    • Error code: 9
    • Thrown when instantiated a new array using the new keyword and the declared length mismatches the array literal's length.
    • Error classification: Panic
  • UnknownFunction
    • Error code: 10
    • Thrown when a function is called but not defined.
    • Error classification: Panic
  • UnknownVariable
    • Error code: 11
    • Thrown when a variable is referenced but not defined in the current scope.
    • Error classification: Panic
  • DivideByZero
    • Error code: 12
    • Thrown when dividing by zero.
    • Error classification: Panic
  • MissingCodeCollection
    • Error code: 13
    • Thrown when a contract's code collection is non-existent at a provided address, or is not SolidVM code.
    • Error classification: Panic
  • InaccessibleChain
    • Error code: 14
    • Thrown when attempting to access an invalid chain.
    • Error classification: Panic
  • InvalidWrite
    • Error code: 15
    • Thrown when attempting to write data to a state variable on another chain.
    • Error classification: Panic
  • InvalidCertificate
    • Error code: 16
    • Thrown when attempting to register an invalid certificate.
    • Error classification: Panic
  • MalformedData
    • Error code: 17
    • Thrown when a message hash, public key or EC signature could not be properly parsed by the built-in verifyCert, verifyCertSignedBy, and verifySignature functions.
    • Error classification: Panic
  • TooMuchGas
    • Error code: 18
    • Not thrown in SolidVM.
    • Error classification: Panic
  • PaymentError
    • Error code: 19
    • Thrown when attempting to pay a non-payable account.
    • Error classification: Panic
  • ParseError
    • Error code: 20
    • Thrown when a contract or its arguments cannot be properly parsed.
    • Error classification: Panic
  • UnknownConstant
    • Error code: 21
    • Thrown when attempting to access an unknown constant of a contract.
    • Error classification: Panic
  • UnknownStatement
    • Error code: 22
    • Thrown when attempting to access a feature not supported by the contract's current SolidVM version.
    • Error classification: Panic

Custom User Error Types

Users can now define custom user error types that can be used for revert statements or for debugging their contracts in a traditional developer way through the use of throw statements.

error <name> (...args);

Custom user error types can be defined at the contract or file level.

Example:

error flError();    // File Level Error
contract A {
    error clError(string message);      // Contract Level Error
    function throwError() {
        throw clError("CRITICAL FAILURE");
    }
    function revertError() {
        revert flError();
    }
}

Users can catch custom user error types in the SolidVM-style of try/catch statements.

Example:

error myError(string message);
contract A {

    constructor() {
        tryCatch();
    }
    function throwError() {
        throw myError("CRITICAL FAILURE");
    }
    function tryCatch() returns (bool) {
        try {
            throwError();
        }
        catch myError(msg) {
            return msg == "CRITICAL FAILURE";       // Returns True
        }
    }
}

Default Mapping Values

Mappings now return the default value (0 or "" or false) of the map, if the key does not exist. This functionality is now in parity with Solidity. The default values are based on the value type of the mapping. Please reference the Default Storage Values table .

This functionality is useful when using mappings to store a large collection of key-value pairs, and you need to query for a possibly non-existent key - user's can than check if the key was present or not by checking the returned type of the mapping access.

Example:

contract DefaultMappingValues {
  mapping(uint => bool) boolMap;
  mapping(address => uint) intMap;
  mapping(uint => string) stringMap;
  bool x;
  uint y;
  string z;
  constructor() {
    boolMap[1] = true;
    intMap[msg.sender] = 1;
    stringMap[1] = "Cartography";
    x = boolMap[9]; // x == false;
    y = intMap[address(0)]; // y == 0;
    z = stringMap[9];  // z == "";
  }
}

After initialising the mappings created in the constructor, the state variables x,y,z are given values for a key that is non-existent in a mapping. The values that x, y, and z have are false,0 and "" respectively.

Reserved Words

The following words are reserved in SolidVM to prevent errors when indexing contract data in Cirrus:

  • block_number
  • block_timestamp
  • block_hash
  • record_id
  • transaction_hash
  • transaction_sender
  • salt

Gas in SolidVM

SolidVM uses "gas" to prevent transactions from running infinitely. Standard Ethereum uses gas that is directly linked to an account's ethereum balance to pay for the transaction. SolidVM only implements a gas limit as a maximum upper bound on the number of computations (SolidVM statements) that can be made in a single transaction. This is possible since STRATO account balances and transaction computation is non-competitive, AKA there is no need to require users to pay for transactions. This allows users to still make their transactions without having to worry about their account balance. Transactions will not subtract tokens from a user's balance, or give additional tokens to validator nodes as "payment" for running the transaction.

The gas limit is set by the transaction parameters gasLimit and gasPrice and is calculated by the equation:

(gasLimit * gasPrice) + value

Where value is set automatically to 0 by the STRATO API. Similarly gasLimit and gasPrice are set default to 100,000,000 and 1 wei respectively when not provided in the transaction parameters. These values allow for a reasonably high number of statements to be run in a transaction.

So for all intents and purposes, users can ignore providing these values and their transactions will run successfully.

SolidVM Parsing

Typechecking

When a contract constructor is run using SolidVM, the SolidVM typechecker will automatically run on the contract code collection. If the contract fails this step, then a SolidVM type error will be thrown. A transaction that uploads a faulty contracts will have an error state, just as if it were a transaction that failed for other reasons, like an index out-of-bounds or non-existent function call. See the Typechecker Documentation in the IDE page for more information.

Optimizer

Following the typechecking stage, the SolidVM optimizer will prune contract code collection to minimize the excution time. No errors will be thrown from the optimizer. This is an on-going development and currently will only "optimize":

  • basic arithmetic literals. Example: 3 + 3 will be turned into 6

  • User Defined Value Types will be wrapped into their basic type

Debugging SolidVM

BlockApps offers several resources that allows developers to easily create and debug their SolidVM smart contract applications on STRATO.

VM Logs

If you are developer making contracts with direct access to an active STRATO node, you can enable VM logs within the STRATO container. Logs are enabled using an environment variable at boot time of a STRATO node.

By enabling logs, it may cause a slight performance hit to the VM, therefore it is recommended that this feature only be enabled during the development process of an application, and disabled during its active deployment. Once enabled, logs are enabled for both VMs on STRATO. See the below sections for info on each VM.

Once logs are enabled, you may access them using the following command on the STRATO host machine:

sudo docker exec -it strato_strato_1 cat logs/vm-runner

You may use similar file inspection commands with the vm-runner logs to inspect them more in-depth, such as grep or tail -f to examine logs as a contract is being executed.

SolidVM Logs

SolidVM offers verbose logs for each expression and statement within a contract, allowing for easy debugging.

Environment variable:

svmTrace=true
Example

Below is a simple contract that stores a value, y, and has a function f which sets y and returns a value depending on the value of its single parameter, x.

contract LogsContract {
    uint y;
    constructor(uint _y) {
        y = _y;
    }
    function f(uint x) returns (uint) {
        if (x % 2 == 0) {
            y = y + x;
            return (x * 2) + 1;
        }
        else {
            y = y - x;
            return x + 3;
        }
    }
}

When running the constructor with the value of _y = 5, we get the following logs:

Creating Contract: 69de75a9d810e139f14cb87d7dcd7fda620fa359 of type LogsContract
setCreator/versioning ---> getting creator org of 058c7ab489998e5b148cd7af1d4b84e236a8e193 for new contract 69de75a9d810e139f14cb87d7dcd7fda620fa359
setCreator/versioning ---> no org found for this creator....
╔══════════════════════════════════════════╗
║ running constructor: LogsContract(_y) ║
╚══════════════════════════════════════════╝

LogsContract constructor> y = _y;
(line 4, column 9):     Setting: y = 5
Done Creating Contract: 69de75a9d810e139f14cb87d7dcd7fda620fa359 of type LogsContract
setCreator/versioning ---> getting creator org of 058c7ab489998e5b148cd7af1d4b84e236a8e193 for new contract 69de75a9d810e139f14cb87d7dcd7fda620fa359
setCreator/versioning ---> no org found for this creator....
create'/versioning --->  we created "LogsContract" in app "" of org ""
[1111-11-11 01:01:01.4792820 UTC]  INFO | ThreadId 6     | printTx/ok                          | ==============================================================================
[1111-11-11 01:01:01.4793648 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Adding transaction signed by: 058c7ab489998e5b148cd7af1d4b84e236a8e193     |
[1111-11-11 01:01:01.4793981 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Tx hash:  6dc7261e9c1020a1cade551a50dddbfad59439918dfe44b172e7b76d10e54395 |
[1111-11-11 01:01:01.4794236 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Tx nonce: 2                                                                |
[1111-11-11 01:01:01.4794457 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Chain Id: <main chain>                                                     |
[1111-11-11 01:01:01.4794669 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Create Contract LogsContract(5) 69de75a9d810e139f14cb87d7dcd7fda620fa359   |
[1111-11-11 01:01:01.4795601 UTC]  INFO | ThreadId 6     | printTx/ok                          | | t = 0.00080s                                                               |
[1111-11-11 01:01:01.4795893 UTC]  INFO | ThreadId 6     | printTx/ok                          | ==============================================================================

We can see in these logs what contract constructor the VM is taking, as well as what values it is assigning to state variables. Notice the setCreator/versioning lines, these are useful when integrating X.509 or contract versioning features into your contracts, and org information or version numbers will show here. We can also see the address of the new contract being created, and of the contract creator.

Following the constructor, we can see that the transaction was successful and its relevant information. In this scenario, the transaction was created on the main chain and the transaction time was 0.00080s.

Suppose we call the function f with the argument of x = 3:

[1111-11-11 01:01:01.4121601 UTC]  INFO | ThreadId 6     | addTx                               | gas is off, so I'm giving the account enough balance for this TX
----------------- caller address: Nothing
----------------- callee address: 69de75a9d810e139f14cb87d7dcd7fda620fa359
callWraper/versioning --->  we are calling LogsContract in app "LogsContract" of org ""
╔═════════════════════════════════════════════════════════════════╗
║ calling function: 69de75a9d810e139f14cb87d7dcd7fda620fa359 ║
║ LogsContract/f(3)                                          ║
╚═════════════════════════════════════════════════════════════════╝

            args: ["x"]
f> if (x % 2 == 0) {
y = y + x;
    return x * 2 + 1;

} else {
y = y - x;
    return x + 3;

}
            %% val1 = SInteger 1
            %% val2 = SInteger 0
(line 7, column 9):        if condition failed, skipping internal code
f> y = y - x;
(line 12, column 13):     Setting: y = 2
f> return x + 3;
╔════════════════════╗
║ returning from f: ║
║ 6                 ║
╚════════════════════╝

[1111-11-11 01:01:01.4126984 UTC]  INFO | ThreadId 6     | printTx/ok                          | ==============================================================================
[1111-11-11 01:01:01.4127614 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Adding transaction signed by: 058c7ab489998e5b148cd7af1d4b84e236a8e193     |
[1111-11-11 01:01:01.4127893 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Tx hash:  46351a0ebf5dd34d06f7e6a56a1b5206f347b2de7b7538cc792a153bb0a0c43b |
[1111-11-11 01:01:01.4128138 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Tx nonce: 3                                                                |
[1111-11-11 01:01:01.4128355 UTC]  INFO | ThreadId 6     | printTx/ok                          | | Chain Id: <main chain>                                                     |
[1111-11-11 01:01:01.4128998 UTC]  INFO | ThreadId 6     | printTx/ok                          | | calling 69de75a9d810e139f14cb87d7dcd7fda620fa359/f(3)                      |
[1111-11-11 01:01:01.4129256 UTC]  INFO | ThreadId 6     | printTx/ok                          | | t = 0.00055s                                                               |
[1111-11-11 01:01:01.4129473 UTC]  INFO | ThreadId 6     | printTx/ok                          | ==============================================================================

Like the contract constructor, it also easy to see the beginning of a call and its initial parameters it was called with. After the first box showing call information of the function, each step of the function is shown, with actual src code prefaced by the name of the function f and right caret. When the VM encounters an if-statement, we can see its comparison below the source code. Each argument in the boolean expression is evaluated and displayed as %%val1 and %%val2 respectively. Since 3 % 2 = 1, val1 = 1. Since the expression evaluated to false, the internal code is skipped. It then follows each line of code, setting y = 2 since 5 - 3 = 2. Lastly it boldly shows the return value in a box. At this point the function execution has terminated and the transaction is complete, and the same transaction information is displayed as before.

Please note due to your display or current window size, logs may format incorrectly or unpredictably. It is best to view tables and wide console outputs on a large display/window.

Enable Contract Debugging Tools

STRATO has several built-in SolidVM smart contract debugging tools. These tools must be enabled manually at STRATO boot time to be used. Currently they are available for easy use through the BlockApps IDE.

Enable SolidVM Debugging by setting the vmDebug environment variable to true:

vmDebug=true
when starting STRATO.

IDE

BlockApps has an IDE through its VS Code extension on the VS Code Marketplace. The IDE provides access to many useful tools for developing applications with STRATO. This allows for rapid development since it brings STRATO features into an accessible and integrated environment.

Features:

  • Static Analysis and type-checking for SolidVM contracts (.sol files)
  • Code Fuzzing Tools (checking for unexpected results)
  • Debugger
  • STRATO Project Management
  • Node information

View the STRATO IDE Documentation for the full list features, and how to setup the extension within your workspace.

Limitations

SolidVM does not currently support the following Solidity features:

  • Unsupported:
    • fixed data type usage.
    • Libraries and the using keyword.
    • Inline assembly (since assembly instructions are not used in SolidVM.)
    • Visibility modifiers on functions and variables are parsed but .