Welcome
This blog discusses various topics, mostly about tezos smart contracts, and software safety (verification). You can find a list of the latest posts here.
Articles (should) be sorted by the topics they discuss:
Latest
- 2019
- April
- Using Techelson With Liquidity updated (
must_fail <msg> <op>
)
- Using Techelson With Liquidity updated (
- February
- April
About Me
Hi. I'm Adrien Champion. I have a PhD in software safety and worked on critical embedded systems (systems controlling planes, nuclear plants, cars, etc.) during both my PhD and my first postdoc at the University of Iowa with Cesare Tinelli, where I worked on the Kind 2 model checker. I did my second postdoc at the University of Tokyo with Naoki Kobayashi. I worked on higher-order model-checking: I developed the hoice Horn Clause solver which proved quite efficient for problems stemming from higher-order program verification.
I now work at OCamlPro. Currently, most of my work there targets tezos smart contracts, Michelson which is tezos' language for smart contracts, and Liquidity, an OCaml-like (relatively) high-level language which (de)compiles to/from michelson.
I am interested in software safety and strongly-typed high-level languages such as Rust and OCaml. I still occasionally work on (critical) embedded system verification.
Michelson/Liquidity
Using Techelson With Liquidity
Techelson is a Test Execution Engine for Michelson smart contracts developed by myself at OCamlPro. With the (upcoming) release of Techelson, here is a relatively simple (although a bit dirty) setup to combine it with Liquidity locally, command-line style. Very little knowledge about Techelson is required. This post only assumes some familiarity with Liquidity.
NB: if you want to learn more about Techelson, make sure to read its user documentation.
Liquidity has a nice feature allowing to declare extensions to the language. Turns out they are powerful enough to generate Techelson's special instructions: you can write your tests directly in Liquidity, and tiny script run the tests for you.
First, make sure you have Liquidity and Techelson in your path. I'll assume they are called
liquidity
and techelson
. Let's structure things a bit and have the working directory look like
this:
.
├── test.sh
├── contracts
│ └── "contracts go here"
└── tests
└── "tests go here"
The first step is to teach Liquidity to generate Techelson special instruction and setup a tiny test script in Liquidity Extensions. Then test our setup on a Basic Example. After I will show how to write a test for an external, non-trivial contract in External Contract Example, using more advanced Techelson commands. Finally, there's The End at the end.
There is a listing of all the files used in this post in the File Listing section. Also, they are available here.
Liquidity Extensions
The first step is to declare the Liquidity extensions we need. Let's create a tests/techel.liq to write our extensions into:
external get_balance :
[%stack: address] -> [%stack: tez]
= "GET_BALANCE"
external get_storage :
[%type: 'a] -> [%stack: address] -> [%stack: 'a option]
= "GET_STORAGE"
external apply_operations :
[%stack: operation list] -> unit
= "APPLY_OPERATIONS"
external start_set_source :
[%stack: address] -> unit
= "SET_SOURCE { #"
external end_set_source :
unit -> unit
= "}"
external must_fail :
[%stack: string option] -> [%stack: operation] -> [%stack: operation]
= "MUST_FAIL string"
It is not crucial to understand these rules precisely, only the power they give us. And that power is the following functions. What they do precisely will become clear when we use them later on.
get_balance address
: takes an address as parameter, and returns the balance of the contract at that addressget_storage [%type: 'g] address
: takes a type and an address as parameter, and returnsSome
of the storage of the contract at that address, if it has type'g
None
otherwise
apply_operations ops
: takes a list of operations and applies them right awaystart_set_source address
andend_set_source ()
: a very, very dirty hack to define a scope where all operations created appear to have been created by whatever is ataddress
must_fail msg_opt op
: tells techelson that the operationop
must fail; this succeeds iffop
fails andmsg_opt
isNone
, ormsg_opt
isSome msg
andop
failed precisely with stringmsg
.
Thanks to Liquidity's file import mechanism, we can use these functions in any Liquidity file
test.liq
with Techel.get_balance
, Techel.get_storage
etc. by simply calling Liquidity as
follows:
$ liquidity tests/techel.liq test.liq
NB: you only need to write
tests/techel.liq
once, and make sure you pass it to Liquidity when compiling your testcases, as intest.sh
below.
So let's fill in the test.sh
file so that it runs our tests. It takes the path to our (future)
Liquidity test file(s) as argument, and compiles it/them along with all the contracts in
contracts/
and the extensions we just defined. So, given some file tests/test.liq
, the script
- generates
tests/test.liq.techel
: contains the testcase and the contract(s) to test, and - runs techelson on
tests/test.liq.techel
.
#! /bin/bash
set -e
test_file="$1"
this_script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
techel_lib="$this_script_dir/tests/techel.liq"
# List of all the contracts.
contracts=""
for file in `find "$this_script_dir/contracts" -iname "*.liq"` ; do
contracts="$contracts $file"
done
# File liquidity will compile to.
target="$test_file.techel"
echo "Compiling $test_file..."
echo
liquidity --no-annot --no-simplify --no-peephole $techel_lib $contracts -o $target $test_file
echo
# Running techelson on the target.
echo "Running test $target"
echo
techelson $target
Warning: function
start_set_source
uses a dirty hack so that it works as a Liquidity extension (similar to a SQL injection). This is a temporary solution, eventually this hack will not be necessary.
Basic Example
Our test is going to be a contract with a unit
(empty) storage. Its (only) entry point will take
a parameter of type unit
and inside that entry point will be our test. Entry points must return a
list of operation and the new value of the storage. It is not relevant for the test: it will just
run and perform tests. Let's have a nothing
helper which is the pair of the empty list of
operations and unit
.
So the simplest test tests/empty.liq we can write is:
type storage = unit
let nothing : operation list * unit = [], ()
let%entry test (_param : unit) (_storage : unit) =
nothing
Which we can compile, and run with the test.sh
script from the previous section.
$ ./../test.sh ../tests/empty.liq
Compiling ../tests/empty.liq...
Module Techel
Contract Multi
Main contract Empty
File "../tests/empty.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/empty.liq.techel
Running test ../tests/empty.liq.techel
Running test `Empty`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Empty`
That was boring. Let's write something slightly more interesting. This new test tests/basic.liq will
- create an account with
13
tez, - check that its balance after deployment is
13
tez, - make a transfer of
29
tez, - check that the new balance is
42
tez.
type storage = unit
let nothing : operation list * unit = [], ()
let%entry test (_param : unit) (_storage : unit) =
let delegate : key_hash option = None in
let operation, address =
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount:13tz
in
(* Apply the operation so that we can interact with the account. *)
Techel.apply_operations [operation];
(* Contract is now live. *)
let balance = Techel.get_balance address in
if balance <> 13tz then (
failwith "balance should be 13tz"
);
let account_contract =
match UnitContract.at address with
| None -> failwith "could not retrieve account"
| Some c -> c
in
let operation = Contract.call ~dest:account_contract ~amount:29tz ~parameter:() in
Techel.apply_operations [operation];
let balance = Techel.get_balance address in
if balance <> 42tz then (
failwith "balance should be 42tz"
);
nothing
and run it
$ ./../test.sh ../tests/basic.liq
Compiling ../tests/basic.liq...
Module Techel
Contract Multi
Main contract Basic
File "../tests/basic.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/basic.liq.techel
Running test ../tests/basic.liq.techel
Running test `Basic`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 13000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (13000000utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[0]@Basic -> address[1] 29000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (13000000utz) address[1]
running TRANSFER[uid:1] address[0]@Basic -> address[1] 29000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (42000000utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Basic`
Let's make sure we are actually testing something. Let's change the final check of tests/basic.liq to
let balance = Techel.get_balance address in
if balance <> 12tz then (
failwith "balance should be 12tz"
);
in tests/basic_err.liq. The test fails indeed:
$ ./../test.sh ../tests/basic_err.liq
Compiling ../tests/basic_err.liq...
Module Techel
Contract Multi
Main contract Basic_err
File "../tests/basic_err.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/basic_err.liq.techel
Running test ../tests/basic_err.liq.techel
Running test `Basic_err`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 13000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (13000000utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:1] address[0]@Basic_err -> address[1] 29000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (13000000utz) address[1]
running TRANSFER[uid:1] address[0]@Basic_err -> address[1] 29000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (42000000utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Test `Basic_err` failed:
Tezos protocol error
Failure on value "balance should be 12tz" : string
Error
1 of the 1 testcase failed
External Contract Example
Let's give ourselves an external contract that we can write a test for. We will use contracts/multi.liq. It's whole code is available in the listing, but for writing a test we only need a high-level understanding of what it does.
Multi
, in contracts/multi.liq, offers its clients to store tokens for them. It has some
administrators, which are the only one able to add new clients. First, the contract's storage is
type storage = {
admins : (string, address) map ;
users : (string, (address * tez * UnitContract.instance)) map ;
}
Multi
stores some named administrators in a map admins
from names to administrator addresses.
It also stores some named clients in a map clients
. It maps the name of a client to
- its address
- the amount of money it has stored on this contract
- an account which will receive all the money the client has if the client asks to drains it.
Entry Points
The entry points of Multi
we are interested in are
let%entry add_client (
(admin_name, user_name, user, c) :
string * string * address * UnitContract.instance
) (storage : storage) =
...
Adds a new user. Only administrators can do this.
let%entry drain (name : string) (storage : storage) =
...
Transfers all the money of a user to the account provided on creation. Only the client can call this entry point (with the right name).
Testing Multi (and failing to do so)
Let's first write a test which creates an instance of Multi
with an admin named root
. It then adds a new administrator:
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.apply_operations [ add_client ];
nothing
Let's run this test. It will fail though, as it should. So we will call it tests/test1_err.liq. The relevant part of the output is
$ ./../test.sh ../tests/test1_err.liq
[....]
Test `Test1_err` failed:
Error
operation TRANSFER[uid:3] address[0]@Test1_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))) was expected to succeed
but failed on operation TRANSFER[uid:3] address[0]@Test1_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
operation failed on "illegal access to admin account" : string
The reason this test failed is because only administrator can add clients. So here, only root
can
add a new client. Hence, we need to pretend to be root
. More precisely, we need to pretend to be
root when we create the operation.
But this test is not worthless. It shows that an outsider cannot add new clients even by using the
name of an existing outsider. What we should do is say that this transfer must fail. This is what
tests/test1.liq does. Only the apply_operations
changes:
let must_fail = Techel.must_fail None bad_add_client in
Techel.apply_operations [ must_fail ];
Extension must_fail None op
produces an operation that succeeds iff op
fails. Techelson notifies you that the failure was confirmed in its output:
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
We can do better than this: we can ask Techelson to verify the error message is the one we expect.
This is exactly what the first argument of must_fail
does. must_fail (Some msg) op
succeeds iff
op
fails with exactly the string msg
.
Test tests/test1_better.liq only changes in the error message:
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
Techel.apply_operations [ must_fail ];
Techelson confirms the failure and its error message (see the output):
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test1_better -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1_better -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
Testing Multi
Let's now make the transfer work by pretending to be root
. The current solution for this is not
very satisfactory, but it will do the job until techelson is more tightly integrated in Liquidity.
The result is tests/test2.liq, where we add:
Techel.start_set_source root ;
(* all operations created in here will appear to have been created by `root` *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.end_set_source () ;
The test is now successful:
$ ./../test.sh ../tests/test1.liq
Compiling ../tests/test1.liq...
Module Techel
Contract Multi
Main contract Test1
File "../tests/test1.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/test1.liq.techel
Running test ../tests/test1.liq.techel
Running test `Test1`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test1 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test1`
Finally, let's add more tests. We will have the client ("lucy"
) deposit 10tz
on her account,
which should leave her with 5tz
(remember she was created with 15tz
). She will then drain her
account, which will trigger Multi to send her all of her money, at which point she should have
15tz
.
The additional code, in tests/test3.liq, is
Techel.start_set_source client ;
let deposit_money =
Contract.call ~dest:main_instance ~amount:10tz ~entry:deposit ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ deposit_money ];
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 5tz then (
failwith "lucy should have 5tz now"
);
(* lucy walks out of the whole thing *)
Techel.start_set_source client ;
let drain =
Contract.call ~dest:main_instance ~amount:0tz ~entry:drain ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ drain ] ;
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 15tz then (
failwith "lucy should have 15tz now"
);
which is successful:
$ ./../test.sh ./../tests/test3.liq
Compiling ./../tests/test3.liq...
Module Techel
Contract Multi
Main contract Test3
File "./../tests/test3.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ./../tests/test3.liq.techel
Running test ./../tests/test3.liq.techel
Running test `Test3`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] "illegal access to admin account" :
string (TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test3 -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Test3`
Last, let's again make sure we're actually testing something by checking that, at the end, lucy's
balance is 2tz
(it's not) in tests/test3_err.liq:
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 2tz then (
failwith "lucy should have 2tz now"
);
This fails:
$ ./../test.sh ../tests/test3_err.liq
Compiling ../tests/test3_err.liq...
Module Techel
Contract Multi
Main contract Test3_err
File "../tests/test3_err.liq.techel" generated
If tezos is compiled, you may want to typecheck with:
tezos-client typecheck script ../tests/test3_err.liq.techel
Running test ../tests/test3_err.liq.techel
Running test `Test3_err`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:0] (@address[1], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3], "tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc", None, true, true, 15000000utz)
{
storage unit ;
parameter unit ;
code ...;
}
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (0utz) address[1]
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] "illegal access to admin account" :
string (TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3])))))))
while running operation TRANSFER[uid:3] address[0]@Test3_err -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
failed with value "illegal access to admin account" : string
running TRANSFER[uid:5] address[1] -> address[2] 0utz (Right (Right (Left ("root", ("lucy", (address[3], address[3]))))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:6] address[3] -> address[2] 10000000utz (Right (Right (Right (Left "lucy"))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:7] address[3] -> address[2] 0utz (Right (Right (Right (Right (Right "lucy")))))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
applying operation TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (10000000utz) address[2]
<anonymous> (5000000utz) address[3]
<anonymous> (0utz) address[1]
running TRANSFER[uid:8] address[2] -> address[3] 10000000utz Unit
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]
<anonymous> (15000000utz) address[3]
<anonymous> (0utz) address[1]
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Test `Test3_err` failed:
Tezos protocol error
Failure on value "lucy should have 2tz now" : string
Error
1 of the 1 testcase failed
The End
That's it. I believe this is enough to get some Liquidity enthusiasts interested in techelson and its future integration in the Liquidity ecosystem. The process will be much more streamlined soon and will most likely remove the need for defining Liquidity extensions.
The next section is a listing of all the scripts, contracts and test files used in this post.
File Listing
Including all files mentioned in this post, the layout should look like this
test.sh
#! /bin/bash
set -e
test_file="$1"
this_script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
techel_lib="$this_script_dir/tests/techel.liq"
# List of all the contracts.
contracts=""
for file in `find "$this_script_dir/contracts" -iname "*.liq"` ; do
contracts="$contracts $file"
done
# File liquidity will compile to.
target="$test_file.techel"
echo "Compiling $test_file..."
echo
liquidity --no-annot --no-simplify --no-peephole $techel_lib $contracts -o $target $test_file
echo
# Running techelson on the target.
echo "Running test $target"
echo
techelson $target
tests/
tests/basic.liq
type storage = unit
let nothing : operation list * unit = [], ()
let%entry test (_param : unit) (_storage : unit) =
let delegate : key_hash option = None in
let operation, address =
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount:13tz
in
(* Apply the operation so that we can interact with the account. *)
Techel.apply_operations [operation];
(* Contract is now live. *)
let balance = Techel.get_balance address in
if balance <> 13tz then (
failwith "balance should be 13tz"
);
let account_contract =
match UnitContract.at address with
| None -> failwith "could not retrieve account"
| Some c -> c
in
let operation = Contract.call ~dest:account_contract ~amount:29tz ~parameter:() in
Techel.apply_operations [operation];
let balance = Techel.get_balance address in
if balance <> 42tz then (
failwith "balance should be 42tz"
);
nothing
tests/basic_err.liq
type storage = unit
let nothing : operation list * unit = [], ()
let%entry test (_param : unit) (_storage : unit) =
let delegate : key_hash option = None in
let operation, address =
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount:13tz
in
(* Apply the operation so that we can interact with the account. *)
Techel.apply_operations [operation];
(* Contract is now live. *)
let balance = Techel.get_balance address in
if balance <> 13tz then (
failwith "balance should be 13tz"
);
let account_contract =
match UnitContract.at address with
| None -> failwith "could not retrieve account"
| Some c -> c
in
let operation = Contract.call ~dest:account_contract ~amount:29tz ~parameter:() in
Techel.apply_operations [operation];
let balance = Techel.get_balance address in
if balance <> 12tz then (
failwith "balance should be 12tz"
);
nothing
tests/empty.liq
type storage = unit
let nothing : operation list * unit = [], ()
let%entry test (_param : unit) (_storage : unit) =
nothing
tests/techel.liq
external get_balance :
[%stack: address] -> [%stack: tez]
= "GET_BALANCE"
external get_storage :
[%type: 'a] -> [%stack: address] -> [%stack: 'a option]
= "GET_STORAGE"
external apply_operations :
[%stack: operation list] -> unit
= "APPLY_OPERATIONS"
external start_set_source :
[%stack: address] -> unit
= "SET_SOURCE { #"
external end_set_source :
unit -> unit
= "}"
external must_fail :
[%stack: string option] -> [%stack: operation] -> [%stack: operation]
= "MUST_FAIL string"
external print_stack :
unit -> unit
= "PRINT_STACK"
external step :
unit -> unit
= "STEP"
tests/test1.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client *)
let bad_add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
let must_fail = Techel.must_fail None bad_add_client in
Techel.apply_operations [ must_fail ];
nothing
tests/test1_better.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client *)
let bad_add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
Techel.apply_operations [ must_fail ];
nothing
tests/test1_err.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.apply_operations [ add_client ];
nothing
tests/test2.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client and fail *)
let bad_add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
(* let's really add a client now *)
Techel.start_set_source root ;
(* all operations created in here will appear to have been created by `root` *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.end_set_source () ;
Techel.apply_operations [ must_fail ; add_client ];
nothing
tests/test3.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client and fail *)
let bad_add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
(* let's really add a client now *)
Techel.start_set_source root ;
(* all operations created in here will appear to have been created by `root` *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.end_set_source () ;
Techel.apply_operations [ must_fail ; add_client ];
(* lucy deposits `10tz` *)
Techel.start_set_source client ;
let deposit_money =
Contract.call ~dest:main_instance ~amount:10tz ~entry:deposit ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ deposit_money ];
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 5tz then (
failwith "lucy should have 5tz now"
);
(* lucy walks out of the whole thing *)
Techel.start_set_source client ;
let drain =
Contract.call ~dest:main_instance ~amount:0tz ~entry:drain ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ drain ] ;
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 15tz then (
failwith "lucy should have 15tz now"
);
nothing
tests/test3_err.liq
let nothing : operation list * unit = [], ()
(* Creates a storage for Multi with one administrator. *)
let one_admin (root : string) (address : address) : Multi.storage = {
Multi.admins =
Map.add root address (Map : (string, address) map) ;
Multi.users =
(Map : (string, (address * tez * UnitContract.instance)) map) ;
}
(* Deploys an account with an arbitrary manager. *)
let deploy_account_op (amount: tez) : operation * address =
let delegate : key_hash option = None in
Account.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~amount
(* Deploys an instance of multi with an arbitrary manager. *)
let deploy_contract_op (storage : Multi.storage) : operation * address =
let delegate : key_hash option = None in
Contract.create
~manager:tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
~delegate
~delegatable:true
~spendable:false
~amount:0tz
~storage
~code:(contract Multi)
(* Storage of the test is irrelevant. *)
type storage = unit
(* Actual test. *)
let%entry test (_param : unit) (_storage : unit) =
let root_op, root = deploy_account_op 0tz in
let storage = one_admin "root" root in
let main_op, main = deploy_contract_op storage in
(* ask techelson to apply these operations. *)
Techel.apply_operations [ root_op ; main_op ];
(* root and main are live now *)
(* let's check root is an admin, and that the address is correct *)
let storage =
match Techel.get_storage [%type: Multi.storage] main with
| Some storage -> storage
| None -> failwith "can't retrieve contract's storage"
in
(
match Map.find "root" storage.Multi.admins with
| None -> failwith "no root in storage"
| Some address -> (
if address <> root then (
failwith "wrong address for root"
)
)
);
let client_op, client = deploy_account_op 15tz in
(* deploy the client *)
Techel.apply_operations [ client_op ];
(* client is live now *)
(* retrieve client instance for registration *)
let client_instance =
match (Contract.at client : UnitContract.instance option) with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* retrieve multi's instance to call it *)
let main_instance =
match Multi.at main with
| None -> failwith "could not retrieve main contract"
| Some instance -> instance
in
(* let's add a client and fail *)
let bad_add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
let error_message = Some "illegal access to admin account" in
let must_fail = Techel.must_fail error_message bad_add_client in
(* let's really add a client now *)
Techel.start_set_source root ;
(* all operations created in here will appear to have been created by `root` *)
let add_client =
Contract.call ~dest:main_instance ~amount:0tz ~entry:add_client ~parameter:(
"root", "lucy", client, client_instance
)
in
Techel.end_set_source () ;
Techel.apply_operations [ must_fail ; add_client ];
(* lucy deposits `10tz` *)
Techel.start_set_source client ;
let deposit_money =
Contract.call ~dest:main_instance ~amount:10tz ~entry:deposit ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ deposit_money ];
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 5tz then (
failwith "lucy should have 5tz now"
);
(* lucy walks out of the whole thing *)
Techel.start_set_source client ;
let drain =
Contract.call ~dest:main_instance ~amount:0tz ~entry:drain ~parameter:"lucy"
in
Techel.end_set_source () ;
Techel.apply_operations [ drain ] ;
(* lucy should have her money back *)
let balance_lucy = Techel.get_balance client in
if balance_lucy <> 2tz then (
failwith "lucy should have 2tz now"
);
nothing
contracts/
contracts/multi.liq
type storage = {
admins : (string, address) map ;
users : (string, (address * tez * UnitContract.instance)) map ;
}
let admin_check (storage : storage) (name : string) (a : address) : unit =
match Map.find name storage.admins with
| None -> failwith "only admins can perform administrative tasks"
| Some address ->
if address <> a then
failwith "illegal access to admin account"
let%entry add_admin
(
(admin_name, nu_admin_name, nu_admin_address) :
string * string * address
) (
storage : storage
)
: operation list * storage
=
admin_check storage admin_name (Current.sender ());
let storage =
storage.admins <- Map.update nu_admin_name (Some nu_admin_address) storage.admins
in
[], storage
let%entry rm_admin (admin_name, user_name : string * string) (storage : storage) =
admin_check storage admin_name (Current.sender ());
let storage = storage.admins <- Map.update user_name None storage.admins in
[], storage
let%entry add_client (
(admin_name, user_name, user, c) :
string * string * address * UnitContract.instance
) (storage : storage) =
admin_check storage admin_name (Current.sender ());
if Map.mem user_name storage.users then (
failwith "username already taken"
);
let data = Some (user, 0tz, c) in
let storage = storage.users <- Map.update user_name data storage.users in
[], storage
let data_of (storage : storage) (name : string) (user : address) : tez * UnitContract.instance =
match Map.find name storage.users with
| None -> failwith "unknown user"
| Some (address, tez, c) ->
if user <> address then
failwith "illegal access to account"
else (tez, c)
let%entry deposit (name : string) (storage : storage) =
let user = Current.sender () in
let money, c = data_of storage name user in
let amount = Current.amount () in
let nu_data = Some (user, money + amount, c) in
[], storage.users <- Map.update name nu_data storage.users
let%entry withdraw (name, amount : string * tez) (storage : storage) =
let user = Current.sender () in
let money, c = data_of storage name user in
if amount > money then
failwith "insufficient balance"
else (
let nu_data = Some (user, money - amount, c) in
[], storage.users <- Map.update name nu_data storage.users
)
let%entry drain (name : string) (storage : storage) =
let user = Current.sender () in
let money, c = data_of storage name user in
let storage = storage.users <- Map.update name None storage.users in
let ops = [Contract.call ~dest:c ~amount:money ~parameter:()] in
ops, storage
Software Safety
There is no post in this category yet.