Liquidity Tutorials — Hands-on Series — Part 5.2

Marty Stumpf
7 min readOct 28, 2019

This is the fifth part of a tutorial series on writing smart contracts in Liquidity. I recommend going through the first, second, third, and fourth parts if you haven’t already.

In this tutorial, we’ll deploy and call the token contract we wrote in the last tutorial via the Tezos client command line interface. I strongly recommend thorough testings of this contract before launching it in the mainnet. Deploying the contract creates a fungible token system running on the Tezos ledger. To recap, the users of this contract include the owner of the token system, the users (account holders) of the tokens, and other contracts. In this tutorial we focus on the owner and the account holders. In the next tutorial, we will write a contract that calls the transferFrom entry point of the token contract.

The owner deploys the contract to start running the token system. The account holders can call the contract to

  • make a transfer via the 1st entry point transfer.
  • make multiple tranfers in one call via the 2nd entry point multiTransfer.
  • make an approval of a transfer amount to another account holder via the 3nd entry point approve.

Deploy

Compile token.liq to token.tz with Liquidity:

liquidity [path to]/token.liq

To deploy the contract, the owner has to specify the initial storage. In the following example, the owner (bootstrap1) initiates a token system with

  • the initial set of accounts: (note that the total number of tokens from all accounts must equal the total supply below.) The owner
  • has a token balance of 10000
  • has an empty allowance map.
  • version number of the token standard is 0.
  • total supply of 10000 tokens.
  • the name of the token is tokenA and the symbol for it is A.
  • the owner’s address is tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx.
tezos-client originate contract tokena for bootstrap1 transferring 1 from bootstrap1 running [path to]/token.tz --init 'Pair {Elt "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" (Pair 10000 {})} (Pair 0 (Pair 10000 (Pair "tokenA" (Pair "A" "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"))))' --burn-cap 10

The output indicates that the token system is initiated, and is memorized as the contract named tokena:

Node is bootstrapped, ready for injecting operations.
Estimated gas: 196408 units (will add 100 for safety)
Estimated storage: 7840 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'ooCCRbCDgrdDikZB4QCPAU1mbjpy2vNXFS7rHbgJ6gx6waS4CMH'
Waiting for the operation to be included...
Operation found in block: BMb3bKVhUoYNwoE67jzzSB99m7voLf2Zgv8onZ7S2GNrxArJ52E (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Fee to the baker: ꜩ0.027547
Expected counter: 1
Gas limit: 196508
Storage limit: 7860 bytes
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ........... -ꜩ0.027547
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.027547
Origination:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
For: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Credit: ꜩ1
Script:

[a whole bunch of scripts]

Initial storage:
(Pair { Elt "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" (Pair 10000 {}) }
(Pair 0 (Pair 10000 (Pair "tokenA" (Pair "A" "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx")))))
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH
Storage size: 7583 bytes
Paid storage size diff: 7583 bytes
Consumed gas: 196408
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ7.583
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.257
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ1
KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH ... +ꜩ1

New contract KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for ooCCRbCDgrdDikZB4QCPAU1mbjpy2vNXFS7rHbgJ6gx6waS4CMH to be included --confirmations 30 --branch BLxBJ1fj3fGCTe1dzyqUDxbqqvoRPgdyF6vb7DJg9Wg5aHuwV82
and/or an external block explorer.
Contract memorized as tokena.

We can check the storage of the contract using the get script storage command:

$tezos-client get script storage for tokena
Pair {}
(Pair 0 (Pair 10000 (Pair "tokenA" (Pair "A" "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"))))

Note that the set of accounts is not shown in the get script storage command, because it is of type big map. To check the accounts, we need to use the get big map value command, and input the address we want to check. E.g., to check the owner's account:

$ tezos-client get big map value for '"tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"' of type address in tokena
Pair 10000 {}

The output indicates that the owner has a balance of 10000 tokens and an empty allowance map, as expected.

Call to transfer

Anyone can call the contract’s transfer entry point to perform a token transfer, if one has enough token balance. For example, the owner can transfer 1000 tokens to bootstrap5 by running:

$ tezos-client transfer 1 from bootstrap1 to tokena --arg '(Left (Pair "tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv" 1000))' --burn-cap 0.01
Node is bootstrapped, ready for injecting operations.
Estimated gas: 157772 units (will add 100 for safety)
Estimated storage: 10 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'oo55saug5Crb5CjAMjAcTYxMLa9FMTtteEW8WwHDQJUSZiXiJeB'
Waiting for the operation to be included...
Operation found in block: BMV1wSgWMri8mUDiZJT2A7SGsvkbnofpPAUEZ7Ph5p8gHpgVLz9 (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Fee to the baker: ꜩ0.016092
Expected counter: 2
Gas limit: 157872
Storage limit: 30 bytes
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ........... -ꜩ0.016092
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.016092
Transaction:
Amount: ꜩ1
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
To: KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH
Parameter: (Left (Pair "tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv" 1000))
This transaction was successfully applied
Updated storage:
(Pair {}
(Pair 0
(Pair 10000 (Pair "tokenA" (Pair "A" 0x000002298c03ed7d454a101eb7022bc95f7e5f41ac78)))))
Storage size: 7593 bytes
Paid storage size diff: 10 bytes
Consumed gas: 157772
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.01
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ1
KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH ... +ꜩ1

The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for oo55saug5Crb5CjAMjAcTYxMLa9FMTtteEW8WwHDQJUSZiXiJeB to be included --confirmations 30 --branch BMb3bKVhUoYNwoE67jzzSB99m7voLf2Zgv8onZ7S2GNrxArJ52E
and/or an external block explorer.

We can check that bootstrap5 has 1000 tokens now:

$ tezos-client get big map value for '"tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU"' of type address in tokena
Pair 1000 {}

Call to make multiple transfers

bootstrap5 (tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv) can call and transfer

  • 20 tokens to bootstrap2(tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN),
  • 30 tokens to bootstrap3(tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU)

using the multiTransfer (2nd) entry point. To choose the 2nd entry point, we start the arg input with (Right (Left .... Then we input the list of address and the amount of tokens to be transferred to that address:

$ tezos-client transfer 1 from bootstrap5 to tokena --arg '(Right (Left {Pair "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" 20 ; Pair "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" 30}))' --burn-cap 0.1
Node is bootstrapped, ready for injecting operations.
Estimated gas: 160396 units (will add 100 for safety)
Estimated storage: 18 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'op29n6VTcoFfPC4C2i4whKw3Y5GwhD5oESiVLd9TkjCu4tTtjPh'
Waiting for the operation to be included...
Operation found in block: BL2v63w6ALSMgdjh3UJXKQNmDbih3PLLfXHvMQfAwpejDkTM3FL (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv
Fee to the baker: ꜩ0.016406
Expected counter: 1
Gas limit: 160496
Storage limit: 38 bytes
Balance updates:
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ........... -ꜩ0.016406
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.016406
Transaction:
Amount: ꜩ1
From: tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv
To: KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH
Parameter: (Right
(Left { Pair "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" 20 ;
Pair "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" 30 }))
This transaction was successfully applied
Updated storage:
(Pair {}
(Pair 0
(Pair 10000 (Pair "tokenA" (Pair "A" 0x000002298c03ed7d454a101eb7022bc95f7e5f41ac78)))))
Storage size: 7611 bytes
Paid storage size diff: 18 bytes
Consumed gas: 160396
Balance updates:
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ... -ꜩ0.018
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ... -ꜩ1
KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH ... +ꜩ1

The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for op29n6VTcoFfPC4C2i4whKw3Y5GwhD5oESiVLd9TkjCu4tTtjPh to be included --confirmations 30 --branch BMV1wSgWMri8mUDiZJT2A7SGsvkbnofpPAUEZ7Ph5p8gHpgVLz9
and/or an external block explorer.

We can check that bootstrap2 has 20, and bootstrap3 has 30 tokens now:

$ $ tezos-client get big map value for '"tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN"' of type address in tokena
Pair 20 {}
$ tezos-client get big map value for '"tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU"' of type address in tokena
Pair 30 {}

For any transfer, if the sender doesn’t have enough tokens, the call fails. For example, if bootstrap3 now transfer 50 tokens to bootstrap2, the call fails, because bootstrap3 only has 30 in the account:

$ tezos-client transfer 1 from bootstrap3 to tokena --arg '(Left (Pair "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" 50))' --burn-cap 0.01
Node is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU
Fee to the baker: ꜩ0
Expected counter: 1
Gas limit: 400000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ1
From: tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU
To: KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH
Parameter: (Left (Pair "tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN" 50))
This operation FAILED.

Runtime error in contract KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH:
001: { parameter
...
383: DIP { DROP ; DROP ; DROP ; DROP } } }
At line 89 characters 17 to 25,
script reached FAILWITH instruction
with (Pair "Not enough tokens for transfer" 30)
Fatal error:
transfer simulation failed

Call to approve

One can call the third entry point (approve) to approve an amount for contracts to transfer tokens on their behalf. For example, bootstrap5 can approve bootstrap3 100 tokens:

$ tezos-client transfer 1 from bootstrap5 to tokena --arg '(Right (Right (Left (Pair "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" 100))))' --burn-cap 0.032
Node is bootstrapped, ready for injecting operations.
Estimated gas: 156430 units (will add 100 for safety)
Estimated storage: 32 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'onzpxq8Xh4vgqpbWLWPABLJHY9ZYs1ThMuX432kVMUMtVRZK8ZS'
Waiting for the operation to be included...
Operation found in block: BLob7fmGfLrYdGMqcQx5e3rJj3uWYPSnRqJLcHwQgogXVzhrn9c (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv
Fee to the baker: ꜩ0.015961
Expected counter: 2
Gas limit: 156530
Storage limit: 52 bytes
Balance updates:
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ........... -ꜩ0.015961
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.015961
Transaction:
Amount: ꜩ1
From: tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv
To: KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH
Parameter: (Right (Right (Left (Pair "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU" 100))))
This transaction was successfully applied
Updated storage:
(Pair {}
(Pair 0
(Pair 10000 (Pair "tokenA" (Pair "A" 0x000002298c03ed7d454a101eb7022bc95f7e5f41ac78)))))
Storage size: 7643 bytes
Paid storage size diff: 32 bytes
Consumed gas: 156430
Balance updates:
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ... -ꜩ0.032
tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv ... -ꜩ1
KT19mTp6Qa8idAZxEBRnx1CoD8qyhDBwTAPH ... +ꜩ1

The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for onzpxq8Xh4vgqpbWLWPABLJHY9ZYs1ThMuX432kVMUMtVRZK8ZS to be included --confirmations 30 --branch BL2v63w6ALSMgdjh3UJXKQNmDbih3PLLfXHvMQfAwpejDkTM3FL
and/or an external block explorer.

Doing so updates the allowance map of bootstrap5. We can see the effect by checking its big map value:

$ tezos-client get big map value for '"tz1ddb9NMYHZi5UzPdzTZMYQQZoMub195zgv"' of type address in tokena
Pair 950 { Elt 0x0000dac9f52543da1aed0bc1d6b46bf7c10db7014cd6 100 }

The output shows that bootstrap5 now has an allowance map with an entry of bootstrap3’s address (in optimized form) and 100 approved tokens. Note that the call does not check that the approver has balance no less than the approved amount. We’ll make use of this approved amount in the next tutorial!

Woohoo! We just launched a token system on Tezos! And we transferred some tokens between accounts, and approved/authorized some token transfers.

In the next tutorial, we will write a contract that make use of the transferFrom entry point to swap tokens between two token systems. Stay tuned!

--

--

Marty Stumpf

(Functional) Software engineer from BC, Canada. thealmarty.com, twitter: @MartyStumpf.