Introduction
Tezos wallets usually feature management of scriptless originated (aka KT1) accounts used to delegate tokens.
This document details the steps needed for wallet developers to update their applications in anticipation to the breaking changes in the Babylon protocol update. See also cryptium’s migration guidelines and Babylon’s documentation for more technical details on the Babylon update.
The Babylon protocol update brings two big changes to the way delegation can be implemented. First, implicit (aka tz) accounts can now directly delegate their tokens (see relevant documentation’s section). Second, the scriptless KT1 accounts, whose main purpose in Athens were delegation, are replaced by smart contracts whose address is unchanged and script is manager.tz.
The “delegation” operation, which was restricted to KT1 accounts in Athens, is now restricted to tz implicit accounts in Babylon. To change or withdraw the delegate of a smart contract in Babylon, the “SET_DELEGATE” Michelson instruction has to be used.
Moreover, smart contract in Babylon cannot be the source of a transaction signed by their manager; the source of a transaction is always a tz implicit account. To spend the tokens of a smart contract running the manager.tz script, its manager sends a 0tz transaction to the smart contract with an argument describing the operation that the smart contract should perform.
Concretely, this means that wallets can be updated in one of the following ways:
-
Remove support for KT1 delegation accounts (pros: simpler, more future proof and aligned with the new accounts taxonomy).
Since tz accounts in Babylon can be delegated, KT1 accounts are not needed anymore for a wallet to feature delegation.
The Babylon’s “delegation” operation on tz accounts is very similar to the Athens’ “delegation” operation on KT1 accounts so updating the wallet should be relatively easy in this case. However, migrating existing accounts would be needed. To learn how to transfer the tokens of a KT1 account to an implicit account (its manager for example), see next section. -
Interact with the manager.tz script (pro: user addresses do not change)
The second option is to keep all tokens and delegations as they are and adapt the wallet code to interact with the manager.tz smart contract script that all currently scriptless KT1 accounts will run after the Babylon migration.
In this case all operations related to KT1 acconts need to be adapted but no account migration is needed. The following sections detail the changes in the operations.
Transfer from a manager.tz smart contract to an implicit (tz) account
To transfer <amount>
tokens from a manager.tz smart contract to a tz
account whose key hash is <destination>
, we need to call the smart
contract on its “%do” entrypoint with a lambda that builds the desired
transfer as argument. The
migration instructions from Cryptium Labs
contain the required Michelson code:
{ DROP ; NIL operation ; PUSH key_hash <destination> ; IMPLICIT_ACCOUNT ; PUSH mutez <amount> ; UNIT ; TRANSFER_TOKENS ; CONS }
The fees, gas limit, and storage limit for this call can be set to the following values:
-
fees: 2941 μꜩ
-
gas limit: 26283
-
storage limit: 0 byte
The amount of the transaction to the smart contract must be 0ꜩ.
Before calling the smart contract, we need to compute and sign the
binary representation of the transfer operation. To get a description
of the binary format, we use a Babylon command line client as follows:
tezos-client describe unsigned operation
. To only get what has
changed in Babylon, we can also read
the online documentation.
The byte sequence of the operation we want can be decomposed as follows:
<32 bytes>: block hash of the current head block [(see below)](#getting-information-to-craft-the-operations)
0x6c: Transaction tag (108)
<21 bytes>: key hash of the source of the transaction (must be the manager of the smart contract)
0xfd16: fees (2941 μꜩ)
<counter>: counter associated to the manager tz account [(see below)](#getting-information-to-craft-the-operations)
0xabcd01: gas_limit (26283)
0x00: storage_limit (0)
0x00: amount (0ꜩ)
<22 bytes>: the address of the smart contract (must start with the byte 0x01 and end with the byte 0x00)
0xff (or any other non-null byte): presence flag for the parameters (entrypoint and argument)
0x02: tag of the "%do" entrypoint
<4 bytes>: length of the argument
0x02: Michelson sequence
<4 bytes>: length of the sequence
0x0320: DROP
0x053d: NIL
0x036d: operation
0x0743: PUSH
0x035d: key_hash
0x0a: Byte sequence
0x00000015: Length of the sequence (21 bytes)
<21 bytes>: <destination>
0x031e: IMPLICIT_ACCOUNT
0x0743: PUSH
0x036a: mutez
<amount>: Amout to be transfered
0x034f: UNIT
0x034d: TRANSFER_TOKENS
0x031b: CONS
Once signed, this operation can be sent to the node by invoking the following RPC:
/chains/main/blocks/head/helpers/preapply/operations
[ { "protocol": "PsBABY5HQTSkA4297zNHfsZNKtxULfL18y95qb3m53QJiXGmrbU",
"branch": "<block hash of the head block in Base58 [(see below)](#getting-information-to-craft-the-operations)>",
"contents":
[ { "kind": "transaction",
"source": "<manager key hash>", "fee": "2941",
"counter": "<counter of the manager [(see below)](#getting-information-to-craft-the-operations)>", "gas_limit": "26283", "storage_limit": "0",
"amount": "0",
"destination": "<address of the smart contract in Base58 (must start with "KT1")>",
"parameters":
{ "entrypoint": "do",
"value":
[ { "prim": "DROP" },
{ "prim": "NIL", "args": [ { "prim": "operation" } ] },
{ "prim": "PUSH",
"args":
[ { "prim": "key_hash" },
{ "bytes":
"<destination (in hexadecimal without the leading '0x')>" } ] },
{ "prim": "IMPLICIT_ACCOUNT" },
{ "prim": "PUSH",
"args":
[ { "prim": "mutez" }, { "int": "<amount>" } ] },
{ "prim": "UNIT" }, { "prim": "TRANSFER_TOKENS" },
{ "prim": "CONS" } ] } } ],
"signature":
"<signature>" } ]
Transfer from a manager.tz smart contract to another smart contract
Transferring to a smart contract is very similar. The main difference
is that we need to check the smart contract type in Michelson using
the expensive CONTRACT
instruction.
The lambda to pass as parameter to the manager.tz smart contract to
make it send <amount>
tokens to the smart contract at address
<destination>
, calling it on the entrypoint <entrypoint>
of type
<ty>
with the parameter <param>
is again given in the migration
instructions from Cryptium
Labs:
{ DROP ; NIL operation ; PUSH address <destination> ; CONTRACT %<entrypoint> <ty> ; ASSERT_SOME ; PUSH mutez <amout> ; PUSH <ty> <param> ; TRANSFER_TOKENS ; CONS }
Note that the entrypoint must be omitted if it is default
(that is,
you should write CONTRACT <ty>
instead of CONTRACT %default <ty>
).
If <ty>
is unit
, the instruction PUSH unit <param>
can be
replaced by the slightly cheaper UNIT
instruction.
The fees and gas limit will depend on the size of the
parameter. In the particular case of a transfer to another manager.tz smart
contract, the gas limit should be set to 44725
.
Storage limit can still be set to 0 bytes.
The amount of the transaction to the smart contract must again be 0ꜩ.
The byte sequence of the operation we want can be decomposed as follows:
<32 bytes>: block hash of the current head block [(see below)](#getting-information-to-craft-the-operations)
0x6c: Transaction tag (108)
<21 bytes>: key hash of the source of the transaction (must be the manager of the smart contract)
<fees>
<counter>: counter associated to the manager tz account [(see below)](#getting-information-to-craft-the-operations)
<gas_limit>
0x00: storage_limit (0)
0x00: amount (0ꜩ)
<22 bytes>: the address of the manager.tz smart contract (must start with the byte 0x01 and end with the byte 0x00)
0xff (or any other non-null byte): presence flag for the parameters (entrypoint and argument)
0x02: tag of the "%do" entrypoint
<4 bytes>: length of the argument
0x02: Michelson sequence
<4 bytes>: length of the sequence
0x0320: DROP
0x053d: NIL
0x036d: operation
0x0743: PUSH
0x036e: address
0x0a: byte sequence
0x00000016: Length of the sequence (22 bytes)
<22 bytes>: <destination> (must start with the byte 0x01 and end with the byte 0x00)
(if the entrypoint is "default"
0x0555: CONTRACT
<ty>
else
0x0655: CONTRACT
<ty>
<entrypoint>
)
0x0200000015072f02000000090200000004034f03270200000000: ASSERT_SOME (unfolded as { IF_NONE { { UNIT ; FAILWITH } } {} })
0x0743: PUSH
0x036a: mutez
<amount>
(if <ty> is unit
0x4f: UNIT
else
0x0743: PUSH
<ty>
<param>
)
0x034d: TRANSFER_TOKENS
0x031b: CONS
Setting the delegate
Setting a delegate for a manager.tz contract is very similar; only the lambda needs to be changed to use the Michelson SET_DELEGATE
instruction.
The lambda to send to the smart contract is
{ DROP ; NIL operation ; PUSH key_hash <delegate> ; SOME ; SET_DELEGATE ; CONS }
The binary version of the operation is:
<32 bytes>: block hash of the current head block [(see below)](#getting-information-to-craft-the-operations)
0x6c: Transaction tag (108)
<21 bytes>: key hash of the source of the transaction (must be the manager of the smart contract)
0xfd16: fees (2941 μꜩ)
<counter>: counter associated to the manager tz account [(see below)](#getting-information-to-craft-the-operations)
0xabcd01: gas_limit (26283)
0x00: storage_limit (0)
0x00: amount (0ꜩ)
<22 bytes>: the address of the smart contract (must start with the byte 0x01 and end with the byte 0x00)
0xff (or any other non-null byte): presence flag for the parameters (entrypoint and argument)
0x02: tag of the "%do" entrypoint
0x0000002f: length of the argument (47 bytes)
0x02: Michelson sequence
0x0000002a: length of the sequence (42 bytes)
0x0320: DROP
0x053d: NIL
0x036d: operation
0x0743: PUSH
0x035d: key_hash
0x0a: Byte sequence
0x00000015: Length of the sequence (21 bytes)
<21 bytes>: <destination>
0x0346: SOME
0x034e: SET_DELEGATE
0x031b: CONS
Removing the delegate
Similarly, the delegate of the smart contract can be removed using the following lambda:
{ DROP ; NIL operation ; NONE key_hash ; SET_DELEGATE ; CONS }
The binary version of the operation is:
<32 bytes>: block hash of the current head block [(see below)](#getting-information-to-craft-the-operations)
0x6c: Transaction tag (108)
<21 bytes>: key hash of the source of the transaction (must be the manager of the smart contract)
0xfd16: fees (2941 μꜩ)
<counter>: counter associated to the manager tz account [(see below)](#getting-information-to-craft-the-operations)
0xabcd01: gas_limit (26283)
0x00: storage_limit (0)
0x00: amount (0ꜩ)
<22 bytes>: the address of the smart contract (must start with the byte 0x01 and end with the byte 0x00)
0xff (or any other non-null byte): presence flag for the parameters (entrypoint and argument)
0x02: tag of the "%do" entrypoint
0x00000013: length of the argument (19 bytes)
0x02: Michelson sequence
0x0000000e: length of the sequence (14 bytes)
0x0320: DROP
0x053d: NIL
0x036d: operation
0x053e: NONE
0x035d: key_hash
0x034e: SET_DELEGATE
0x031b: CONS
Origination of the manager script
To originate a smart contract running the manager.tz script, we need to send an “origination” operation with the following data:
-
“source”, “counter”, “balance”, and “delegate” with the same values as a KT1 account origination in Athens
-
“gas_limit”: at least 15555 gas units
-
“storage_limit”: at least 489 bytes
-
“storage”: the (Michelson representation of the) key_hash of the manager
-
“script”: the manager script
-
JSon version:
{ "code":
[ { "prim": "parameter",
"args":
[ { "prim": "or",
"args":
[ { "prim": "lambda",
"args":
[ { "prim": "unit" },
{ "prim": "list",
"args": [ { "prim": "operation" } ] } ],
"annots": [ "%do" ] },
{ "prim": "unit", "annots": [ "%default" ] } ] } ] },
{ "prim": "storage", "args": [ { "prim": "key_hash" } ] },
{ "prim": "code",
"args":
[ [ [ [ { "prim": "DUP" }, { "prim": "CAR" },
{ "prim": "DIP",
"args": [ [ { "prim": "CDR" } ] ] } ] ],
{ "prim": "IF_LEFT",
"args":
[ [ { "prim": "PUSH",
"args":
[ { "prim": "mutez" },
{ "int": "0" } ] },
{ "prim": "AMOUNT" },
[ [ { "prim": "COMPARE" },
{ "prim": "EQ" } ],
{ "prim": "IF",
"args":
[ [],
[ [ { "prim": "UNIT" },
{ "prim": "FAILWITH" } ] ] ] } ],
[ { "prim": "DIP",
"args": [ [ { "prim": "DUP" } ] ] },
{ "prim": "SWAP" } ],
{ "prim": "IMPLICIT_ACCOUNT" },
{ "prim": "ADDRESS" },
{ "prim": "SENDER" },
[ [ { "prim": "COMPARE" },
{ "prim": "EQ" } ],
{ "prim": "IF",
"args":
[ [],
[ [ { "prim": "UNIT" },
{ "prim": "FAILWITH" } ] ] ] } ],
{ "prim": "UNIT" }, { "prim": "EXEC" },
{ "prim": "PAIR" } ],
[ { "prim": "DROP" },
{ "prim": "NIL",
"args": [ { "prim": "operation" } ] },
{ "prim": "PAIR" } ] ] } ] ] }
- Michelson version:
parameter
(or
(lambda %do unit (list operation))
(unit %default));
storage key_hash;
code
{ UNPAIR ;
IF_LEFT
{ # 'do' entrypoint
# Assert no token was sent:
# to send tokens, the default entry point should be used
PUSH mutez 0 ;
AMOUNT ;
ASSERT_CMPEQ ;
# Assert that the sender is the manager
DUUP ;
IMPLICIT_ACCOUNT ;
ADDRESS ;
SENDER ;
ASSERT_CMPEQ ;
# Execute the lambda argument
UNIT ;
EXEC ;
PAIR ;
}
{ # 'default' entrypoint
DROP ;
NIL operation ;
PAIR ;
}
};
- Binary version (including the initial 4-byte size):
0x000000c602000000c105000764085e036c055f036d0000000325646f046c000000082564656661756c740501035d050202000000950200000012020000000d03210316051f02000000020317072e020000006a0743036a00000313020000001e020000000403190325072c020000000002000000090200000004034f0327020000000b051f02000000020321034c031e03540348020000001e020000000403190325072c020000000002000000090200000004034f0327034f0326034202000000080320053d036d0342
The binary format for this operation is as follows:
<32 bytes>: block hash of the current head block [(see below)](#getting-information-to-craft-the-operations)
0x6d: Origination tag (108)
<21 bytes>: key hash of the source of the transaction (must be the hash of the key that will sign the operation, not necesseraly the manager)
0x8510: fees (2053 μꜩ)
<counter>: counter associated to the manager tz account [(see below)](#getting-information-to-craft-the-operations)
0xbe7a: gas_limit (15678)
0xfd03: storage_limit (509 bytes)
<init balance>: initial balance in μꜩ
0xff (or any other non-null byte): presence flag for the delegate
<21 bytes>: key hash of the initial delegate
0x000000c602000000c105000764085e036c055f036d0000000325646f046c000000082564656661756c740501035d050202000000950200000012020000000d03210316051f02000000020317072e020000006a0743036a00000313020000001e020000000403190325072c020000000002000000090200000004034f0327020000000b051f02000000020321034c031e03540348020000001e020000000403190325072c020000000002000000090200000004034f0327034f0326034202000000080320053d036d0342: manager script
0x0000001a: storage length (26 bytes)
0x0a: storage is a byte sequence
0x00000015: length of the sequence (21 bytes)
<21 bytes>: storage, this is the key hash of the smart contract's manager
We can then sign this sequence of bytes with the sender’s private key and send the json version of the operation through an RPC
/chains/main/blocks/head/helpers/preapply/operations
[ { "protocol": "PsBABY5HQTSkA4297zNHfsZNKtxULfL18y95qb3m53QJiXGmrbU",
"branch": "<block hash of the head block in Base58 [(see below)](#getting-information-to-craft-the-operations)>",
"contents":
[ { "kind": "origination",
"source": "<sender>", "fee": "2053",
"counter": "<counter [(see below)](#getting-information-to-craft-the-operations)>", "gas_limit": "15678",
"storage_limit": "509", "balance": "<balance>",
"script":
{ "code":
[ { "prim": "parameter",
"args":
[ { "prim": "or",
"args":
[ { "prim": "lambda",
"args":
[ { "prim": "unit" },
{ "prim": "list",
"args": [ { "prim": "operation" } ] } ],
"annots": [ "%do" ] },
{ "prim": "unit", "annots": [ "%default" ] } ] } ] },
{ "prim": "storage", "args": [ { "prim": "key_hash" } ] },
{ "prim": "code",
"args":
[ [ [ [ { "prim": "DUP" }, { "prim": "CAR" },
{ "prim": "DIP",
"args": [ [ { "prim": "CDR" } ] ] } ] ],
{ "prim": "IF_LEFT",
"args":
[ [ { "prim": "PUSH",
"args":
[ { "prim": "mutez" },
{ "int": "0" } ] },
{ "prim": "AMOUNT" },
[ [ { "prim": "COMPARE" },
{ "prim": "EQ" } ],
{ "prim": "IF",
"args":
[ [],
[ [ { "prim": "UNIT" },
{ "prim": "FAILWITH" } ] ] ] } ],
[ { "prim": "DIP",
"args": [ [ { "prim": "DUP" } ] ] },
{ "prim": "SWAP" } ],
{ "prim": "IMPLICIT_ACCOUNT" },
{ "prim": "ADDRESS" },
{ "prim": "SENDER" },
[ [ { "prim": "COMPARE" },
{ "prim": "EQ" } ],
{ "prim": "IF",
"args":
[ [],
[ [ { "prim": "UNIT" },
{ "prim": "FAILWITH" } ] ] ] } ],
{ "prim": "UNIT" }, { "prim": "EXEC" },
{ "prim": "PAIR" } ],
[ { "prim": "DROP" },
{ "prim": "NIL",
"args": [ { "prim": "operation" } ] },
{ "prim": "PAIR" } ] ] } ] ] } ],
"storage":
{ "bytes": "<manager's key hash in hexadecimal without the leading 0x>" } } } ],
"signature": "<signature>" } ]
Getting informations to craft the operations
To craft the operations described in the previous sections, you will need to query a synchronised node as follow:
-
The block hash of the current head block is obtained from the RPC
GET /chains/main/blocks/head/hash
-
the counter
<counter>
associated to a<tz...>
account is obtained from the RPCGET /chains/main/blocks/head/context/contracts/<tz...>/counter
.
Gas cost recap
Due to the changes brought by Babylon, gas costs will change and will probably change again in the future. Ideally, the gas cost of a transaction should be queried from a trusted node or indexer. For convenience, we list recommended gas costs for the most important cases below.
- implicit (tz) to implicit: 10307
- implicit to manager.tz: 15385
- manager.tz to implicit: 26283
- manager.tz to manager.tz: 44725
Conclusion
The Babylon update simplifies the organisation of Tezos accounts by removing the “delegatable” and “spendable” flags together with the “manager” address of KT1 accounts and smart contracts. The interaction of these flags with smart contract codes were sometimes difficult to grasp. All modifications to KT1 states (balance, delegate, or storage) now have to go through the smart contract code. These breaking changes impact the development of wallet application but come with the new feature of delegating tz account which was developed to simplify the delegation workflow since originating a KT1 account is no more necessary to delegate.