# Chapter 9
Let's take a little break after a long chapter about pairs and before another long chapter about 3 essential types of Michelson: maps, big maps and sets.
In this chapter, we will learn more about less common operations that you can accomplish in a Michelson smart contract. First of all, we will check how to convert types between each other, which is called "casting". Then, we will have a look at the different possibilities to compare values and we will finish with a talk about bytes and their use cases.
# Casting types
One particularly pesky headache of using a strongly typed language is the conversion of values from one type to another. Obviously, some conversions are forbidden or impossible, it wouldn't make any sense to convert an integer to a list. However, some conversions may seem practical: after all, a value that's an integer, a natural number or a mutez is a numeric value and in some situations, it could be more convenient to change its type.
Imagine a contract with a storage that's a value of type nat
, you receive 2 ints, you add them, get a new int
and want to save it in the storage. But you can't, because the storage needs a nat
value. Or vice-versa, the storage is an int
value, you add 2 nats together and want to get an int
back. In this situations, Michelson offers two instructions: ABS
will return the absolute value of a number (i.e a nat
value) and INT
will change a nat
value into an int
.
storage int ;
parameter nat ;
code {
CAR ;
INT ;
NIL operation ;
PAIR
} ;
RUN %default 9 8 ;
stdout
storage: updated parameter: updated code: updated BEGIN %default / _ => (9 * 8) CAR / (9 * 8) => 9 INT / 9 => 9 NIL / _ => [] PAIR / [] : 9 => ([] * 9) END %default / ([] * 9) => _
storage nat ;
parameter int ;
code {
CAR ;
ABS ;
NIL operation ;
PAIR
} ;
RUN %default 9 8 ;
stdout
storage: updated parameter: updated code: updated BEGIN %default / _ => (9 * 8) CAR / (9 * 8) => 9 ABS / 9 => 9 NIL / _ => [] PAIR / [] : 9 => ([] * 9) END %default / ([] * 9) => _
You may also want to check if the int
value you have in the stack is above zero before turning it into a nat
value (because ABS
will remove the negative sign of any integer). If it's the case, you can use ISNAT
. If the int
value is equal or greater than zero, the instruction returns (option nat) (Some value)
. If the int
value is less than zero, the instruction returns (option nat) (None)
:
parameter int ;
storage nat ;
code {
CAR ;
ISNAT ;
IF_SOME
{
NIL operation ;
PAIR
}
{ FAIL }
} ;
RUN %default 9 8 ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => (9 * 8) CAR / (9 * 8) => 9 ISNAT / 9 => 9? IF_NONE / 9? => 9 NIL / _ => [] PAIR / [] : 9 => ([] * 9) END %default / ([] * 9) => _
Now if you change 9
for -9
, you will see the contract fail as the negative integer triggers the FAIL
instruction.
You can also use "hacks" to cast some values. One of the most frequently encountered is a hack based on the subtraction. Let's say you need to cast a timestamp
into an int
. There is no instruction for that. However, if you subtract a timestamp
from another timestamp
, you get an int
. You can then create a timestamp
equal to zero and subtract it from the timestamp
you want to cast into an int
:
parameter unit ;
storage int ;
code {
DROP ;
PUSH timestamp 0 ;
NOW ;
SUB ;
NIL operation ;
PAIR ;
} ;
RUN %default Unit 0 ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => (Unit * 0) DROP / (Unit * 0) => _ PUSH / _ => 0 NOW / _ => 0 SUB / 0 : 0 => 0 NIL / _ => [] PAIR / [] : 0 => ([] * 0) END %default / ([] * 0) => _
Now you have an int
whose value is the same as the provided timestamp
!
With a little bit of imagination, it is also possible to cast other values, for example a mutez
into a nat
! This time, we can use EDIV
to turn a mutez
value into a nat
value. When you divide two mutez
values together, you get an optional value with a pair that contains a nat
(the result) in the left field. When you divide a value by 1
, the result will be the same value, so if you divide mutez 22
by mutez 1
, the result will be nat 22
as illustrated below:
parameter unit ;
storage nat ;
code {
DROP ;
PUSH mutez 1 ;
PUSH mutez 5566778899 ;
EDIV ;
IF_SOME
{
CAR ;
NIL operation ;
PAIR ;
}
{ FAIL }
} ;
RUN %default Unit 0 ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => (Unit * 0) DROP / (Unit * 0) => _ PUSH / _ => 0.000001 PUSH / _ => 5566.778899 EDIV / 5566.778899 : 0.000001 => (5566778899 * 0)? IF_NONE / (5566778899 * 0)? => (5566778899 * 0) CAR / (5566778899 * 0) => 5566778899 NIL / _ => [] PAIR / [] : 5566778899 => ([] * 5566778899) END %default / ([] * 5566778899) => _
Finally, it is also possible to cast a contract to its address and an adress to a contract. Observe the following example:
parameter (contract unit) ;
storage address ;
code {
CAR ;
ADDRESS ;
NIL operation ;
PAIR
} ;
RUN %default "KT1X3zxdTzPB9DgVzA3ad6dgZe9JEamoaeRy" "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => (KT1X3z…eRy%default * tz1VSU…cjb) CAR / (KT1X3z…eRy%default * tz1VSU…cjb) => KT1X3z…eRy%default ADDRESS / KT1X3z…eRy%default => KT1X3z…eRy NIL / _ => [] PAIR / [] : KT1X3z…eRy => ([] * KT1X3z…eRy) END %default / ([] * KT1X3z…eRy) => _
In this example, we turn a (contract unit)
type into an address. It is also possible to do the opposite:
storage (contract unit) ;
parameter address ;
code {
CAR ;
CONTRACT unit ;
IF_SOME
{
NIL operation ;
PAIR
}
{ FAIL }
} ;
RUN %default "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" "KT1X3zxdTzPB9DgVzA3ad6dgZe9JEamoaeRy" ;
stdout
storage: updated parameter: updated code: updated BEGIN %default / _ => (tz1VSU…cjb * KT1X3z…eRy%default) CAR / (tz1VSU…cjb * KT1X3z…eRy%default) => tz1VSU…cjb CONTRACT: skip type checking for tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb CONTRACT / tz1VSU…cjb => tz1VSU…cjb%default? IF_NONE / tz1VSU…cjb%default? => tz1VSU…cjb%default NIL / _ => [] PAIR / [] : tz1VSU…cjb%default => ([] * tz1VSU…cjb%default) END %default / ([] * tz1VSU…cjb%default) => _
If you remember from the Chapter 5, this step of turning an address into a contract address of type (contract unit)
is also a necessary step of transferring tokens.
# Working with bytes
Although a rather uncommon type, Michelson allows us to manipulate bytes. All the instructions available for strings are also available for bytes (namely COMPARE
, CONCAT
, SIZE
and SLICE
) + two extra instructions to pack (value -> bytes) and unpack (bytes -> value) bytes. Most of the times you will have to use bytes is when you will use cryptographic instructions, let's check an example:
storage bytes ;
parameter nat ;
code {
CAR ;
PACK ;
SHA256 ;
NIL operation ;
PAIR ;
} ;
RUN %default 12345 0x ;
stdout
storage: updated parameter: updated code: updated BEGIN %default / _ => (12345 * 0x) CAR / (12345 * 0x) => 12345 PACK / 12345 => 0x0500b9c001 SHA256 / 0x0500b9c001 => 0xcea356435ff4eee1981f2e2ddc81bee203b3af767c48f97fd84b64fa767d2285 NIL / _ => [] PAIR / [] : 0xcea356435ff4eee1981f2e2ddc81bee203b3af767c48f97fd84b64fa767d2285 => ([] * 0xcea356435ff4eee1981f2e2ddc81bee203b3af767c48f97fd84b64fa767d2285) END %default / ([] * 0xcea356435ff4eee1981f2e2ddc81bee203b3af767c48f97fd84b64fa767d2285) => _
This simple contract takes a value of type nat
, converts it to bytes
with the PACK
instruction, gets its SHA256 hash and saves it in the storage. We can use the result of the PACK
instruction to write a contract that does the inverse:
storage nat ;
parameter bytes ;
code {
CAR ;
UNPACK nat ;
IF_SOME
{ NIL operation ; PAIR ; }
{ FAIL }
} ;
RUN %default 0x0500b9c001 0 ;
stdout
storage: updated parameter: updated code: updated BEGIN %default / _ => (0x0500b9c001 * 0) CAR / (0x0500b9c001 * 0) => 0x0500b9c001 UNPACK / 0x0500b9c001 => 12345? IF_NONE / 12345? => 12345 NIL / _ => [] PAIR / [] : 12345 => ([] * 12345) END %default / ([] * 12345) => _
At the end of this contract, we get back our nat
value of 12345
😃
Now, let's try to use another operation on bytes:
parameter (pair nat nat) ;
storage int ;
code {
CAR ;
UNPAIR ;
PACK ;
SWAP ;
PACK ;
COMPARE ;
NIL operation ;
PAIR ;
} ;
RUN %default (Pair 44 33) 0 ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => ((44 * 33) * 0) CAR / ((44 * 33) * 0) => (44 * 33) UNPAIR / (44 * 33) => 44 : 33 PACK / 44 => 0x05002c SWAP / 0x05002c : 33 => 33 : 0x05002c PACK / 33 => 0x050021 COMPARE / 0x050021 : 0x05002c => -1 NIL / _ => [] PAIR / [] : -1 => ([] * -1) END %default / ([] * -1) => _
It is also possible to compare two byte
values with the COMPARE
instruction. Just like when you compare two comparable values, the instruction returns -1
is the first one is less than the second one, 0
if they are both equal and 1
if the first one is greater than the second one. Try to change the values in the pair passed in the parameter to see it working.
We can also extend our previous example and compare the sizes of the bytes:
parameter (pair nat nat) ;
storage int ;
code {
CAR ;
UNPAIR ;
PACK ;
SIZE ;
SWAP ;
PACK ;
SIZE ;
COMPARE ;
NIL operation ;
PAIR ;
} ;
RUN %default (Pair 44 33) 0 ;
stdout
parameter: updated storage: updated code: updated BEGIN %default / _ => ((44 * 33) * 0) CAR / ((44 * 33) * 0) => (44 * 33) UNPAIR / (44 * 33) => 44 : 33 PACK / 44 => 0x05002c SIZE / 0x05002c => 3 SWAP / 3 : 33 => 33 : 3 PACK / 33 => 0x050021 SIZE / 0x050021 => 3 COMPARE / 3 : 3 => 0 NIL / _ => [] PAIR / [] : 0 => ([] * 0) END %default / ([] * 0) => _
As you can see from the 0
returned by the COMPARE
instruction, the two bytes we created have the same size.
NOTE
not all types can be packed, if you want to know which ones are and are not, you can read the Michelson reference from Nomadic Labs and check in the PA
column.
← Chapter 8 Chapter 10 →