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 the SENDER 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] ...)