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)
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.
- Takes a PEM/DER encoded certificate and parses it as a
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
- This function checks if the certificate is signed by the public key. This contrasts with
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
- The
tx.organization
: string- The
tx.origin
's registered Organization
- The
tx.group
: string- The
tx.origin
's registered Organizatinal Unit
- The
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.
- The Unix timestamp of the expiration of the certificate and can be parsed as an
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
}
}
{
"_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"; |
string |
address |
The string literal value is interpreted as a base-16 blockchain address. | string s = "12"; |
int/uint |
string |
Converts a number to its base-10 string representation. | int x = 12; |
int/uint |
address |
Interprets a number as a base-16 value. See account/address types. | int x = 12; |
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;
}
}
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:
- 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.
- 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
- Error code:
- InternalError
- Error code:
2
- Reason: Thrown when an internal error occurs in the VM execution.
- Error classification:
Panic
- Error code:
- InvalidArguments
- Error code:
3
- Thrown when an invalid number of arguments are given to a function.
- Error classification:
Panic
- Error code:
- IndexOutOfBounds
- Error code:
4
- Thrown when an invalid index of an array is accessed.
- Error classification:
Panic
- Error code:
- TODO
- Error code:
5
- Thrown when a feature/operation is unimplemented in SolidVM.
- Error classification:
Panic
- Error code:
- MissingField
- Error code:
6
- Thrown when a symbol or element is missing from a statement or expression.
- Error classification:
Panic
- Error code:
- MissingType
- Error code:
7
- Thrown when a symbol is declared as a non-existent type.
- Error classification:
Panic
- Error code:
- 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
- Error code:
- UnknownFunction
- Error code:
10
- Thrown when a function is called but not defined.
- Error classification:
Panic
- Error code:
- UnknownVariable
- Error code:
11
- Thrown when a variable is referenced but not defined in the current scope.
- Error classification:
Panic
- Error code:
- DivideByZero
- Error code:
12
- Thrown when dividing by zero.
- Error classification:
Panic
- Error code:
- 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
- Error code:
- InaccessibleChain
- Error code:
14
- Thrown when attempting to access an invalid chain.
- Error classification:
Panic
- Error code:
- InvalidWrite
- Error code:
15
- Thrown when attempting to write data to a state variable on another chain.
- Error classification:
Panic
- Error code:
- InvalidCertificate
- Error code:
16
- Thrown when attempting to register an invalid certificate.
- Error classification:
Panic
- Error code:
- 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
, andverifySignature
functions. - Error classification:
Panic
- Error code:
- TooMuchGas
- Error code:
18
- Not thrown in SolidVM.
- Error classification:
Panic
- Error code:
- PaymentError
- Error code:
19
- Thrown when attempting to pay a non-payable account.
- Error classification:
Panic
- Error code:
- ParseError
- Error code:
20
- Thrown when a contract or its arguments cannot be properly parsed.
- Error classification:
Panic
- Error code:
- UnknownConstant
- Error code:
21
- Thrown when attempting to access an unknown constant of a contract.
- Error classification:
Panic
- Error code:
- UnknownStatement
- Error code:
22
- Thrown when attempting to access a feature not supported by the contract's current SolidVM version.
- Error classification:
Panic
- Error code:
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 into6
-
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
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
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 .