I tested Tezos Part. 3

Mathis Selvi
ON-X Blockchain (Chain-Accelerator)
28 min readJun 7, 2019
Tezos Logo

This is the third part of our “I tested Tezos” serie. Please, first read:
- Part 1: https://medium.com/chain-accelerator/i-tested-tezos-b254504775be
- Part 2: https://medium.com/chain-accelerator/i-tested-tezos-part-2-da5d94ce1259

In this story, we’re going to use the first two parts to learn about contract origination and interaction, we will then learn more about Michelson data structures. Finally, we’ll develop an auction smart contract.

Getting ready

In order to start, we’re going to need an account with some funds in it. We will use what we learned on the first part to create an account named genny.
If you’re following this DIRECTLY after part 2, you will need to shut down the sandboxed node we used.

For now we’ll run a node like we did in the first part, by doing this:

chainacc@tezos:~$ cd tezos/chainacc@tezos:~/tezos$ ./tezos-node run --rpc-addr 127.0.0.1 --connections 10

Your node should start syncing. Wait for it to be fully synced. Once it’s done, let’s create our genny account.

To do so, go back to the faucet: https://faucet.tzalpha.net/ and grab a new .json file. Then, use the following command to create your account:

chainacc@tezos:~/tezos$ ./tezos-client activate account genny with <path to the faucet>.json

which should display something similar to the following:

Warning:

This is NOT the Tezos Mainnet.

The node you are connecting to claims to be running on the
Tezos Alphanet DEVELOPMENT NETWORK.
Do NOT use your fundraiser keys on this network.
Alphanet is a testing network, with free tokens.
Node is bootstrapped, ready for injecting operations.
Operation successfully injected in the node.
Operation hash is 'op5X1nPsVTgcTtu7Yptv5GWA43fHwgbDd6eDRJMz3RXkqbfexvi'
Waiting for the operation to be included...
Operation found in block: BMebKHPu4MVwjaA7wAqaMhe3B6AN3CTJdy5UfpfC1oUfSUEKmuS (pass: 2, offset: 0)
This sequence of operations was run:
Genesis account activation:
Account: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... +ꜩ11788.812458
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for op5X1nPsVTgcTtu7Yptv5GWA43fHwgbDd6eDRJMz3RXkqbfexvi to be included --confirmations 30 --branch BL6wadKUahHFZkGDwMcpeoom2ufcNkR5ewtc635461144yKzoYE
and/or an external block explorer.
Account genny (tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m) activated with ꜩ11788.812458.

Awesome, we now have our genny account funded with some tez. Let’s get to the next part, contract origination :)

Contract origination

Our first step is to push a simple contract onto the Alphanet. To do so, we need to use our sandboxed node like we did in Part 2. If you didn’t let your sandboxed node run, go back to Part 2 and follow the first instructions to run it, and then to be able to use emacs on another terminal in which you previously entered the eval command as explained in Part 2.

Once you’re ready, create a new file called idString.tz:

chainacc@tezos:~/tezos$ emacs idString.tz

And write the following code in it:

parameter string;
storage string;
code {CAR; NIL operation; PAIR;};

First, test the contact locally by running this command (that we learned in Part 2):

chainacc@tezos:~/tezos$ ./tezos-client run script idString.tz on storage '"foo"' and input '"bar"'

It’s a very simple contract that will replace the storage string "foo" by the parameter passed to it as the new storage, "bar" like so:

Warning:

This is NOT the Tezos Mainnet.

The node you are connecting to claims to be running on the
Tezos Alphanet DEVELOPMENT NETWORK.
Do NOT use your fundraiser keys on this network.
Alphanet is a testing network, with free tokens.
storage
"bar"
emitted operations

We just tested it locally, it works. Perfect, but that’s not what we want. We want to push the contract to the Alphanet. To do so, we’re going to use our previously created account, genny. Run the following command:

chainacc@tezos:~/tezos$ ./tezos-client originate contract idString transferring 1 from genny running idString.tz --init '"hello"'

You should have the famous --burn-cap error. According to the error, add the flag to your command and run it again. Should display something like that:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 11262 units (will add 100 for safety)
Estimated storage: 303 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'ooB6zzF8V8KGPaZFwRTeaKgwqe792zZ8TkYWiGM8Gu356APQDph'
Waiting for the operation to be included...
Operation found in block: BLhfj8LUiDRiJevL1cS88U4f1u34RvSVEaasTdL5ifk9Xj17R55 (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.00126
Expected counter: 58492
Gas limit: 10000
Storage limit: 0 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.00126
fees(tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB,174) ... +ꜩ0.00126
Revelation of manager public key:
Contract: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Key: edpkujpm6Bo1ixZJFhdetCPEyCDJQMb9wL2K8U3KDF3kx2ZJCBCB4W
This revelation was successfully applied
Consumed gas: 10000
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.001343
Expected counter: 58493
Gas limit: 11362
Storage limit: 323 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.001343
fees(tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB,174) ... +ꜩ0.001343
Origination:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
For: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Credit: ꜩ1
Script:
{ parameter string ;
storage string ;
code { CAR ; NIL operation ; PAIR } }
Initial storage: "hello"
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Storage size: 46 bytes
Paid storage size diff: 46 bytes
Consumed gas: 11262
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.046
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.257
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ1
KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU ... +ꜩ1
New contract KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for ooB6zzF8V8KGPaZFwRTeaKgwqe792zZ8TkYWiGM8Gu356APQDph to be included --confirmations 30 --branch BMWU5Xv4amy4EqSZZrJaV5x7KVd9BHyuEHuEgbSAQLP2LdP7HPb
and/or an external block explorer.
Contract memorized as idString.

Relevant lines are:

This origination was successfully applied
Originated contracts:
KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Storage size: 46 bytes

These lines display the contract address and the size of the contract + contract’s storage in bytes. Our contract, after a little compression, is 41 bytes long, and the string we sent, “hello” is 5 bytes long, making the total storage size of 46 bytes.

As you understand, and it is VERY important, the bigger the storage size is, the more we have to pay the network.

Now, in order to be able to check it on https://alphanet.tzscan.io/ we have to wait a little bit for the operation to get confirmed. Once it’s done, head to TzScan and let’s check for our contract by copying and pasting the contract address (see above) in the search bar. In the origination tab, you should be able to see your contract, and view its code:

Our contract code

You can also view the current storage value, which should be “Hello”:

Our contract’s current storage value

Congratulations! You just pushed your first smart contract onto the Tezos network. ;)

Now let’s go ahead and see how to call a contract.

Calling a contract

Calling a contract means sending a transaction to its address. When you transfer tez from Bob to Alice, you’re basically calling a contract (Alice’s one in this specific case).

To call our contract idString we’re going to use the transfer command towards the contract itself. First, let’s make sure our client knows the contract needed for this:

chainacc@tezos:~/tezos$ ./tezos-client list known contracts

My results:

idString: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
bob_del: KT1C9UTY9fVXWaEpUxLfxaxdZKi2uuoFjRa1
genny: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Bob: tz1hBT7dx9aaiZTEsSJUnB8fKQ76EswyTLgZ
Alice: tz1VFgcWrcLvxpWcMvKnzFZpLjWxNT4wuosx

Accounts starting with tz are contracts that do not have any code, meaning the only thing they can do is hold a balance. Contracts that can execute code however, start with KT1.

What we will do to call our idString contract is transfer some tez to it. To pass some Michelson data to the contract, we will use the --arg <parameter> flag. As of right now, our current contract storage value is "hello", but we will change it to "world" with the parameter.

One more thing before doing this, to simulate the command and see what it does without sending anything out the network yet, we will use the --dry-run flag.

Let’s do this:

chainacc@tezos:~/tezos$ ./tezos-client transfer 0 from genny to idString --arg '"world"' --dry-run

And let’s analyze the result:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 11375 units (will add 100 for safety)
Estimated storage: no bytes added
Operation: 0x7eb94335bddc743390672d053f844e5ef484664b047b2136ffcbf34dc5c29d9a080000507a2f5a931ad249da4d60b6d9da64960bb67c09850bfec803d359000001766cb32a3ea52a68ab894b328c858eac0ecaeaf800ff0000000a0100000005776f726c64577978be6c6c177bd868857e16aec3b1244025c61b2be827815e2c784c423bb1e07cd19415759198d7286a9249590addda3750ce7c1b80cddd51f03131a04309
Operation hash is 'onfSeSps2hqK2ZE7HZ2b6ydfUQ9hfDcrFb5SPfke2ZAtd2GEBHU'
Simulation result:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.001413
Expected counter: 58494
Gas limit: 11475
Storage limit: 0 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.001413
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,175) ... +ꜩ0.001413
Transaction:
Amount: ꜩ0
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
To: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Parameter: "world"
This transaction was successfully applied
Updated storage: "world"
Storage size: 46 bytes
Consumed gas: 11375

Everything seems fine and our transaction WILL go through without any problem. Storage size is still 46 bytes because both "hello" and "world" are the same length (5 bytes).

To see what happens when something is wrong, let’s try the same command with a wrong parameter, for example an int instead of a string:

chainacc@tezos:~/tezos$ ./tezos-client transfer 0 from genny to idString --arg '667' --dry-run

As expected, we get an error:

Node is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0
Expected counter: 58494
Gas limit: 400000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ0
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
To: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Parameter: 667
This operation FAILED.
Invalid argument passed to contract KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU.
At (unshown) location 0, value 667 is invalid for type string.
At (unshown) location 0, unexpected int, only a string can be used here.
Fatal error:
transfer simulation failed

In order for the network to accept the transaction, you must respect the parameter’s type.

You should always use the --dry-run flag to simulate the behavior of your commands.

Now, let’s do this the right way (like we did 5 minutes ago), but without the --dry-run flag.

chainacc@tezos:~/tezos$ ./tezos-client transfer 0 from genny to idString --arg '"world"'

We have this:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 11375 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'op1cfcsDQVvvzaxBeDsH1di9d3KkWAwSXLw1PGezmdPC4Bu9BBK'
Waiting for the operation to be included...
Operation found in block: BLJwi12bd92gid6MNRYyqS9e2MYCzcNGnPXromHYDwgZgEpv1LM (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.001413
Expected counter: 58494
Gas limit: 11475
Storage limit: 0 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.001413
fees(tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB,175) ... +ꜩ0.001413
Transaction:
Amount: ꜩ0
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
To: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Parameter: "world"
This transaction was successfully applied
Updated storage: "world"
Storage size: 46 bytes
Consumed gas: 11375
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for op1cfcsDQVvvzaxBeDsH1di9d3KkWAwSXLw1PGezmdPC4Bu9BBK to be included --confirmations 30 --branch BKo2X96x7ysEcCB6hnNNSDQJf92wvfgWcbPQPwP5MQsZbsHU6md
and/or an external block explorer.

After waiting for a bit, we can now go back to the explorer and check the storage of our contract.

Our contract’s updated storage value

Congratulations! You just called your contract from the command line :) Now, let’s see how to call it from ANOTHER contract.

Calling a contract from another contract

We’re now going to call our idString contract from a new contract named stringCaller.tz. We will call this contract with a string parameter, and it will take care of passing the parameter to our idString contract.

Create a new file named stringCaller.tz and let’s write this code in it:

parameter string;
storage address;
code { DUP;
DUP;
CDR;
CONTRACT string;
IF_NONE {DROP; NIL operation}
{SWAP;
CAR;
DIP {PUSH mutez 0};
TRANSFER_TOKENS;
DIP {NIL operation};
CONS;
};
DIP { CDR };
PAIR;
};

To better understand the code, let’s use the typecheck command with the -v flag:

chainacc@tezos:~/tezos$ ./tezos-client typecheck script stringCaller.tz -v

It should display the following:

Well typed
Gas remaining: 399293 units remaining
{ parameter string ;
storage address ;
code { /* [ pair (string @parameter) (address @storage) ] */
DUP
/* [ pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */ ;
DUP
/* [ pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */ ;
CDR
/* [ @storage address : pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */ ;
CONTRACT
string
/* [ @storage.contract option (contract string)
: pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */ ;
IF_NONE
{ /* [ pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */
DROP
/* [ pair (string @parameter) (address @storage) ] */ ;
NIL operation
/* [ list operation : pair (string @parameter) (address @storage) ] */ }
{ /* [ @storage.contract.some contract string
: pair (string @parameter) (address @storage)
: pair (string @parameter) (address @storage) ] */
SWAP
/* [ pair (string @parameter) (address @storage)
: @storage.contract.some contract string
: pair (string @parameter) (address @storage) ] */ ;
CAR
/* [ @parameter string : @storage.contract.some contract string
: pair (string @parameter) (address @storage) ] */ ;
DIP { /* [ @storage.contract.some contract string
: pair (string @parameter) (address @storage) ] */
PUSH mutez
0
/* [ mutez : @storage.contract.some contract string
: pair (string @parameter) (address @storage) ] */ }
/* [ @parameter string : mutez : @storage.contract.some contract string
: pair (string @parameter) (address @storage) ] */ ;
TRANSFER_TOKENS
/* [ operation : pair (string @parameter) (address @storage) ] */ ;
DIP { /* [ pair (string @parameter) (address @storage) ] */
NIL operation
/* [ list operation : pair (string @parameter) (address @storage) ] */ }
/* [ operation : list operation : pair (string @parameter) (address @storage) ] */ ;
CONS
/* [ list operation : pair (string @parameter) (address @storage) ] */ } ;
DIP { /* [ pair (string @parameter) (address @storage) ] */ CDR /* [ @storage address ] */ }
/* [ list operation : @storage address ] */ ;
PAIR
/* [ pair (list operation) (address @storage) ] */ } }

We have seen most of the operations in previous parts, so I will only explain the new ones.

CONTRACT

The CONTRACT string; instruction will simply push the contract at an address onto the stack.

MIchelson mode for CONTRACT instruction

This instruction use a new type we’ve never used before: option. This type has two data constructors: None and Some 'a which respectively holds a data value of None and 'a. It’s basically an IF statement that will check if the top value of the stack is a None or a Some 'a. If it’s a None, it will run instructions in the first branch, in our case: {DROP; NIL operation}. But if it’s a Some 'a, it will put the value, a', on top of the stack and run instructions from the second branch, in our case:

{SWAP;
CAR;
DIP {PUSH mutez 0};
TRANSFER_TOKENS;
DIP {NIL operation};
CONS;
};

To summarize this, in our case, we will check if the contract we send to the CONTRACT instruction exists or not. If not, top stack value will be updated to None and if yes, top stack value will be updated with a typed representation of the contract at the specified address, wrapped in the Some constructor.

In that case, it’s way safer to interact with the specified contract.

TRANSFER_TOKENS

The TRANSFER_TOKENS; instruction will forge a transaction from a parameter and mutez value to a recipient contract.

MIchelson mode for TRANSFER_TOKENS instruction

As you can see, our first two elements on top of the stack are a parameter and a mutez value. The instruction uses those two elements for the transaction and then send it to a recipient contract, the new top element of the stack ( (@storage.contract.some (contract string))). Final element is an (operation).

This instruction does the exact same thing we did in the Part. 2 using command line to transfer some Tez. It created an emitted network operation that is pushed on top of the stack.

A contract returns an emitted operation list that contains the side-effects your contract will have on the Tezos blockchain, such as a transaction, a delegation, a contract origination, etc…

We’re now all done with the explanation of the code. Now, let’s originate stringCaller ;)

Originating stringCaller

We first need the address of our previous contract, idString:

chainacc@tezos:~/tezos$ ./tezos-client show known contract idString

which displays:

KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU

Obviously, yours will be different. We will use the address to pass it as initial storage to stringCaller, with a burn-cap:

chainacc@tezos:~/tezos$ ./tezos-client originate contract stringCaller for genny transferring 1 from genny running stringCaller.tz --init '"KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU"' --burn-cap 0.5

which should display something similar to:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 13445 units (will add 100 for safety)
Estimated storage: 383 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'opGQqCCLqc7Tt1KZ5RzqAYCVUaF8p57THx1ySjvZgx6g31SiUo6'
Waiting for the operation to be included...
Operation found in block: BLUFGWUZKBEScD1knrWcwR724yqviytjgC9Cjh2QYEkfg3Exq8x (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.001751
Expected counter: 58495
Gas limit: 13545
Storage limit: 403 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.001751
fees(tz1P7VKowPyixkoFDqGrfEaoXnaTLqkZ99MU,182) ... +ꜩ0.001751
Origination:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
For: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Credit: ꜩ1
Script:
{ parameter string ;
storage address ;
code { DUP ;
DUP ;
CDR ;
CONTRACT string ;
IF_NONE
{ DROP ; NIL operation }
{ SWAP ;
CAR ;
DIP { PUSH mutez 0 } ;
TRANSFER_TOKENS ;
DIP { NIL operation } ;
CONS } ;
DIP CDR ;
PAIR } }
Initial storage: "KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU"
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2
Storage size: 126 bytes
Paid storage size diff: 126 bytes
Consumed gas: 13445
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.126
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.257
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ1
KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2 ... +ꜩ1
New contract KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2 originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for opGQqCCLqc7Tt1KZ5RzqAYCVUaF8p57THx1ySjvZgx6g31SiUo6 to be included --confirmations 30 --branch BLX53ac5B7ibCbHQB1d4J8XVVMXshNHc8WH4APKbH5tqYE24KTn
and/or an external block explorer.
Contract memorized as stringCaller.

If you now check this contract on the Blockchain (after waiting a bit), the correct storage should appear:

Updated storage of stringCaller

Congrats! You just originated stringCaller and now we’re going to run it ;)

Running stringCaller

Let’s call it with the string "medium", with a --dry-run first:

chainacc@tezos:~/tezos$ ./tezos-client transfer 0 from genny to stringCaller --arg '"medium"' --burn-cap 0.001 --dry-run

Everything seems fine, it displays:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 35759 units (will add 100 for safety)
Estimated storage: 1 bytes added (will add 20 for safety)
Operation: 0xac2375e78ea6133230e1a4b43091c7e195acddf4efbb19cf29c1e049191482da080000507a2f5a931ad249da4d60b6d9da64960bb67c098d1e80c903939802150001fd792eb7cf2f1443717fdf437bc82942cdfb103d00ff0000000b01000000066d656469756d5e386de688887dc0a2c9909cf41ce28a47f270bbefc46a0668bbf98ec06ac3b0a1dc58fb8f2df37c0314e54a6b6fa2b9362612506fe983469a8bcfc325a1a508
Operation hash is 'oo1gMwRJ9aGBNi6r8uumRYARJndP3pi4cDq3TfZTy9LJJFWCEBx'
Simulation result:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.003853
Expected counter: 58496
Gas limit: 35859
Storage limit: 21 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.003853
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,182) ... +ꜩ0.003853
Transaction:
Amount: ꜩ0
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
To: KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2
Parameter: "medium"
This transaction was successfully applied
Updated storage: 0x01766cb32a3ea52a68ab894b328c858eac0ecaeaf800
Storage size: 126 bytes
Consumed gas: 24369
Internal operations:
Transaction:
Amount: ꜩ0
From: KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2
To: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Parameter: "medium"
This transaction was successfully applied
Updated storage: "medium"
Storage size: 47 bytes
Paid storage size diff: 1 bytes
Consumed gas: 11390
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.001

As everything is okay, we can run it without the --dry-run flag. It should now display something similar to:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 35759 units (will add 100 for safety)
Estimated storage: 1 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'opZDHCmJAJ1wmoAfboemtUSSYNwiy6F3N3omKM8mUpzxkgb6Eri'
Waiting for the operation to be included...
Operation found in block: BMF7aNK8gPf1K6KGSCotTdDfNvEatCumCipNUdiy9jLd1fEP6KZ (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
Fee to the baker: ꜩ0.003853
Expected counter: 58496
Gas limit: 35859
Storage limit: 21 bytes
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ............. -ꜩ0.003853
fees(tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB,182) ... +ꜩ0.003853
Transaction:
Amount: ꜩ0
From: tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m
To: KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2
Parameter: "medium"
This transaction was successfully applied
Updated storage: 0x01766cb32a3ea52a68ab894b328c858eac0ecaeaf800
Storage size: 126 bytes
Consumed gas: 24369
Internal operations:
Transaction:
Amount: ꜩ0
From: KT1Xh1hCt8cQt5WDU373Wyzxy15uuNEPJFh2
To: KT1KNwWzyHZ688DiHmf8XwNU75cu7AiHkiCU
Parameter: "medium"
This transaction was successfully applied
Updated storage: "medium"
Storage size: 47 bytes
Paid storage size diff: 1 bytes
Consumed gas: 11390
Balance updates:
tz1SyZ82VfHweVYfUnVcMP5NMGM6Dor3RZ8m ... -ꜩ0.001
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for opZDHCmJAJ1wmoAfboemtUSSYNwiy6F3N3omKM8mUpzxkgb6Eri to be included --confirmations 30 --branch BLk44NoxMUYxE7NthYVrAf8kSoZtmGRTcgA5qnxfv7hCxdqWtaL
and/or an external block explorer.

After waiting for a bit, we can check again storage for idString on tzScan:

idString storage updated

Booom! It worked! Congratulations, you just called a Michelson contract from another Michelson contract.

We’re now going to go further with the language and learn about Michelson data structures. Let’s get right into it!

Michelson Data Structures

We will go over a few different Michelson types, how they work and the instructions that operate on them. To do so, we’re going to create a contract that will only be used to test types, data constructors and instructions.

Create your file:

chainacc@tezos:~/tezos$ emacs testing.tz

And inside it, write the following code:

parameter unit;
storage unit;
code { DROP;
# begin arbitrary
PUSH string "Chain Accelerator Academy";
DROP;
# end arbitrary, stack MUST be empty
PUSH unit Unit; NIL operation; PAIR;
};

This contract is perfect for testing purposes. Indeed, you can write your code, that performs arbitrary stack manipulations, directly inside the comments. Although, one VERY important thing is that the stack must be EMPTY at the end of the arbitrary block.

We will use this code to manipulate different data types.

Pairs

So we already know about the PAIR instruction that takes the first two elements of the stack, makes a pair with them and push the pair on top of the stack. Now we’re going to see the differences between PAIR, pair and Pair.

  • PAIR, as we already know, is an instruction.
  • pair ‘l ‘r is a type constructor. It takes two types, a left-hand one ('l) and a right-hand one ('r) and composes both of them together into a product type.
  • Pair is a data constructor. It takes two data values and composes them together into a pair. To understand better, if a value val1 has type int and a value val2 has type string, then Pair val1 val2 has type pair int string.

To understand even better, let’s update our program to add an example. Open testing.tz and add the following lines inside the comments:

PUSH (pair nat int) (Pair 1 1);
DROP;

Let’s not forget the DROP instruction as we want to return an empty stack at the end of the arbitrary block. It should look like this:

parameter unit;
storage unit;
code { DROP;
# begin arbitrary
PUSH (pair nat int) (Pair 42 42);
DROP;
# end arbitrary, stack MUST be empty
PUSH unit Unit; NIL operation; PAIR;
};

PUSH takes a type parameter (in our case: (pair nat int)) and a value (Pair 42 42) and push a value with that type on top of the stack.

Let’s quickly typecheck our program:

chainacc@tezos:~/tezos$ ./tezos-client typecheck script testing.tz -v

And it displays:

Well typed
Gas remaining: 399721 units remaining
{ parameter unit ;
storage unit ;
code { /* [ pair (unit @parameter) (unit @storage) ] */
DROP
/* [] */ ;
PUSH (pair nat int) (Pair 42 42)
/* [ pair nat int ] */ ;
DROP
/* [] */ ;
PUSH unit Unit
/* [ unit ] */ ;
NIL operation
/* [ list operation : unit ] */ ;
PAIR
/* [ pair (list operation) unit ] */ } }

We can clearly see our type pair nat int in the output. Note that nat is the type for natural numbers.

As we’ve seen, pairs can be constructed with the PAIR operation, but also with the PAPAIR family of macros.

To learn more about macros in Michelson, you should check here: http://tezos.gitlab.io/zeronet/whitedoc/michelson.html#viii-macros

In practice, you usually don’t have to pay attention to the details of the macro expansions, and can mostly just use them as normal. The one caveat to this is that macros are expanded before contract origination, so only sequences of regular (also called “primitive”) operations appear on the Tezos chain. This can effect the transparency and legibility of the contract. Users can use the unexpand operation that allow them to see the unexpanded contract. Furthermore, since short macros can expand to relatively longer sequences of operations, use of macros can sometimes cause the author of a contract to underestimate gas consumption.

Let’s see how to use PAPAIR now.

Constructing Pairs with the PAPAIR macro

The PAPAIR family of macros allows for the construction of arbitrary pair structures from values on the stack.

For example, let’s consider the following stack:

x : y : z : []

PAPAIR will turn the stack into:

(Pair x (Pair y z)) : []

The final pair is composed of values x and (Pair y z).

Now, what if we wanted (Pair (Pair x y) z) with the nested pair on the LEFT? Well, simple, we would just use another macro from the family: PPAIIR.

To understand better the logic, just know that every P stands for Pair, every A for a left-hand leaf, every I for a right-hand leaf, and R to terminate the macro. See below for a schema:

PPAIIR Schema

Another way to understand this is to manipulate the macro like this:
First, let’s start with PAPPAIIR

Add some space between each letter:

P A P P A I I R 

Replace every P with an open parenthesis and a pair:

(pair A (pair (pair A I I R

Now, add a close parenthesis after every I:

(pair A (pair (pair A I) I) R

To end the manipulation, replace the R with as many close parenthesis as needed to complete the operation:

(pair A (pair (pair A I) I) )

The elements from the stack will now be assigned to this tree in the order they appear in the above string. So for example, if our stack is:

1 : 2 : 3 : 4 : []

Our operation would be:

(Pair 4 (Pair (Pair 3 2) 1) )

I hope you get the basics. Feel free to play around with these macros using testing.tz to go further.

Destructing Pairs with UNPAIR

The UNPAIR family of macros also exists. It is the inverse of the PAIR family. These macros take a nested pair tree at the top of the stack and place its leaves on the stack according to the order of the leaf in the macro string (as we’ve seen above).

The UNPAPPAIIR macro will simply reverse the PAPPAIIR one.

Selecting leaves with CAR and CDR macros

We can also destruct pair trees down to a single leaf by using the CAR and CDR primitive operations, which are part of CADR family of macros.

We already know that CAR selects the left-hand side while CDR selects the right-hand one. We can now combine them thanks to their macro. For example, CDDR does the SAME thing as CDR; CDR;.
In the same way, CDADR; does the same thing as CDR; CAR; CDR;, as you can see below:

PUSH int 1
/* [ int ] */ ;
PUSH nat 2
/* [ nat : int ] */ ;
PUSH string "3"
/* [ string : nat : int ] */ ;
PUSH bool True
/* [ bool : string : nat : int ] */ ;
PAPPAIIR
/* [ pair bool (pair (pair string nat) int) ] */ ;
CDADR
/* [ nat ] */ ;

Now that we learned about more data structures in Michelson, we’re going to use EVERYTHING we’ve seen so far to create an English auction smart contract. A CONCRETE USE CASE!

A Smart Contract: English Auctions

Our smart contract will be running English auctions (most famous auctions, open ascending price auction). The first version will be simple, but the point is to make it more realistic and complex, step by step.

First version: a simple auction

As it’s a simple version, the smart contract will be an auction of a single and unspecified item. A bid mainly consist of sending money to our contract. However, we also have to identify each bidder so that if someone’s bid is higher than previous one, the previous bidder gets his money back. To do so, we can get bidder’s address by using the SENDER instruction.
There is a problem with that though. What happens if the sender is a smart contract that fails when being called? Well, no new bid can be placed. Luckily, there is a way around this. Each bidder will be required to provide an address to where its bid will be returned. As a transfer to an implicit account never fails, the address will be given as a public key hash.

So, here is our contract input:

parameter (key_hash);

Our contract will need to store:

  • The last bidder, who is, as we’ve seen a minute ago, represented by a key_hash. We’ll have to use the option type, as there will be no bidder when the auction starts. key_hash will therefore be None initially.
  • The last bid’s amount, as mutez. This component’s initial value is the minimal amount the owner asks for his item.
  • Finally, the address of the item’s owner (or the auction beneficiary if it is different). Indeed, we need the address to let the owner retrieve the highest bid (if any) at the end of the auction. The contract must be called manually for the beneficiary to get his money, as contracts can not activate themselves.

All of this make our program’s storage look like this:

storage (pair (pair (option key_hash) mutez) address);

Let’s start implementing our smart contract. Fire up your Emacs and open a file called simpleAuction.tz. As seen earlier, first two lines of our program will be:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);

Parameter and storage, as usual. Let’s continue by opening our code brackets and adding our first lines into it:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
}

UNPAIR will separate the parameter/storage pair so we can have parameter on top of the stack and storage below it. Then, the next list of instructions SWAP; DUP; CDR; allows us to select the item owner’s address. We also need the address of the contract caller. To do so, we can use the SENDER instruction as seen earlier:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
SENDER;
}

We now have two addresses on top of the stack: first, the contract caller address (the one who called the contract to, for example, make a bid), and second, the item owner’s address. We want to compare them and see if they are equal, because if they are, it means that the person who called the contract is the item’s owner, which means he wants to collect the potential gained funds. To check, we add this:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
SENDER;
IFCMPEQ
{
# brackets 1
}
{
# brackets 2
}
}

IFCMPEQ will check if both addresses are equal. If yes, it’s going to apply the instructions we put in our brackets right after it (# brackets 1), and if not, it will apply what’s inside # brackets 2. Let’s first handle the case where both addresses are equal, meaning owner wants to collect the funds. What we’re going to do is check if there is a current bid or not. If there is one, it means the owner must be paid with that bid, and if not, we will just “ignore” owner’s request and close the auction. Let’s add this to our code, in # brackets 1:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
SENDER;
IFCMPEQ
{
DIP { DROP };
DUP; UNPPAIIR;
IF_SOME
{
DROP;
DIP { CONTRACT unit; ASSERT_SOME };
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{
DROP; DROP; NIL operation };
}
{
# brackets 2
}
}

Let’s analyze what we added. First, with

DIP { DROP };

We’re simply dropping the parameter as we do not need it, so we ignore it. Then, with:

DUP; UNPPAIIR;

We duplicate and then decompose the storage pair in order to get the previous bidder on top of the stack (if any), meaning that a potential ((option key_hash)) will be on top of the stack. We check if it is indeed there or not with the following line:

IF_SOME

If there is a ((option key_hash)) it means that there was at least one bid, and this is the last one (the winning one). Here is what we do in that case:

        IF_SOME   
{
DROP;
DIP { CONTRACT unit; ASSERT_SOME };
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}

Basically, we pay the item’s owner. The line starting with the DIP instruction will simply protect the top elem of the stack (in our case, mutez) and create a contract with the next elem in the stack (in our case, address). UNIT; will push a unit value on top of the stack. And then, TRANSFER_TOKENS will take unit, mutez and our (@contract.some (contract unit)) to emit an operation to the network (this is where the item owner gets paid). It will return an (operation). Finally, we prepare the final return value by using NIL operation; SWAP; CONS; that will create a list of operation, swap our operation and our list of operation, and then prepend our operation to the list of operation. We will need that return value at the end of the contract.

Now what happen if there was no bid? As we said, we simply ignore owner’s request (since there is no funds to collect), and we terminate the auction by terminating the contract, which is what we do in the next brackets, here:

         {
DROP; DROP; NIL operation };
}

We drop the first two elements of the stack, we create our list of operation, and our return value is now ready. ;)

Let’s continue with the code. What we did so far is handle the case where it’s the owner who called the contract. Now let’s handle the second case: someone makes a bid. This is where we put the code in # brackets 2, feel free to scroll up a bit to see where it is located if you don’t remember. So, let’s add it into our code:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
SENDER;
IFCMPEQ
{
DIP { DROP };
DUP; UNPPAIIR;
IF_SOME
{
DROP;
DIP { CONTRACT unit; ASSERT_SOME };
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{
DROP; DROP; NIL operation };
}
{
DUP; CADR;
AMOUNT;
ASSERT_CMPGT;
UNPPAIIR;
IF_SOME
{
IMPLICIT_ACCOUNT;
SWAP;
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{ DROP; NIL operation; };
DIP
{ SWAP;
SOME;
DIP { AMOUNT };
PPAIIR; };
};
}

Let’s analyze what we just added. The following lines:

DUP; CADR;
AMOUNT;
ASSERT_CMPGT;
UNPPAIIR;

DUP; CADR; will get the previous highest bid, and AMOUNT; will get the bid amount. Then, the next instruction is very important to understand. ASSERT_CMPGT; will check if the top element on the stack (the current bid) is bigger/higher than the second-to-top element (the previous bid, or the starting price if there is no bid). If it is, we’re all good, it means we have to refund the previous bidder (if any). If it’s not, it means the current bid wasn’t high enough, therefore the contract will fail and the bidder will have to make a higher bid. We then use UNPPAIIR; to decompose the pair tree containing ((option key_hash)), (mutez) and (address) which belong to previous bidder.

Then we use another IF_SOME instruction to check if there is a value (that is (key_hash)) on top of our stack. If there is, it means that we DO have a previous bidder. Therefore, we can execute the code inside our brackets:

IF_SOME
{
IMPLICIT_ACCOUNT;
SWAP;
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}

IMPLICIT_ACCOUNT will return the implicit contract associated to the public key hash on the top of the stack, so we can transfer to the address linked with this public key hash. We SWAP and UNIT again, in order to have all the elements needed for the TRANSFER_TOKENS instruction ready and in the right order. Once we made the transfer (previous bidder was refunded, cool!), we prepare our return value as we did earlier, using NIL operation; SWAP; CONS; succession of instructions.

If we didn’t have any previous buyer, we do nothing and we prepare our return value with the next line following the IF_SOME brackets:

{ DROP; NIL operation; };

Then, we need to update the bid. So far, if there was a previous bidder, he was refunded, and if there wasn’t any previous bidder, we just made sure the new bid was higher than the starting price. We can now update the bidder and his bid. To do so, we use these lines:

DIP
{ SWAP;
SOME;
DIP { AMOUNT };
PPAIIR; };

We protect our top elem by using DIP so what’s inside the brackets won’t have any impact on the element on top of the stack. Inside the brackets, we simply update with the new bidder and his bid, and we compose a pair tree using the macroPPAIIR that we already learned about. Congrats, bidder and his bid were updated :) We’re close to the end.

Let’s build our final return value to end our smart contract, by adding one more line at the end of the contract, a very easy one:

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) address);
code
{ UNPAIR;
SWAP; DUP; CDR;
SENDER;
IFCMPEQ
{
DIP { DROP };
DUP; UNPPAIIR;
IF_SOME
{
DROP;
DIP { CONTRACT unit; ASSERT_SOME };
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{
DROP; DROP; NIL operation };
}
{
DUP; CADR;
AMOUNT;
ASSERT_CMPGT;
UNPPAIIR;
IF_SOME
{
IMPLICIT_ACCOUNT;
SWAP;
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{ DROP; NIL operation; };
DIP
{ SWAP;
SOME;
DIP { AMOUNT };
PPAIIR; };
};
PAIR;
}

PAIR will simply construct our final pair value that we need to return, so we can have what we had in storage at the beginning AND our list of operation.

I have some great news for you… WE’RE DONE… with the simple version of our auction smart contract.

Before we test anything, we’ll update our current version right now to the next step. So far, in our first version, we did not specify a way to end the auction.

How should we end it? With a deadline!

Doing so, the item’s owner will be allowed to collect the funds only AFTER the deadline. On the other hand, a bidder will be allowed to place a bid only BEFORE the deadline. In order to make this happen, we’re going to use a new op-code we’ve never seen before. Cool, right? Let’s do this!

Updating our English auctions with a deadline

To do so, we’re going to use the new op-code NOW that will give us the current time (more precisely, it retrieves the timestamp of the block whose validation triggered this execution). We also have to update the storage to include a timestamp value, which will be the deadline for our auction.

New storage will be:

storage (pair (pair (option key_hash) mutez) (pair address timestamp));

Now, here comes the code. Basically, it is very similar to our first version, so I will only explain what changes. You can create a new file, I called it simpleAuctionTimestamp.tz. It is better to create a new one, so you can compare it with our old auction, I will explain why later.

parameter (key_hash);
storage (pair (pair (option key_hash) mutez) (pair address timestamp));

code
{ UNPAIR;
SWAP; DUP; CDAR;
SENDER;
IFCMPEQ
{
DUP; CDDR;
NOW;
ASSERT_CMPGT;
DIP { DROP };
DUP; UNPPAIPAIR;
IF_SOME
{
DROP;
DIP { CONTRACT unit; ASSERT_SOME };
UNIT;
TRANSFER_TOKENS;
NIL operation; DIIP { DROP }; SWAP; CONS;
}
{
DROP; DROP; DROP; NIL operation };
}
{
DUP; CDDR;
NOW;
ASSERT_CMPLT;
DUP; CADR;
AMOUNT;
ASSERT_CMPGT;
UNPPAIIR;
IF_SOME
{
IMPLICIT_ACCOUNT;
SWAP;
UNIT;
TRANSFER_TOKENS;
NIL operation; SWAP; CONS;
}
{ DROP; NIL operation; };
DIP
{ SWAP;
SOME;
DIP { AMOUNT };
PPAIIR; };
};
PAIR;
}

If you’ve read this new smart contract, you probably noticed a few changes. There are a few minor changes: for example, in the second line inside the code brackets, we used CDAR (instead of CDR) to select the item’s owner. Indeed, as we changed the storage, a few instructions will be slightly modified. I strongly advise you to take a little bit of time and use the Michelson mode on Emacs in order to understand what changed, and feel free to compare with the first version so you can clearly see what is going on, what changed, why and how, …

Let’s now focus on the major changes. We added a deadline, remember?

DUP; CDDR;
NOW;
ASSERT_CMPGT;

DUP; CDDR; will select the deadline. NOW will then give us the timestamp on top of the stack. We can then use ASSERT_CMPGT to check if the deadline has passed or not. If yes, the owner can collect funds, the code continues. If not, the contract fails, meaning the owner can NOT collect funds, which is what we want.

We use something very similar later in the code, when someone makes a bid:

DUP; CDDR;
NOW;
ASSERT_CMPLT;

The first two lines make exactly the same thing as above. However, notice how the next line is a little bit different from the other one? Indeed, here it’s ASSERT_CMPLT, meaning that it will check if the deadline is over or not yet. If it’s not over, the code will continue and the bidder will be able to bid. If it’s over, contract will fail and bidder’s request will be cancelled, as the auction is terminated.

With simply updating a few lines, we were able to change how our auction ends. Now, there is a deadline. Pretty cool, right? ;)

We’re done with the smart contract, we just need to test it, but I will keep that for the next article. As soon as the 4th article is done, I will post the link right here so you can check it.

EDIT: Here is part 4 link: https://medium.com/chain-accelerator/i-tested-tezos-part-4-a06ac5011e64

Let us know which offer you are interested in and apply here => this form.

Don’t forget to follow us on social medias : twitter, telegram, slack

--

--

Mathis Selvi
ON-X Blockchain (Chain-Accelerator)

🛠️ Working on Tezos 👟 Fashion Enthusiast 💿 Music Lover