_images/combine.png

Contracts

Smart Contracts

A smart contract is a script whose inputs and outputs are data stored on a distributed database. The logic of the contract can be interacted with through transactions and can run complex logic that processes reads and writes to the database. Smart contracts can model a specific process associated with a real world financial instrument or contract, but are only a subset of the logic which involves counterparty communication and settlement. They are not an embodiment of legal logic or terms of a contract. Put simply, contracts are effectively a data reconciliation workflow on a database that corresponds to a business process.

_images/methods.png

Contracts are constructed in Uplink’s scripting language, FCL. The contracts constructed in FCL are described in an asset modeling language which does not allow arbitrary computation to be constructed - it is not a general purpose tool. Instead, FCL is constructed to be model a specific set of requirements around the static analysis of contracts and facilitate both humans and machine intelligences to analyze the integrity and all possible states of the contract.

State Machine Representation

Every smart contract in Uplink is equivalent to a state machine which describes the valid transitions between accessible logic in the contract. The state machine consists of a finite set of accessible methods and valid graph of transitions between them. Within the methods is all the logic that describes the time-varying transfer of rights, obligations and assets between counterparties named in the contract.

A contract consists of a state sequence diagram which is a directed graph. A contract is only ever in a single state at a point in time. The possible sequences of states that the contract can enter is described as a graph structure of edges and nodes which define the validity of a state transition. This allows us to analyze all possible paths of execution, and their memory states by restricting the methods and logic that a contract can execute while in a state.

_images/transition.png

Every contract has an initial and terminal state which are are the start and end points of its lifetime. The reset of the intermediate states (example: settlement, confirmation, trading, etc) are named nodes in the graph which restrict the possible methods and logic that can be called.

Transition Declarations

In order to define the state sequence diagram, we need to provide in our contract a list of transitions. For example, we can describe the graph above with the following list of transition declarations:

transition initial -> orange;
transition initial -> yellow;
transition orange -> green;
transition yellow -> green;
transition green -> terminal;

The FCL compiler performs a couple of sanity checks on the graph defined in a contract:

  • every state can be reached from the initial state
  • every state can reach the terminal state

These checks help the contract writer ensure that the contract will not have unreachable states and that there is always a path towards the termination of the contract.

State Annotations

Methods are annotated with a precondition on the state upon entry into the method.

@annotation
foo() {
  ...
}

The annotation names the state the contract has to be in, in order to allow calls to the annotated method. That means, if the previous method call didn’t invoke transitionTo(:annotation), calls to foo() will be rejected. State annotations are used to define valid directions inside a contract. This is easily seen when looking at contracts written in FCL, our open source Financial Core Language, where a contract’s life cycle has to start in the method annotated with @initial and end using a call to the primop (primitive operation)``terminate(msg)``.

The FCL compiler checks for every method that its state annotation (also called the method’s source state) and transitionTo states (or the method’s destination states) correspond to transitions present in the list of transition declarations. Vice versa, it is checked that every transition declaration has at least one method that can trigger that particular state transition.

Another way to interpret this is that transition declarations provide a specification of the state diagram of the contract. The compiler then ensures that the collection of methods form a valid implementation of this specification.

Turing-Incomplete & Totality Enforcement

FCL was specifically designed as a Turing incomplete programming language. General purpose languages like Python and Java are Turing complete. Turing complete languages give the programmer the ability to write arbitrary programs which can compute arbitrarily complex results. In contrast, Turing incomplete languages restrict the type of programs one can write. Restricting the kind of programs the user could write might look like a bad idea but as it turns out, being Turing complete is a disadvantage when safety and a high degree of analyzability of programs are of highest priority. Being Turing incomplete makes it possible for us to guarantee program termination.

Uplink and FCL are tailored for financial applications which do not require Turing completeness and greatly profit from the benefits of safety and analyzability.

In FCL contracts are also total, all programs and calls to methods terminate in a finite number of steps which is enforced statically. Unbounded computation is rejected by construction in the language.

Methods

FCL methods are defined in the source code of smart contracts, directly following global and local variable declarations and assignment. They are comprised of a state annotation (as described above), an optional access restriction, the method name, a list of zero or more arguments, and a function body which may contain variable assignments and primitive operations (defined in the Primitive operations section) and may or may not return a result.

Calling a method

Calling a method is done by issuing a CallContract transaction. Methods can neither call themselves nor other contract-defined methods within their body, as this would allow for recursion and would severely limit the ways in which the contract code can be statically analyzed. Methods are publicly available interfaces that define the means by which counterparties can interact with a contract. A transaction that includes a successful call to a method may result in an altered contract state, as outlined above. However, methods can call any one of zero or more Helper Functions defined at the bottom of the FCL program such that repeated logic that has no effect on the ledger can be extracted such that FCL users can properly implement the DRY (Don’t Repeat Yourself) ideology.

Example:

@someState
transfer(account u, assetFrac2 a, fixed2 amount) {
  if (accountExists u && holderBalance(sender()) >= amount) {
    transferHoldings(sender(), a, addInterest(amount), u);
  }
}

...

addInterest(fixed2 amount) {
  amount + amount * 0.05f;
}

Types

FCL is a typed language which assigns a static type to all possible values in the language. This type restricts the possible values and functions that can operate over the expression in the language and prevent invalid states from occurring.

Kinds

In FCL there is a notion of “classes” or “kinds” of types: general, asset, holdings and collection a. Most types are of kind general, such that it is legal to use these types only where the exact type is specified. Types of other kinds may be used anywhere that their general kind is mentioned, as well as anywhere their explicit type is specified. For example, the type account is a general type and thus may only be used anywhere the type account is mentioned in a method, helper function, or prim-op type signature. In contrast, the type map<account,int> has kind collection a and can be used anywhere the kind collection a is specified, but also wherever the specific type map<account,int> is mentioned. Furethermore, the collection a kind is parameterized by another type; That is, the type kind collection a can be read as “a collection type with values of type a” (where a is any type). The type-kind hierarchy is depecticted below:

               general
   _____________/ | \_____________
  /               |               \
asset          holdings       collection a

i.e If a type is of kind asset, holdings, or collection a then it is also of kind general, but if a type is of kind general then it is not of any other kind.

Kinds of types can be thought of as “classes of types”, where all types are in the class of general types, but not all general types are in the subclasses asset, holdings, and collection. The kind of a type dictates which primops it may be used as an argument for and/or returned from; In other domains, this language feature is known as polymorphism. Furthermore, because of FCL’s sophisticated type inference, certain arguments of primops are fixed to be a certain type depending on the kind of type passed in as other arguments to the same primop. For example, the circulate primop takes two arguments, one of kind asset and the other of kind holdings; when the asset type assetDisc is applied as the first argument, the holdings type is fixed to be int, as you can only circulate an integer amount of an asset that is divided discretely.

FCL has a type system that represents exactly the types of values needed in the domain of designing complex multi-party workflows:

Type Kind Description
int holdings Type of 64 bit integers
float holdings Type of double precision floats
fixed1 holdings Type of fixed-point number with precision 1
fixed2 holdings Type of fixed-point number with precision 2
fixed3 holdings Type of fixed-point number with precision 3
fixed4 holdings Type of fixed-point number with precision 4
fixed5 holdings Type of fixed-point number with precision 5
fixed6 holdings Type of fixed-point number with precision 6
bool general Type of booleans
account general Type of account addresses
assetDisc asset Type of discrete asset addresses
assetBin asset Type of binary asset addresses
assetFrac1 asset Type of fractional asset addresses with precision 1
assetFrac2 asset Type of fractional asset addresses with precision 2
assetFrac3 asset Type of fractional asset addresses with precision 3
assetFrac4 asset Type of fractional asset addresses with precision 4
assetFrac5 asset Type of fractional asset addresses with precision 5
assetFrac6 asset Type of fractional asset addresses with precision 6
contract general Type of contract addresses
msg general Type of messages
sig general Type of ECDSA signature
datetime general Type of date and time values
timedelta general Type of periods of time
state general Type of graph state labels
map<a,b> collection Type of a key value store mapping keys of type a to values of type b
set<a> collection Type an unordered collection of unique values of type a
(a,b,...) -> c helper Type of helper function in FCL
any general The type of type; This type cannot be written by the user

Primitive Operations

Primitive``rations (primops) are methods defined in the FCL language specification, usable in any smart contract method body. They provide functionality that allow smart contract composers to perform useful operations that cannot be expressed using other language primitives.

Some of the primops included in FCL operate over only specific kinds of types (as described above); Furthermore, other primops operate over specific types and/or kinds of types with which other arguments supplied to the same primop are forced to be of a certain type based on the type of the most important argument.

*Note: For primops operating over kinds of types, we will use the ' prefix to denote argument and returns types ranging over a kind of type. For instance, if the transferHoldings primop takes a type of kind asset as an argument, we will denote it as such transferHoldings('asset, ...).

Function Arity Return Type Description
verify(account, msg, sig) 3 bool Verify a signature
sign(msg) 1 sig Sign a message
block() 0 int Active block
deployer() 0 account Get the deployer (or owner) of a contract
sender() 0 account Get the creator of current transaction
created() 0 datetime Time of contract creation
address() 0 address Address of contract
validator() 0 account Current validator of contract
sha256(any) 1 msg SHA256 digest of a message
accountExists(account) 1 bool Check if an account with the given address exists in world state
contractExists(contract) 1 bool Check if a contract with the given address exists in world state
terminate(msg) 1 any Transition to terminal state
now() 0 datetime Get block creation time of current transaction
transitionTo(state) 1 void Transition to named state
currentState() 0 state Get the current state
between(datetime, datetime, e) 3 bool Evaluate the expr e if now is between the arg1 and arg1
txHash() 0 msg Get hash of current transaction
contractValue(contract, msg) 2 <inferred> Get a value in the contract’s storage, return type inferred
contractValueExists(contract, varName) 2 bool Get a value’s existence in a contract’s global storage
contractState(contract) 1 state Get the state of a smart contract
novationInit(int) 1 void Start novation side logic
novationStop() 0 void Start novation side logic
isBusinessDayUK(datetime) 1 bool Check if datetime is a business day or not
nextBusinessDayUK(datetime) 1 datetime Get the next business day after the supplied datetime
isBusinessDayNYSE(datetime) 1 bool Checking if datetime is a business day or not
nextBusinessDayNYSE(datetime) 1 datetime Get the next business day after the supplied datetime
fixed1ToFloat(fixed1) 1 float Coerce a fixed point number into a floating point number
fixed2ToFloat(fixed2) 1 float Coerce a fixed point number into a floating point number
fixed3ToFloat(fixed3) 1 float Coerce a fixed point number into a floating point number
fixed4ToFloat(fixed4) 1 float Coerce a fixed point number into a floating point number
fixed5ToFloat(fixed5) 1 float Coerce a fixed point number into a floating point number
fixed6ToFloat(fixed6) 1 float Coerce a fixed point number into a floating point number
floatToFixed1(float) 1 fixed1 Coerce a floating point number into a fixed point number
floatToFixed2(float) 1 fixed2 Coerce a floating point number into a fixed point number
floatToFixed3(float) 1 fixed3 Coerce a floating point number into a fixed point number
floatToFixed4(float) 1 fixed4 Coerce a floating point number into a fixed point number
floatToFixed5(float) 1 fixed5 Coerce a floating point number into a fixed point number
floatToFixed6(float) 1 fixed6 Coerce a floating point number into a fixed point number

Asset Operations

The subset of the primops operate over only the asset kind, known as “asset-primops”, have the type of one of their arguments or their return type decided by the type of asset that was passed as an argument. For example, a call to the transferHoldings asset-primop with the argument of type assetDisc, the type of the third argument (the amount of holdings to be transferred) must be int. This is because discrete asset holdings are referred to with integers, as integers are a numeric type that cannot be divided non-whole numbers. A more explicit example is denoted by this concise FCL contract:

transition initial -> terminal;

@initial
transfer(account from, assetFrac2 a, fixed2 amount, account to)
  transferHoldings(from, a, amount  to);
  terminate("transfer complete");
}

In this example, the transferHoldings primop takes an argument of type asset, such that the argument amount must have type fixed2, since the type of the holdings (illustrated below) of the assetFrac2 or fractional assets which are divisible up to two decimal places. In fact, FCL will report a type error if a user passes in an amount to transfer of a different type than fixed2 due to the languages type inference engine. A list of all the primops that operate over types of kind asset and kind holdings are found below:

Asset Function (AssetPrimOp) Arity Return Type Description
assetExists('asset) 1 bool Check if an asset with the given address exists in world state
holderBalance('asset, account) 2 'holdings Get balance of a holder of a of any type of asset
transferTo('asset, 'holdings) 2 void Transfer n asset holdings to account
transferFrom('asset, 'holdings, account) 3 void Transfer n asset holdings from contract address to account
circulate('asset, 'holdings) 2 void Circulate n asset supply from the asset supply to the asset issuer
transferHoldings(account, 'asset, 'holdings, account) 4 void Transfer n asset holdings from an account to another account

The mapping of types of kind asset to the types of their holdings is provided below:

Asset Type FCL Type (kind asset) Asset Holdings Type (kind holdings)
Binary assetBin bool
Discrete assetDisc int
Fractional Prec1 assetFrac1 fixed1
Fractional Prec2 assetFrac2 fixed2
Fractional Prec3 assetFrac3 fixed3
Fractional Prec4 assetFrac4 fixed4
Fractional Prec5 assetFrac5 fixed5
Fractional Prec6 assetFrac6 fixed6

Collections

Collections are a kind of type that represents a structure agnostic collection of values; This type kind is parameterized by another type, the type of values contained in the collection. The two kinds of collections currently supported by FCL are maps (general key/value stores), and sets (unordered list of unique values). The reason for this generalization of collections over discussing the specific map and set types is due to the nature of the structure of collections; There are many operations that operate over a collection of values and rather than define a specific function to operate over a specific collection, these operations can be generalized. In this case, instead of defining the transform primop for both maps and sets explicitly, FCL can define the primop once and generalize its arguments to take any collection as an argument. This allows the FCL primop list and code to be general and succinct, instead of having to defined a specific primop for both maps (mapTransform) and sets (setTransform), and for every type of kind collection added to FCL in the future.

Higher Order Functions

Along with collection primops comes the notion of higher-order functions, functions that can either take functions as arguments or return them; i.e. functions become values, like integers or booleans. In FCL, collection primops (and some primops that operate over specific types of collections) and helper functions adopt this property, where the former may sometimes take a helper function name as an argument denoting the function to apply to each member of the collection. This feature allows for FCL users to update or modify existing collection values in the workflow clearly and succinctly.

*Note: As the values of collection types can be of any type, we will specify these types with a type variable, an arbitrary variable name that must unify with all occurrences of the same type variable in the type signature given. For instance, if a primop type signature was (a,b) -> a then the first argument and the return type must be the same type.

Collection Operations

Collection Primop Arity Return Type Description
aggregate(a, (a,b) -> a, 'collection a) 3 a Combines all the values in the collection with the provided initial value and accumulator function.
transform(a -> b, 'collection b) 3 'collection b Modifies each value in the collection with the provided helper function
filter(a -> bool, 'collection a) 2 'collection a Returns the original collection without the values that do not satisfy the provided predicate
element(a, 'collection a) 2 bool Returns true if the value specified is an element of the provided collection
isEmpty('collection a) 1 bool Returns true if the collection is empty, and false otherwise

Example:

global int totalMap;

global map<account, int> balances =
  { u'H1tbrEKWGpbPjSeG856kz2DjViCwMU3qTw3i1PqCLz65' : 1
  , u'fwBVDsVh8SYQy98CzYpNPcbyTRczVUZ96HszhNRB8Ve'  : 2
  , u'6pxGdGG6nQP3VoCW7HoGkCGDNCiCEWP3P5jHtrvgphBc' : 3
  };

transition initial -> terminal;

@initial
sumBalances() {
  totalMap = aggregate(0, sum, balances);
  transitionTo(:terminal);
}

sum (int x, int y) { x + y; }

In this example, the aggregate higher order function is used, and the helper function sum is passed as an argument along with the initial accumulated value of 0 such that the sum of all the values in the map is computed.

Maps

Maps are key/value stores that map each unique key to a corresponding value.

Map Operations

The subset of primops that operate exclusively over values of type map also have the types of some of their arguments determined by the type of the map that is passed as an argument to the primop.

Map Primop Arity Return Type Description
mapInsert(a, b, map<a, b>) 3 map<a, b> Insert a value into the map at the provided key
mapDelete(a, map<a, b>) 2 map<a, b> Delete the value from the map at the provided key
lookup(a, map<a, b>) 2 b Lookup a value at the provided key
modify(a, b -> b, map<a,b>) 3 map<a, b> Modify the value at the provided key with the *helper function

Example:

global asset a;
global map<account, int> shares = {};
global map<account, int> invested = {};
global int totalRaised;

transition initial -> raise;
transition raise   -> terminal;

@initial
setAsset(asset a') {
  if (sender() == deployer()) {
    a = a';
    transitionTo(:raise);
  };
}

@raise
addInvestor (account a, int amount)  {
  invested = mapInsert(a, amount, invested);
  if (amount >= 100) {
    shares = mapInsert(a, 100, shares);
  } else {
    shares = mapInsert(a, 10, shares);
  };
}

@raise
removeInvestor (account a) {
  shares = mapDelete(a, shares);
}

@raise
end () {
  if (sender() == deployer()) {
    totalRaised = aggregate(0, sum, invested);
    terminate("");
  };
}

sum(x,y) { x + y; }

Sets

Sets are unordered collections of unique values of the same type.

Set Operations

The subset of primops that operate exclusively over values of type set also have the types of some of their arguments determined by the type of the map that is passed as an argument to the primop.

Set Primop Arity Return Type Description
setInsert(a, set<a>) 2 set<a> Insert a value into the set
setDelete(a, set<a>) 2 set<a> Delete the value from the set
enum role
  { BigInvestor
  , MedInvestor
  , SmallInvestor
  };

global map<enum role, set<account>> investors =
  { `BigInvestor : ()
  , `MedInvestor : ()
  , `SmallInvestor : ()
  };

transition initial -> terminal;

@initial
insertInvestor(account a, enum role x) {
  currSet = lookup(x, investors);
  newSet =
    if (!element(a, currSet)) {
      setInsert(a, currSet);
    } else {
      setInsert(a, ());
    };
  investors = mapInsert(x, newSet, investors);
}

@initial
deleteInvestor(account a, enum role x) {
  currSet = lookup(x, investors);
  newSet =
    if (element(a, currSet)) {
      setDelete(a, currSet);
    } else {
      currSet;
    };
  investors = mapInsert(x, newSet, investors);
}

@initial
end() { terminate(""); }

Effects

Expressions and statements in FCL may induce a side-effect: we may have methods that read from the ledger state, methods that may modify it, or not have any effect at all.

To see what kind of effects we keep track of in FCL, consider the following contract:

transition initial -> otherState;
transition otherState -> terminal;

@initial
noEffects () {
  1 + 2;
  void;
}

@initial
reads () {
  sender();
  void;
}

@initial
writes () {
  transitionTo(:otherState);
}

@initial
readsAndWrites (assetDisc asst, account acct) {
  circulate(asst, 100);
}

@otherState
terminates () {
  terminate("Bye");
}

Running uplink scripts compile on this contract, we get back the following list of method signatures:

Signatures:
noEffects: () -> () {}
reads: () -> () {read}
writes: () -> () {write}
readsAndWrites: (assetDisc, account) -> () {read, write}
terminates: () -> any {write}

The first method, noEffects, contains some expressions, but it does not modify the ledger. The signature therefore shows an empty set of effects (denoted as {}).

We can use certain primitive operations (see Primitive operations) to read from the ledger state. In the method reads, we use sender(), which means that the method has effects {read}. An example of a primitive operation that writes to the ledger is transitionTo, as used in the method writes. Similarly, terminate also has as effect {write}, as can be seen in the method terminates. Operations that circulate or transfer assets modify the ledger state, hence have effect {write}.

The effects we consider in FCL, form the following so-called lattice:

The lattice tells us how the effects combine. For example, combining an expression with no effects with an expression with read effects yields an expression that has read effects. Combining an expression with read effects with one that has write effects, brings us to the bottom of the lattice: the resulting expression has a “read and write” effect.

One are where FCL checks the effects is in the definitions of global variables, see Global Variables for more information.

Helper Functions

Helper functions in FCL are method-like function definitions that are the last construct to occur in the FCL program structure, following method declarations. FCL Helper functions were introduced to simplify method definitions; they allow users to separate redundant code (lines of code repeated in several places throughout the contract source code) from lines of code in the method body more integral to overarching method semantics.

There are several properties of helper functions that limit their semantics due to semantic constraints on FCL given because of the restricted domain that which FCL programs are designed for:

  • Helper functions cannot refer to themselves in their function body due to the prohibition of reursion; If they could, self-recursion would be introduced.
  • Helper functions can only call helper functions in their function body that have been defined previously in the FCL program; If they could refer to helper functions defined later in the script, helper functions would be able to “mutually recurse”.
  • Helper functions are not annotated by contract states as methods are; Helper functions can be used in any method.

Example:

...

add100(int x) { x + 100; }
add200(int x) { add100(add100(x));

calcTotal(float rate, fixed2 gross) {
  grossFloat = fixed2ToFloat(gross);
  grossFloat + grossFloat * rate);
}

Another, equally important reason to add helper functions to FCL is the support of higher-order primitive operations, such that helper functions are higher-order function values. With regards to prim-ops, “higher-order” means that some primitive operations (built in functions) can take helper functions as arguments; Conversely, in the context of helper functions “higher-order” means that these functions can be passed as arguments to other functions (specifically prim-ops) as discussed in the collections section.

Deltas

A delta is a “side-effect” induced by calling a method of a contract, that alters some aspect of the ledger world state. When a block is validated, all deltas are computed for all transactions and applied to the end-result world-state of the block.

There different types of deltas are:

Delta Description
ModifyGlobal Modify a contract state variable
ModifyLocal Modify a local state variable
ModifyAsset Modify an asset
Terminate Terminate the contract

Global Variables

At the top of an FCL contract we can declare our global and local variables, which form part of the state of the contract. We can assign initial values to these variables upon declaration.

For example, if we want to keep track of how many times a certain method has been called by its deployer, we can define the following contract:

global int count = 0;

transition initial -> ready;
transition ready -> terminal;

@initial
repeat() {
  if (count > 4) {
    transitionTo(:ready);
  } else {
    count = count + 1;
    transitionTo(:initial);
  };
}

@ready
finish() {
  terminate("Bye");
}

We can put any FCL expression on the right-hand side of a definition of a global variable, as long as the expression does not have any side-effects. For example, we can have the following global variable definitions:

global datetime startDate = "2018-03-02T09:30:00+00:00";
global datetime endDate = startDate + 10d;

Definitions that have side-effects are not allowed, for example:

global assetDisc asst = '39oS7aToiKTazHDL7hu5ktbBUETRzVFybwGbhwj2DifC';
global void result = transferTo(asst, 100);

The above will give us the following error message:

Expected no effects
Actual effects: {write}
location: Line 2, Column 22

We can also leave the global variable uninitialized when declaring it. A scenario where this is useful is when we do not know the initial values at the time of deployment. We may want a special user (for example the deployer of the script) to initialize these variables at some point after deploying the contract. For example, we can write:

global int deployerInt;

transition initial -> set;
transition set -> set;
transition set -> terminal;

@initial {deployer()}
initialize(int val) {
  deployerInt = val;
  transitionTo(:set);
}

@set
update() {
  deployerInt = deployerInt + 1;
  transitionTo(:set);
}

@set
finish() {
  terminate("Bye");
}

However, we may only refer to a global once it has been assigned a value. The FCL compiler checks for this. If we were to replace the method initialize in the above script with the following, the compiler will raise an error message:

@initial {deployer()}
initialize(int val) {
  if (val > 5) {
    deployerInt = val;
  };
  transitionTo(:set);
}

The compiler will give us the following error message:

In method update:
Variable "deployerInt" undefined at:
  - line 17:3
Stack trace leading up to error:
  - @set:update to @set
  - @initial:initialize to @set

If we were to call initialize with a value small than 5, the contract state would transition to the state :set. This then would allow us to invoke the method update(), which refers to the global variable deployerInt, but this has not been initialized yet, hence we cannot evaluate the method. The compiler therefore checks that if a method refers to a global variable, each path leading up to the method’s source state assigns a value to this variable.

Data Model

There are two main forms of data storage that exists in Uplink. The main storages are denoted on-ledger and off-ledger, in which data is either contained within a contract’s state or stored locally to a party’s node, respectively. The third is temporary storage, containing data that exists only for the duration of the execution of a contract method call.

  • Off-ledger - Data stored off-ledger on counterparties’ local node (denoted by keyword “local” in top-level variable definitions in contracts).
  • On-ledger - Data stored on-ledger within a contract’s state (denoted by keyword “global” in top-level variable definitions in contracts).
  • On-ledger encrypted - Data stored on-ledger within a contract’s state and computed on using cryptographic protocols.

Global Stores An on-ledger storage that maps variables to their values. These values are stored unencrypted such that their values are visible to all nodes on the network running the contract.

Local Stores An off-ledger storage that maps variables to their values. The state of of these variables is stored on the counterparties local system and are kept in sync by exchanging hashes of their state after every contract interaction. The local state of a contract synchronizes all counterparties local stores without explicitly sharing their data. This is used for private data that is kept hidden from the rest of the network.

Datetimes

In Uplink contracts, Dates and times are represented by Datetime values, syntactically designated by ISO8601 formatted strings. These values can be compared to other datetime values and manipulated by adding and subtracting timedeltas, allowing users to write sophisticated datetime logic in contracts like you may expect in any useful financial DSL.

Syntax

"YYYY-MM-DDThh:mm:ssTZD"
  where
    YYYY = four-digit year
    MM   = two-digit month (01=January, etc.)
    DD   = two-digit day of month (01 through 31)
    hh   = two digits of hour (00 through 23) (am/pm NOT allowed)
    mm   = two digits of minute (00 through 59)
    ss   = two digits of second (00 through 59)
    s    = one or more digits representing a decimal fraction of a second
    TZD  = time zone designator (Z or +hh:mm or -hh:mm)

Example:

"1999-02-23T23:13:40+05:00"
"2015-10-10T00:00:00Z"

Operations:

Datetime == Datetime
Datetime < Datetime
Datetime <= Datetime
Datetime > Datetime
Datetime >= Datetime
Datetime + TimeDelta (*)
Datetime - TimeDelta (*)

[*] Behavior of TimeDelta Addition/Subtraction:

Because Uplink contracts support adding and subtracting timedeltas using the time units of years and months, there are some non-intuitive properties of these operations that emerge; Notably, adding and then subtracting the same timedelta (or vice versa) from a datetime value does not always result in the initial datetime. This arises in situations where the intermediate datetime month has fewer days than the initial datetime. In this case, adding 1 month does not add a consistent number of days to the initial datetime, but instead increments the month number by one. In the case that the resulting day of the month is not a valid day of the month, the day of the month is reduced to the last valid day of the resulting month. This behavior is the same for subtracting delta values from datetime values.:

"2017-08-31T00:00:00Z" + 1mo == "2017-09-30T00:00:00Z"
"2017-10-31T00:00:00Z" - 1mo == "2017-09-30T00:00:00Z"

In the case of leap years, adding or subtracting timedeltas behave the same as in the example above, but the only instance in which this happens is when the initial and resulting years are both a leap year or both not a leap year, and the initial datetime is Feb 29th of the leap year.:

"2016-02-29T00:00:00Z" + 1y    == "2017-02-28T00:00:00Z"
"2015-01-31T00:00:00Z" + 1y1mo == "2016-02-29T00:00:00Z"
"2017-03-31T00:00:00Z" - 1y1mo == "2016-02-29T00:00:00Z"

TimeDeltas

In Uplink contracts a timedelta represents an amount of time comprised of years, months, days, hours, minutes, and seconds. The values of each must be positive, and any number of each may be specified. Internal to Uplink, hours, minutes, and seconds are capped at 23, 59, and 59 respectively. that if a larger number of each is specified in the body of a timedelta literal, they will roll over into the next time quantity (e.g. 25h will be turned into 1d1h). Though the time units must be written in order, it is not necessary to include every time unit in the value representation of the timedelta. For instance, if adding only 1 month and 12 hours is desired, one can write 1mo12h instead of 0y1mo0d12h0m0s.

Syntax:

NyNmoNdNhNmNs
  where
    N = natural number

Examples:

1y6mo3d4h4m2s
(read as 1 year, 6 months, 3 days, 4 hours, 4 mins, and 2 seconds)

6mo12h
(read as 6 months and 12 hours)

1y100d59s
(read as 1 year, 100 days, and 59s)

Operations:

TimeDelta + TimeDelta
TimeDelta - TimeDelta (*)
Datetime + TimeDelta
Datetime - TimeDelta

[*] Note: If the result of one of the units of time in a timedelta is negative after subtraction, that particular unit of time is trimmed to 0. This results from the invariant that timedeltas cannot be negative, i.e. a change in time is intuitively positive, and this positive value can be added or subtracted from a datetime.:

1y6mo2d - 5mo3d == 1y1mo
3y20d50m - 21d35m == 3y15m

Enumeration Types

Apart from using any of the pre-defined types, we can define our own enumeration types in a contract. For example:

enum Color { Red , Green , Blue , Orange };

Defining a global variable of an enumeration type can be done as follows:

global enum Color = `Red;

Note that when referring to an element of an enumeration type outside the type its definition is preceded by a backtick (`).

We can perform case analysis on a value of an enumeration type:

complement = case(color) {
  `Red    -> `Green;
  `Green  -> `Red;
  `Blue   -> `Orange;
  `Orange -> `Blue;
};

Such a case analysis must be exhaustive: there must be a case for every value of the enumeration type.

Addressing

A contract its address is derived from the transaction that deployed the contract, as described in the documentation about Transactions. The addresses are represented as base58 encoded byte strings within Uplink.

\[\text{address}_{\small \text{CONTRACT}} \ = \ \texttt{base58}(\texttt{sha3}(\texttt{base16}(\texttt{sha3}(\text{transaction}))))\]

Access Restriction

Some operations on a workflow should be reserved to a privileged group of accounts. This includes

  • method calls
  • write access to variables

In FCL this can be introduced by including access restrictions. An access restriction consists of a set of expressions of type account and must not have any write effects. This includes account literals like u'CssRnWaxBhRRwhVL7ESgXkP7cgZ9vjFzV5nmr4jj3ZAh', variables of type account and the primops deployer(), sender() and validator().

The syntax for an access restriction is

\[\texttt{\{} u_1 \texttt{,} \ u_2 \ ...\texttt{,} \ u_n \texttt{\}}\]

where u ranges over expressions.

Note: It is not valid to syntactically repeat an expression, so {deployer(), admin, deployer()} will cause the script to be rejected with a duplication error. This is purely syntactic; it is fine if admin and deployer() happen to be one and the same account.

An example of a workflow that uses access restrictions:

global account {admin} admin = u'CssRnWaxBhRRwhVL7ESgXkP7cgZ9vjFzV5nmr4jj3ZAh';

transition initial -> intermediate;
transition intermediate -> terminal;

@initial {admin, deployer()}
init() {
  transitionTo(:foo);
}

@intermediate
setAdmin(account a) {
  admin = a;
}

@intermediate
finish() {
  terminate("Goodbye.");
}

In the above example, either admin or the deployer may call init(). A call to this method by any other account will result in a failed transaction (meaning that the ledger state does not get altered).

A simple notion of roles can be achieved by assigning accounts to variables: In the intermediate state, admin may set a new value for admin. This is because of the the {admin} access restriction on the global variable at the top of the script. After a successful transaction, only the new admin may change the admin role.

Example: Minimal

global int x = 0 ;

transition initial -> set;
transition set -> terminal;

@set
end () {
  terminate("End contract.");
}

@initial
setX () {
  x = 42;
  transitionTo(:set);
}

Example: Loan Product

global account issuer = 'CssRnWaxBhRRwhVL7ESgXkP7cgZ9vjFzV5nmr4jj3ZAh';
global account borrower = 'vVVq8PMEa9qGLL7LEmHUZKM78ze5JUgDPnjSE6FES63';
global assetFrac2 asset_ = 'HjdMu5LtF7BW6pGzvZGsV7ABUA4dkZFB2TmXQiymNpue';
global fixed2 interest= 0.05f;
global fixed2 principal;
global fixed2 payout;

global datetime maturityDate = "2018-03-08T10:12:00+00:00";

transition initial -> amountConfirmed;
transition amountConfirmed -> amountTransferred;
transition amountTransferred -> terminal;

@initial {issuer}
propose(fixed2 ask) {
  principal  = ask;
  transitionTo(:amountConfirmed);
}


@amountConfirmed
loan() {issuer} {
  transferHoldings(issuer, asset_, principal, borrower);
  transitionTo(:amountTransferred);
}


@amountTransferred
settle() {issuer} {
  after(maturityDate) {
    if((isBusinessDayUK(now()))){
      payout = (principal + (principal * interest));
      transferHoldings(issuer, asset_, payout, borrower);
      transitionTo(:terminal);
    };
  };
}

Example: Notary

global sig signature;

transition initial -> set;
transition set -> terminal;

@initial { deployer() }
put(msg x) {
  signature = sign(x);
  transitionTo(:set);
}

@set
end() {
  terminate("This is the end.");
}

Simulating Contract Execution

Within the Uplink ledger it may be beneficial (perhaps recommended) to simulate an FCL contract before deploying the contract to the production ledger– Uplink is targeted to facilitate the modeling of structured products and derivatives such that secure multi-party workflows can be realized on-chain, and these are often hard to get right the first time– Even comprehensive static analysis does not catch all logical bugs within the contracts. Contract simulation entails deploying a contract in a closed environment, distinctly separate from the simulation node’s copy of the ledger. The simulation uses a snapshot of either a previously saved ledger state or the current ledger state of the node on which the simulation is being created and run.

Since multiple contracts can be simulated at the same time, each simulated contract is assigned a simulation-id, calculated during the simulation creation process, and returned to the simulation creator (the “simulator”) such that the simulator can denote which simulated contract they wish to interact with. In future versions of uplink, simulated contracts will be able to reference each other such that simulations of groups of contracts that represent secure, complex, multi-party workflows may be tested off-chain before deploying to the ledger.

Contract simulation provides the following interface through which one can issue updates to the variables within the closed environment of a particular contract simulation, or query the current state of the contract of its encapsulating environment. All updates and queries are issued with a specific contract simulation identifier specifying the contract simulation to which the update/query should be issued.

Simulation Creation

Create Simulation: Creates a simulation, returning a simulation identifier such that the creator of the simulation can reference when issuing updates or queries to a specific simulated contract.

Simulation Updates

Set Timestamp: Sets the current timestamp of the evaluation context; Users can set the time at which the contract is being evaluated to any time after the Unix epoch. If the contract logic has a strong temporal foundation (i.e. its logical progression relies heavily on the current time of method evaluation), then setting the current time of the contract is necessary for a useful simulation to be conducted. The timestamp should be in the form of an ISO_8601 string.

Add Timedelta: Adds the supplied timedelta to the simulation’s current timestamp. This is useful for incrementing the timestamp by a set duration rather than having to deal with explicitly ISO_8601 timestamps.

Call Method: Naturally, contract methods can be called on a simulated contract. Simulated contract method calls are conducted by supplying the name of the method and the arguments to be supplied to the method call. This action updates the encapsulated world state associated with the simulated contract.

Simulation Query

Query Methods: Query the callable methods of the simulated contract. “Callable” methods refers to the notion that the methods that are available to be called on a contract is determined by the state the contract is in.

Query Contract: Query the contract that is being simulated.

Query Assets: Query the assets in the contract simulation environment.

Query Asset: Query a specific asset in the contract simulation environment.

It is important to note that contract simulations are held entirely in memory, and not persisted to disk; i.e. contract simulation data will be lost if the node that is performing the simulation stops running. This is contrary to uplink ledger data, which is persisted to disk, as a node is able to be stopped and started as determined by the user running the node, without fear that ledger data will be lost or corrupted. In future versions of uplink, contract simulations will be persisted to disk and a more complete interface will be supplied.


Contract REPL

Built into the Uplink executable is the FCL REPL, a command line tool that allows Uplink users to simulate contracts utilizing most of the update and query interface outlined above, along with a few other bits of functionality. In the instance of using the REPL, a simulated method call returns the list of Deltas emitted during the evaluation of the function body. Currently, the REPL is not fully integrated with the contract simulation feature of uplink, so only one contract is able to be simulated at a time.

Starting the REPL with the simple contract loaded:

uplink repl examples/simple.s

An interactive session using the simple.s contract:

>>> :contract
State: "initial"
Global Storage:
        x : int = 0

>>> :methods
Methods:
setX : int -> ()

>>> setX(42)
UpdateSimulationSuccess
>>> :contract
State: "set"
Global Storage:
        x : int = 84
>>> end()
UpdateSimulationSuccess
>>> :contract
State: "terminal"
Global Storage:
        x : int = 84

We can also have the REPL show more information when evaluation method calls, using the verbose mode:

uplink repl -v examples/simple.s

Invoking the same commands as above now yields the following interaction:

>>> setX(10)
UpdateSimulationSuccess
State: "set"
Global Storage:
        x : int = 52

>>> end()
UpdateSimulationSuccess
State: "terminal"
Global Storage:
        x : int = 52

>>>

Of interest are the Methods and GlobalStorage contents fields. Signatures tells us which methods are callable in the current state and what arguments they take. GlobalStorage contents depicts a mapping of global variables to their current values stored on the ledger.

Providing a Ledger State

A JSON dump of the ledger state obtained with:

uplink data export ledger -f JSON ledger_dump.json

can be passed as the Ledger state to simulate the contract in as an optional parameter to the repl command:

uplink repl examples/simple.s ledger_dump.json