Testing for Failures
This section is going to use a slightly more complex contract in order to showcase failures and how to test them. Even if you are not familiar with liquidity, the contract's code will most likely be more readable in liquidity than in michelson. Here is the liquidity version, admins.liq:
[%%version 0.405]
type storage = {
admins : (string, address) map ;
(* Unused in this example. *)
nus : (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_name, nu_address) : string * string * address)
(storage : storage)
: operation list * storage
=
admin_check storage admin_name (Current.sender ());
let storage =
storage.admins <- Map.update nu_name (Some nu_address) storage.admins
in
[], storage
Note that the clients
field of the storage is unused in this example. The admins
map maps
administrator names to addresses. The only entry point (in this example) is add_admin
which
allows administrators to add new administrators by registering their name and their address. More
precisely, calling this contract is only legal if the SENDER
(Current.sender ()
) of the call is
an administrator (c.f. admin_check
). If the call to the contract is not legal, the transfer
fails:
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"
The parameters of the entry point are
admin_name
: name associated with theSENDER
administrator,nu_name
: name of the new administrator to add,nu_address
: the address of the new administrator.
let%entry add_admin
((admin_name, nu_name, nu_address) : string * string * address)
Using liquidity to compile the contract to michelson (for instance using liquidity's online
editor), we obtain admins.tz. Here are the storage
and parameter
types:
parameter (pair string (pair string address));
storage
(pair :storage
(map %admins string address)
(map %clients string (pair address (pair mutez (contract :UnitContract unit)))));
We omit the contract's code (admins.tz) as i) it is not very readable and ii)
we do not need to know what the code precisely is to create the contract and call it, as long as we
know the storage
and parameter
types.
Creation
Creating a contract has been covered in previous sections, so let's give ourselves some code to
create the contract with one administrator called root
. In fact, let's make an account for root
and register it as an administrator. The new administrator new_admin
is also deployed as an
account. Testcase create.techel does exactly that:
{
NIL operation ;
{ # Create an account for `root`.
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@root_manager" ;
SHA512 ;
CREATE_ACCOUNT @root ;
} ;
SWAP ;
DIP { CONS } ;
{ # Create an account for `new_admin`.
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@new_admin_manager" ;
SHA512 ;
CREATE_ACCOUNT @new_admin ;
} ;
SWAP ;
DIP { SWAP ; DIP CONS } ;
{ # Create an `admins` contract.
# Create the storage's (empty) `clients` field.
EMPTY_MAP @clients string (pair address (pair mutez (contract unit))) ;
# Create the storage's `admins` field and register `root`.
EMPTY_MAP @admins string address ;
DUUUUP ; # Retrieve root's address.
SOME @address ;
PUSH @name string "root" ;
PRINT_STACK ;
UPDATE ;
PAIR @storage ;
PUSH @balance mutez 0 ;
PUSH @delegatable bool True ;
PUSH @spendable bool False ;
PUSH @delegate (option key_hash) None ;
PUSH @manager key "@contract_manager" ;
SHA512 ;
CREATE_CONTRACT @admins "Admins"
} ;
SWAP ;
DIP { SWAP ; DIP { SWAP ; DIP CONS } } ;
DIIIP { APPLY_OPERATIONS } ;
PRINT_STACK ;
STEP "after applying creation operations."
}
Running this test produces the following output
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/create.techel
Running test `Create`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| [ CREATE[uid:1] (@address[2]@new_admin, "sha512:@new_admin_manager", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
}, CREATE[uid:0] (@address[1]@root, "sha512:@root_manager", None, true, true, 0utz)
{
storage unit ;
parameter unit ;
code ...;
} ] |
| (list operation) |
|--------------------------------------------------------------------------------------------------|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| @clients |
| Map { } |
| (map string (pair address (pair mutez (contract unit)))) |
|--------------------------------------------------------------------------------------------------|
| @admins |
| Map { } |
| (map string address) |
|--------------------------------------------------------------------------------------------------|
| @address |
| (Some address[1]@root) |
| (option address) |
|--------------------------------------------------------------------------------------------------|
| @name |
| "root" |
| string |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| @admins |
| address[3]@admins |
| address |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stopping [after applying creation operations.] press `return` to continue
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `Create`
Transfer Failures
Let's now add new_admin
as a new administrator. Testcase addAdmin.techel only adds the
following few instructions at the end of create.techel:
# Retrieve the actual contract.
CONTRACT (pair string (pair string address)) ;
IF_NONE {
PUSH string "failed to retrieve `admins` contract" ;
STEP
} {} ;
# Saving the contract for later.
DUP ;
PUSH @amount mutez 0 ;
# New admin's address.
DUUUUP ;
# New admin's name.
PUSH @new_name string "new_admin" ;
PAIR ;
# Root's name.
PUSH @name string "root" ;
PAIR @storage ;
TRANSFER_TOKENS ;
DIP { NIL operation } ;
CONS ;
APPLY_OPERATIONS ;
What should the result of applying this transfer be? Remember than before adding an administrator, the contract checks that the sender is an admin.
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_name, nu_address) : string * string * address)
...
=
admin_check storage admin_name (Current.sender ());
...
So, if everything goes well, the transfer should fail: the sender here is not root
, but the
testcase. In techelson, the testcase currently running has its own address. It is in particular
not the address of root
. Hence, the transfer fails as it should and so does the whole testcase.
The (relevant part of the) output is
Test `AddAdmin` failed:
Error
operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)) was expected to succeed
but failed on operation TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
operation failed on "illegal access to admin account" : string
You can see in the transfer the sender and the target of the transfer:
TRANSFER[uid:3] address[0]@AddAdmin -> address[3]@admins
AddAdmin
is the name of our testcase, and address[0]@AddAdmin
is its address. Name "root"
does not map to this address in the contract and the transfer fails.
Handling Failures
Before getting into making this transfer work (next section), note that this (failing) testcase is actually useful. Or at least it should be: the transfer we are trying to make is illegal indeed. We do want the transfer to fail, but the testcase should
- succeed if the transfer does fail,
- fail if the transfer succeeds: anyone can add admins, which is bad.
This is what the MUST_FAIL
techelson extension does. It takes an operation wraps it in a
construct telling techelson that this operation must fail: either the operation itself or, if it is
a transfer, the operations created by this transfer. Here is its signature:
instruction | parameter | stack |
---|---|---|
MUST_FAIL | <type> | :: option <type> : operation : 'S |
-> operation : 'S |
Let's ignore the <type>
parameter and the first stack argument for now and just use this
instruction right away. Testcase mustFail.techel is the same as addAdmin.techel except for a
few lines after the transfer:
TRANSFER_TOKENS ;
PUSH (option string) None ;
MUST_FAIL @this_must_fail string ;
PRINT_STACK ;
The test now passes successfully:
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/mustFail.techel
Running test `MustFail`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| @this_must_fail |
| MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation |
|==================================================================================================|
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `MustFail`
Notice that Techelson lets you know the failure is confirmed:
failure confirmed on test operation
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@MustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
(More) Precise Failure Testing
Now, MUST_FAIL
(as it is used here) succeeds if the transfer ends in a tezos protocol failure.
This include explicit failures in the code, illegal transfers due to insufficient funds, duplicate
operations, etc. It does not include type-checking errors and internal techelson errors.
This means in particular that if the transfer above fails for a reason different from "illegal access to admin account"
then MUST_FAIL
will consider the test a success. To make sure the cause
for failure is actually the one we want, we must use MUST_FAIL
's optional stack parameter. A
failure in michelson code always has a value of some type associated to it. In this case, the type
of this value is string
and its value is "illegal access to admin account"
. Testcase
preciseMustFail.techel only changes mustFail.techel to pass the failure value expected to MUST_FAIL
:
TRANSFER_TOKENS ;
PUSH (option string) (Some "illegal access to admin account") ;
MUST_FAIL @this_must_fail string ;
PRINT_STACK ;
As a consequence, if the transfer fails with anything else than an explicit failure with a value of
type string
equal to "illegal access to admin account"
, then the whole testcase will fail.
Everything works fine here, and the output is
$ techelson --contract rsc/admins/contracts/admins.tz -- rsc/admins/okay/preciseMustFail.techel
Running test `PreciseMustFail`
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
applying operation CREATE[uid:2] (@address[3]@admins, "sha512:@contract_manager", None, false, true, 0utz) "Admins"
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: none
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
stack:
|==================================================================================================|
| @root |
| address[1]@root |
| address |
|--------------------------------------------------------------------------------------------------|
| @new_admin |
| address[2]@new_admin |
| address |
|--------------------------------------------------------------------------------------------------|
| address[3]@admins |
| (contract (pair string (pair string address))) |
|--------------------------------------------------------------------------------------------------|
| @this_must_fail |
| MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))) |
| operation |
|==================================================================================================|
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]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
timestamp: 1970-01-01 00:00:00 +00:00
live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
running TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
timestamp: 1970-01-01 00:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@new_admin
Admins (0utz) address[3]@admins
<anonymous> (0utz) address[1]@root
failure confirmed on test operation
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin)))
while running operation TRANSFER[uid:3] address[0]@PreciseMustFail -> address[3]@admins 0utz ("root", ("new_admin", address[2]@new_admin))
failed with value "illegal access to admin account" : string
running test script...
timestamp: 1970-01-01 00:00:00 +00:00
Done running test `PreciseMustFail`
Notice that the MUST_FAIL
operation now mentions the value expected:
MUST_FAIL[uid:4] "illegal access to admin account" : string (TRANSFER[uid:3] ...)
as opposed to the _
wildcard from testcase mustFail.techel, which means no value was given:
MUST_FAIL[uid:4] _ (TRANSFER[uid:3] ...)