Timestamp Control

Some contracts need to reason about time, based on the timestamp of the block the computation takes place in. By default, the timestamp of all blocks in Techelson is 1970-01-01T00:00:00Z. Testcases can set this timestamp to anything, the only constraint is that the new timestamp is older than the previous one. The relevant instruction is

instruction parameter stack
SET_TIMESTAMP none :: timestamp : 'S
-> 'S

As an example, consider the following contract Timestamp which takes (or unit (contract unit)): it

  • receives money once (Left Unit), and
  • unlocks it if one week has passed since it receives money.

To unlock the money, someone must call the contract and give it a unit contract to collect the money (Right <contract>). Unlocking the money is only legal after one week (604800 seconds) has passed since the money was received.

The code follows. Its storage is (option timestamp) which stores the last time it receives money. The contract fails if asked to

  • receive money but it's already storing money (its storage is not None),
  • unlock the money but it hasn't received anything (its storage is None), and
  • unlock the money but one week hasn't passed since it was received.
storage (option timestamp) ;
parameter (or unit (contract unit)) ;
code {
    UNPAIR @storage @param ;
    IF_LEFT {
        DROP ;
        IF_NONE {
            NOW ;
            SOME ;
            NIL operation ;
            PAIR
        } {
            PUSH string "cannot receive money twice" ;
            FAILWITH
        }
    } {
        SWAP ;
        IF_NONE {
            PUSH string "cannot send money, no money received" ;
            FAILWITH
        } {
            NOW ;
            SUB ;
            PUSH int 604800 ; # One week in seconds.
            IFCMPGT {
                PUSH string "cannot send money, it has not been one week yet" ;
                FAILWITH
            } {
                BALANCE ;
                UNIT ;
                TRANSFER_TOKENS ;
                DIP {
                    NONE timestamp ;
                    NIL operation
                } ;
                CONS ;
                PAIR
            }
        }
    } ;
} ;

Let's go through TestTimestamp, the testcase for Timestamp. The first step should be unsurprising by now: deploy the contract and an account (so that we can unlock the money later).

{
    { # Deploy contract.
        NONE timestamp ;
        PUSH mutez 0 ;
        PUSH bool False ;
        PUSH bool True ;
        NONE key_hash ;
        PUSH key "manager" ;
        HASH_KEY ;
        CREATE_CONTRACT @contract "Timestamp" ;
        DIP NIL operation ;
        CONS ;
    } ;

    { # Deploy account to retrieve the money later on.
        PUSH mutez 0 ;
        PUSH bool True ;
        NONE key_hash ;
        PUSH key "manager" ;
        HASH_KEY ;
        CREATE_ACCOUNT @account ;
        DIP SWAP
    } ;

    CONS ;
    APPLY_OPERATIONS ;
    SWAP ;

    { # Retrieve contract.
        CONTRACT (or unit (contract unit)) ;
        IF_NONE {
            PUSH string "failed to retrieve contract" ;
            FAILWITH
        } {}
    } ;

    DIP { # Retrieve account.
        CONTRACT unit ;
        IF_NONE {
            PUSH string "failed to retrieve account" ;
            FAILWITH
        } {}
    } ;

Next, let's set the timestamp to January 1 2019, 11am, and send some money to the contract.

    { # Set timestamp.
        PUSH timestamp "2019-01-01T11:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Send money to the account.
        DUP ;
        PUSH mutez 10 ;
        UNIT ;
        LEFT (contract unit) ;
        TRANSFER_TOKENS ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

We now check the storage is what it should be:

    { # Check the storage is correct.
        DUP ;
        GET_STORAGE (option timestamp) ;
        IF_NONE {
            PUSH string "failed to retrieve storage" ;
            FAILWITH
        } {
            IF_NONE {
                PUSH string "storage should not be `None`" ;
                FAILWITH
            } {
                PUSH timestamp "2019-01-01T11:00:00Z" ;
                IFCMPNEQ {
                    PUSH string "storage should be `2019-01-01T11:00:00Z`" ;
                    FAILWITH
                } {}
            }
        }
    } ;

Let's make sure unlocking the money before one week has passed fails. First, the testcase sets the timestamp to January 8 2019, 9am, which is not one week later than the date at which we sent money to the contract. So this should fail.

    { # Set timestamp to almost one week later.
        PUSH timestamp "2019-01-08T09:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Try to retrieve the money.
        DUP ;
        PUSH mutez 0 ;
        DUUUUP ;
        RIGHT unit ;
        TRANSFER_TOKENS ;
        PUSH (option string) (Some "cannot send money, it has not been one week yet") ;
        MUST_FAIL string ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

Last, let's set the date to January 8 2019, 11am, at which point unlocking the money should work.

    { # Set timestamp to exactly one week later.
        PUSH timestamp "2019-01-08T11:00:00Z" ;
        SET_TIMESTAMP
    } ;

    { # Try to retrieve the money.
        DUP ;
        PUSH mutez 0 ;
        DUUUUP ;
        RIGHT unit ;
        TRANSFER_TOKENS ;
        DIP NIL operation ;
        CONS ;
        APPLY_OPERATIONS
    } ;

    { # Check the account received it.
        DROP ;
        GET_BALANCE ;
        PUSH mutez 10 ;
        IFCMPNEQ {
            PUSH string "account's balance should be 10utz" ;
            FAILWITH
        } {}
    }
}

All set. Running techelson yields the following output. It is split in two parts here, first up to the request to unlock the money on January 8 2019 at 9am, which should fail:

$ techelson --contract rsc/timestamp/contracts/timestamp.tz -- rsc/timestamp/okay/testTimestamp.techel
Running test `TestTimestamp`

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation CREATE[uid:1] (@address[2]@account, "b58check:manager", 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]@account
                   Timestamp (0utz) address[1]@contract

running test script...
   timestamp: 1970-01-01 00:00:00 +00:00

applying operation TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
   timestamp: 2019-01-01 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (0utz) address[1]@contract

running TRANSFER[uid:2] address[0]@TestTimestamp -> address[1]@contract 10utz (Left Unit)
   timestamp: 2019-01-01 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running test script...
   timestamp: 2019-01-01 11:00:00 +00:00

applying operation MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" : 
string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
   timestamp: 2019-01-08 09:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 09:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract
failure confirmed on test operation
  MUST_FAIL[uid:4] "cannot send money, it has not been one week yet" : 
  string (TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account))
while running operation TRANSFER[uid:3] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
failed with value "cannot send money, it has not been one week yet" : 
string

So far so good. Finally, the rest of the output should go smoothly and succeed:

running test script...
   timestamp: 2019-01-08 09:00:00 +00:00

applying operation TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:5] address[0]@TestTimestamp -> address[1]@contract 0utz (Right address[2]@account)
   timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

applying operation TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
   timestamp: 2019-01-08 11:00:00 +00:00
   live contracts: <anonymous> (0utz) address[2]@account
                   Timestamp (10utz) address[1]@contract

running TRANSFER[uid:6] address[1]@contract -> address[2]@account 10utz Unit
   timestamp: 2019-01-08 11:00:00 +00:00
=> live contracts: <anonymous> (10utz) address[2]@account
                   Timestamp (0utz) address[1]@contract

running test script...
   timestamp: 2019-01-08 11:00:00 +00:00

Done running test `TestTimestamp`