Nomadic Labs
Nomadic Labs
Babylon update instructions for delegation wallet developers

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:

  1. 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.

  2. 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.


Receive Updates

ATOM

Contacts