Skip to main content

Smart Contract

In the PREDA model, all contract deployed on the chain have a unique name in the format of "DAppName.ContractName". The dapp name is given as a parameter when deploying the contract. The contract name is defined in the contract's source code.

Contract Definition

The main part of PREDA source code is the definition of the contract, which usually looks like:

contract MyContract {                // here, the contract name is defined as "MyContract"
// contract code here:
// enumeration type definition
// structure type definition
// user-scope definition
// interface definition
// state variable definition
// function definition
}

Enumeration and structure definition have already been covered in previous sections, scopes and interfaces will be introduced later in this section. These definitions don't have to strictly follow the order shown above and can be interleaved, although it's recommended to keep them structured to allow for easier reading.

State Variables

A state variable is defined similarly way to regular variables, except that it does not have an initializer and an optional scope could be added before the type (discussed later).

[scope] TypeName variableName;

Similar to regular variables, a state variable declaration statement can also be prefixed by the 'const' keyword. In this case, it is not actually stored in the contract state storage, but rather used as a compile-time constant. Therefore, scope is no longer necessary and an initializer should be provided.

'const' TypeName variableName = initializer;

Since this variable must be constant at compile-time, the initializer can only reference other constant state variables or literals.

contract c{
const hash hhh = ccnwe8x9sig7gb98zkb6gh@qarffhvf6c3ok9433@tz9ne4mb6qi:hash; // Ok: initialized as a literal
const string sss = string(hhh); // Ok: Only referencing another constant
int32 i;
const int32 j = i; // Compile error: 'i' is not constant
}

Functions

A function is defined as follows:

[scope] 'function' [returnValueType] functionName(parameterList) [accessSpecifier] ['const'] {
//function body
}

If returnValueType is not given, the function does not return any value.

When specified after the parameter list. 'const' makes the function constant, which means that it cannot modifier any state variable and cannot call other non-const functions (whether in the same or another contract). Constant functions also cannot issue a relay call.

By default, all functions of a contract can only be accessed from within the contract itself. To make a function accessible from other places, access specifiers need to be added to the function definition.

There are two access specifiers available, to enable a function to be invocable from a transaction or another contract:

specifieraccessibilityconstraints on function
exportcan be invoked by a transactionCannot have move-only parameters
publiccan be called from another contractno constrains

A function can also have both specifier so that it's available for both contracts and transactions.

Sharding Schemes and Scopes

On conventional non-sharding blockchains, each smart contract's state can be seen as a single global instance, which is accessible across the chain. On sharding blockchains, the state of a contract be distributed across multiple shard based on its sharding scheme to achieve parallelism. In the PREDA model, contract developers have the flexibly to freely define how the state of a contract is structured on a shading blockchain by using scopes.

Each state variable or function, as shown in the previous section, can include a scope in its definition. In general, the scope of a state variable defines how many copies of that variable are there on the chain and how they are indexed; and the scope of a function defines which state variables it has access to.

Built-in Scopes

PREDA has the following built-in scopes global, shard, address, uint32, uint64, uint96, uint128, uint160, uint256 and uint512.

The global scope is the equivalent of a conventional smart contract, everything defined in the global scope has only one single instance globally.

The shard scope defines states that has one instance for each shard on the chain.

The address scope defines states that has one instance for each valid address on chain.

The uint scopes are similar to address, except that the defined state has one instance for each valid value of the corresponding uint type.

contract MyContract {
@global uint32 numTotalAccounts; // only one instance globally
@shard uint32 numAccountsInShard; // one instance per shard
@address uint512 addressBalance; // one instance per address
@uint256 string str; // one instance for each
// valid value of uint256
}

Note: When a state variable or function is defined without specifying a scope, it defaults to @global.

In the above case, str can have up to 2^256 - 1 instances, indexable by a uint256 value. In which shard each of these instances resides, is decided by the underlying blockchain system and transparent to the contract.

Accessing State Variables and Functions across scopes

A state variable defined in any scope other than global can have multiple instances stored across the blockchain. What a function is executed, it has access to state variables defined with the same scope but limited to one instance. This instance is indexed by the so-called scope target. Access to variables in another target of the same scope is only possible with an asynchronous relay.

contract MyContract {
@address string s;

// SetS() has scope address and is always executed with a scope target of type address
@address function SetS(string newS) {
s = newS; // the accessed state variable s is from the current scope target
}

@address function SetRemoteS(address otherAddr, string newS) {
relay@otherAddr SetS(newS); // other instances of the same scope only accessible via relay, here it calls SetS() with newS as the scope target
}
}

A function cannot access state variables defined in another scope directly but only via relaying to a function of that scope.

contract MyContract {
@address string s;

@address function SetS(string newS) {
s = newS;
}

@shard function SetAddressS(address addr, string newS) {
relay@addr SetS(newS); // relay to a function in address scope with newS as the scope target
}
}

global and shard scopes are two special cases. Since the global scope has only instance, it is readable in any scope but only modifiable inside the global scope. Its const functions can also be called directly from any scope.

contract C {
// state variables and function defined without a scope defaults to @global
int32 i;
function int32 Get() const {
return i;
}
function Set(int32 newValue) const {
i = newValue;
}

@address int32 j;
@address function CopyValue() {
j = i; // read-only access to i defined in global scope
j = Get(); // call a const function defined in global scope
}
@address function SetGlobalValue(int32 newValue) {
relay@global Set(newValue); // non-const global function only accessible via relay, like functions in other scopes
}
}

State variables in the shard scope has one instance in each shard of the blockchain. Any other scope other than the global scope has read-write access to the instance in the current shard.

contract C {
@shard bool b;
@shard function Enable() {
b = true;
}
@address function f() {
b = !b; // direct read write access to shard scope instance in the current shard
Enable(); // call a function in the shard scope, it is executed in the context of the current shard
}
@global function g() {
relay@shards Enable(); // global functions are not executed in any shard, it can only use relay@shards statement to broadcast to all shards
}
}

Note: Relaying to a specific shard using a shard index is not possible.

System Reserved Functions

System-reserved functions are a group of special functions with the names reserved by PREDA for special purposes. They don't always have to be defined by a contract. But when they are, the definition must match a certain signature and will be invoked by the system at certain points.

on_deploy()

on_deploy is a global function that is automatically invoked when a contract is deployed. It works like a constructor and can be used to do some initialization of the contract state. The signature is:

function on_deploy(parameterList)

on_scaleout()

on_scaleout is a shard function that is invoked when a scaleout happens, i.e. when the shard order of the blockchain system is increased by 1 and the total number of shards doubles from 2^(shard_order-1) to 2^shard_order.

On scaleout, each of the old 2^(shard_order-1) shards is forked to two new shards: shard[i] -> shard[i] and shard[i + 2^(shard_order-1)], where 0 <= i < 2^(shard_order-1). on_scaleout is called 2^shard_order times, once per shard. It can be used to split the old per-shard contract state to the into the two new shards. The signature is:

function on_scaleout(bool)

The boolean parameter tells whether the current shard is forked in place (when false), or with offset 2^(shard_order-1) (when true). Its value is basically block.get_shard_index() >= 1u32 << (block.get_shard_order() - 1u32).

Execution Context

During the execution of contract code, the runtime provides with some built-in data and interfaces called the execution context.

An execution context includes:

  1. Contract state context, including states variables defined in the contract. If the function is defined as const, the access is read-only, otherwise it's read-write. These variables can be directly accessed using their name.
  2. Transaction context, containing metadata of the transaction that directly / indirectly triggered the function call. Typical data in the transaction context are sender address (who authorized the transaction), current address (in the case of a relay call), transaction parameters, etc. These data can be accessed through built-in functions.
  3. Block context, containing metadata of the block, in which the transaction is about to be included. Typical data in the block context are shard index, block height, block timestamp, etc. These data can be accessed through built-in functions.

Check the Runtime Environment section for a detailed list of available data in these contexts.

Working with Multiple Contracts

In PREDA, a contract could interact with other contracts that are already deployed on the chain.

Importing Contracts

To interact with another contract, that contract must first be imported to the current contract.

import DAppName.ContractName [as AliasName];

DAppName and ContractName are the corresponding names assigned when deploying that contract. AliasName is an optional arbitrary identifier to reference it in the current contract. If AliasName is not given, ContractName will be used instead for referencing.

import must be declared before contract definition.

Explicit Import and Implicit Import

When a contract is imported by an import directive, it is explicitly imported. Besides that, a contract could also be implicitly imported if it is indirectly imported, like in the following example.

contract ContractA{
}
import MyDApp.ContractA as A;    // ContractA is explicitly imported
contract ContractB{
}
import MyDApp.ContractB as B;    // ContractB is explicitly imported
// ContractB imports ContractA, therefore ContractA is implicitly imported here
contract ContractC{
}

An implicitly-imported contract doesn't have a user-defined alias and can be reference by its contract name by the compiler. In the above example, MyDApp.ContractA is referenced as ContractA in Contract C. To have a specific alias, it could be explicitly imported again. For example:

import MyDApp.ContractB as B;    // ContractB is explicitly imported
import MyDApp.ContractA as A; // now ContractA is explicitly imported as A, this overrides the implicit import via contractB
contract ContractC{
}

Using Types and Scopes Defined in Other Contracts

After importing a contract, all user-defined types from it could be accessed under the contract alias.

contract ContractA{
struct S{
int32 i;
}
enum E{
E0,
E1
}
}
import MyDApp.ContractA as A;
contract ContractB{
@address A.S s;
@address A.E e;
@address function f(){
s.i = 1i32;
e = A.E.E0;
}
}

Calling Functions Defined in Other Contracts

Similar to user-defined types, public functions defined in other contracts could also be directly referenced via the alias.

contract ContractA{
struct S{
int32 i;
}
enum E{
E0,
E1
}
// must be defined as public to be callable from other contracts
@address function f(S s, E e) public{
}
}
import MyDApp.ContractA as A;
contract ContractB{
@address A.S s;
@address A.E e;
@address function f(){
A.f(s, e); // call public function f from MyDApp.ContractA
}
}

The basic scope visibility rules hold for cross-contract calls, i.e. each scope can only call function in the same scope, in the shard scope and const functions in the global scope

Interfaces

Interfaces provide another way to work with multiple contracts. While only known contracts can be imported, interfaces enables interaction with arbitrary contracts that implements it, thus achieving runtime polymorphism.

Defining an Interface

Interfaces are defined at the contract level. Each interface is a set of function definitions with empty bodies. Similar to regular functions, the functions of an interface must also reside in scopes:

contract A {
// defining an interface
interface Addable {
// The interface has two functions, each in a different scope
@address function Add(uint64 value);
@global function uint64 GetTotal() const;
}
}

The above contract defines an interface Addable with 2 functions, each in a different scope. Interfaces can use scopes freely like scopes in contracts, including user-defined scopes and imported scopes from other contracts.

Implementing an Interface

Contracts can choose to implement interfaces using the implements keyword at definition. A contract can choose to implement arbitrary number of interfaces, which can either be those defined in the same contract, or imported interfaces from other contracts.

import A;
contract B implements A.Addable, Printable { // use "implements" to implement interfaces
interface Printable {
@global function Print() const;
}

uint64 total;
function uint64 GetTotal() public const { // GetTotal() for A.Addable
return total;
}
function Print() public const { // Print() for Printable
__debug.print(globalTotal);
}
@address function Add(uint64 value) public { // Add() for A.Addable
relay@global (^value) { // global scope is read only in other scopes, must use relay to modify its state
total += value;
}
}
}

The above contract implements two interface: Printable defined in the contract itself, and Addable defined in contract A from the previous section.

To implement an interface, a contract must implement all the functions defined in that interface, and the signature of the implemented function must match exactly the definition in the interface, i.e. same function name, parameter list and type, return type, const-ness and scope. In addition, interface function must be implemented as public, since they used for cross-contract calls.

Using Interfaces

When a contract implements an interface, other contracts can interact with it via the interface. For example:

import B;                                       // A is implicitly imported via B
contract C {
@address function test() {
A.Addable addable = A.Addable(B.__id()); // define a variable of interface A.Addable
// and initialize it with contract B's id
addable.Add(100u64); // Calls B.Add() via the interface
}
}

In the code above, a variable of interface type A.Addable is defined. Interface types can be initialized with a contract id. Here, it is initialized with B's id using the build-in function __id() that is automatically generated for each contract. Once a interface variable is initialized, it can be used to call any function defined in the interface and routed to the corresponding implementation in contract B.

With interfaces, a contract can interact with any other contract that implements the interface without knowing them. For example:

import A;       // No need to import any other contract other than A, where the interface is defined
contract Adder {
@address function Add(A.Addable addable, uint64 value) public {
addable.Add(value);
}
}

Here the function Add accepts an A.Addable interface as parameter, which could possibly be initialized by the id of any other contract that implements A.Addable.

Note: If calling a function on an interface variable that is uninitialized, or initialized with the id of a contract that actually doesn't implement the interface, an error would occur and contract execution will stop immediately.

All interface types have the following built-in functions: | function | return type | arguments | is const | description | |:--------:|:-----------:|:---------:|:--------:|:--------------------------------------------:| | id | uint64 | None | Yes | Returns id of the current bound contract | | valid | bool | None | Yes | Checks if the bound contract implements this interface |

Deploy Unnamed Contract

Contracts can be deployed via a transaction or from within a contract. A contract deployed via a transaction is already named, that it could be imported using its name, as shown in Importing Contracts. A contract deployed programmatically inside a contract, on the other hand, is unnamed, that it cannot be referenced through a name but rather only through its contract id.

An unnamed contract is deployed using the deploy statement:

deploy contractName(parameters);

Here contractName is the alias name of an imported contract and parameters should match the argument list of the imported contract's on_deploy function. It can be thought of as and create a new contract using the code of contractName but with fresh new contract state. The deploy statement returns a uint64 value as the contract id of the newly deployed contract, which could be used to reference it using a contract type variable.

contract ContractA{
int32 value;
function on_deploy(int32 v) {
value = v;
}
function int32 get_value() {
return value;
}
}
import MyDApp.ContractA as A;
contract ContractB {
function f() {
uint64 cid0 = deploy A(42); // deploy a new contract using A's code
A a0 = A(cid0); // Reference the newly created contract using a contract type variable
uint64 cid1 = deploy A(100); // deploy another new contract using A's code
A a1 = A(cid1);

int32 v0 = a0.get_value(); // v0 is 42
int32 v1 = a1.get_value(); // v1 is 100
int32 v = A.get_value(); // Referencing the named contract A, the value of v depends on the argument
// passed by the transaction that deployed A
}
}

Deploy statements are only allowed inside a non-constant global scope function.

Supply Tokens from a Contract

A contract can supply its own type of token using built-in functions __mint and __burn that are automatically generated for each contract. | function | return type | arguments | is const | description | |:------------:|:-----------:|:-----------------:|:--------:|:--------------------------------------------:| | mint | token | bigint amount | Yes | mint the amount of token | | burn | None | token tk | Yes | burn the tokens stored in tk |

The id of the token returned by __mint is the same as the id of the contract. tk passed to __burn must contain token with the same id of the contract, otherwise the function would do nothing.