_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 read 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. 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.

Reasoning

_images/reasoning.png
  • State Constraints -
  • Temporal Constraints -
  • Information Constraints -

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 order to allow calls to the annotated method. That means, if there wasn’t a call to 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 AML, our proprietary Asset Modeling Language, where a contract’s life cycle has to start in the method annotated with @initial and end using the primop 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. The are comprised of a 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.

Methods cannot call themselves nor other contract defined methods within the body, as this would allow for recursion and would severely limit the ways in which the contract code can be statically analyzed. Methods are publically available functions that define the means in which counterparties can interact with smart contracts; they provide the means in which smart contracts make state transitions referenced in the State Diagram section.

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.

Type Description
int Type of 64 bit integers
float Type of double precision floats
fixed1 Type of fixed-point number with precision 1
fixed2 Type of fixed-point number with precision 2
fixed3 Type of fixed-point number with precision 3
fixed4 Type of fixed-point number with precision 4
fixed5 Type of fixed-point number with precision 5
fixed6 Type of fixed-point number with precision 6
bool Type of booleans
account Type of account addresses
assetDisc Type of discrete asset addresses
assetBin Type of binary asset addresses
assetFrac1 Type of fractional asset addresses with precision 1
assetFrac2 Type of fractional asset addresses with precision 2
assetFrac3 Type of fractional asset addresses with precision 3
assetFrac4 Type of fractional asset addresses with precision 4
assetFrac5 Type of fractional asset addresses with precision 5
assetFrac6 Type of fractional asset addresses with precision 6
contract Type of contract addresses
msg Type of messages
sig Type of ECDSA signature
void Type of void
datetime Type of date and time values
timedelta Type of periods of time
state Type of graph state labels
any Type of any type

Built-in Functions

Primitive operations 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 would not be able to be written using other language primitives.

Some of these prim-ops are “asset agnostic”; This means that they can take any type of asset address (Binary, Discrete, or Fractional) as an argument.

Function Arguments Return Type Description
verify(addr, msg, sig) 3 bool Verify a signature
sign(msg) 1 sig Sign a message
block() 0 int Active block
deployer() 0 account Return the deployer (or owner) of a contract
sender() 0 account Return the creator of current transaction
created() 0 int 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 date 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 Hash of current transaction
contractValue(addr, varName) 2 <inferred> Query a value in the contract’s storage, return type inferred
contractValueExists(contract, varName) 2 bool Query a value’s existence in a contract’s global storage
contractState(contract) 1 state Query 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 Predicate checking if datetime is a business day or not
nextBusinessDayUK(datetime) 1 datetime Returns the next business day after the supplied datetime
isBusinessDayNYSE(datetime) 1 bool Predicate checking if datetime is a business day or not
nextBusinessDayNYSE(datetime) 1 datetime Returns 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 Prim Ops

A subset of the asset agnostic prim ops, or “asset-prim-ops”, 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-prim op with the asset argument of type assetDisc, the type of the 3rd 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, assetDisc a, int amount, account to)
  if (sender() == from) {
    transferHoldings(from, a, amount  to);
    terminate("transfer complete");
  };
}

We denote prim op asset arguments of varying asset types as <assetType>, and the argument types or return types that depend on the asset type supplied as <assetHoldingsType>.

Asset Function (AssetPrimOp) Arguments Return Type Description
assetExists(<assetType>) 1 bool Check if an asset with the given address exists in world state
holderBalance(<assetType>, address) 2 <assetHoldingsType> Query the balance of a holder of a of any type of asset
transferTo(<assetType>, <assetHoldingsType>) 2 void Transfer n asset holdings to account
transferFrom(<assetType>, <assetHoldingsType>, account) 3 void Transfer n asset holdings from contract address to account
circulateSupply(<assetType>, <assetHoldingsType>) 2 void Circulate n asset supply from the asset supply to the asset issuer
transferHoldings(account, <assetType>, <assetHoldingsType>, account) 4 void Transfer n asset holdings from an account to another account
Asset Type FCL Type (<assetType>) Asset Holdings Type (<assetHoldingsType>)
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

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) {
  if (sender() == 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 Built-in Functions) 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.

Deltas

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

There are five different types of deltas

Delta Description
ModifyGlobal Modify a contract state variable
ModifyLocal Modify a local state variable
ModifyAsset Modify an asset
Atomic Combine two operations as atomic
Terminate Terminate the contract

Deltas are created for updating global variables, local variables, and assets.

  • ModifyGlobal change the global contract state visible to all participants of the network.
  • ModifyLocal describes operations on local storage kept off the network which resides off-chain only on the involved parties’ nodes.
  • ModifyAsset deltas describe asset changes in the ledger world state.
  • Terminate halts contract execution

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;
  };
}

@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
initialize(int val) {
  if (sender() == deployer()) {
    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
initialize(int val) {
  if (sender() == deployer()) {
    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 without being the deployer of the contract, 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 smart 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 and conditional branching logic in smart 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 Additon/Subtraction:

Because Uplink smart contracts support adding and subtracting time deltas 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 time delta (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 time deltas 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 smart 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, mins, and seconds are capped at 23, 59, and 59 respectively. te that if a larger number of each is specfied in the body of a timedelta literal, they wil 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 time delta. 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 complete: there must be a case for every value of the enumeration type.

Addressing

Contract addresses are derived from the unique transaction that was issued to create 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}} \ = \ \mathit{base58}(\ \mathit{sha3}_{256}(\ \mathit{base16}(\ \mathit{sha3}_{256}(\ \text{transaction}\ )\ )\ )\ )\]

Example: Minimal

global int x = 0 ;

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

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

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

Example: Lonad 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
propose(fixed2 ask) {
   if((sender() == issuer)) {
    principal  = ask;
    transitionTo(:amountConfirmed);
  };

}


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


@amountTransferred
settle(){
  after(maturityDate) {
    if((sender() == issuer)){
      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
put(msg x) {
  if (sender() == deployer()) {
    signature = sign(x);
    transitionTo(:set);
  };
}

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

Simulating Contracts

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 reperesent 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 it’s encapsulating environment. All updates and queries are issued with a specific contract simulation id 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. it’s 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 time delta to the simulation’s current timestamp. This is useful for incrementing the timestamp by a set duration rather than having to deal with explicity 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.


FCL Contract REPL

Built in to 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 intergrated 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 contracts/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 contracts/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 contracts/simple.s ledger_dump.json