How to update the delegation feature of your wallet for 005 (aka. Babylon)

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 RPC GET /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.