Hot on the trail of better Ticket transfer UX

Tezos tickets have been introduced since the update Edo based on having fungible and non-fungible tokens as a 1st-class citizen on Tezos.

Marigold, Nomadic Labs, and TriliTech have contributed many ticket improvements. From the Mumbai protocol, it is possible for implicit accounts and contracts to own tickets. We can also transfer tickets between any two implicit accounts and contracts. That brought the potential of realizing protocol-level ownership of digital assets and tokens to Tezos.

Nevertheless, from the user’s point of view, a few missing parts could still be addressed to have a seamless user experience. Here, based on a proposal by Michael Zaikin from the Baking Bad team, we plan to include the following improvements to the coming protocols.

Operation for sending tickets

Our ticket transfer process currently relies on the transfer_ticket manager operation, which accepts only a single ticket as its argument. For example, an operation that transfers a ticket with a string content "foo" and amount 3 to an entrypoint receive_ticket would look like this:

{
  "kind": "transfer_ticket",
  "source": "tz1_source",
  "ticket_contents": { "string": "foo" },
  "ticket_ty": { "prim": "string" },
  "ticket_ticketer": "KT1_ticketer",
  "ticket_amount": "3",
  "destination": "KT1_contract",
  "entrypoint": "default",
  ...
}

The restriction of only being able to send a single ticket in an operation is problematic in scenarios where we need to include additional data along with the ticket. For instance, when we are sending tickets to an L2 proxy contract, we want to have, in addition to the tickets, the L2 address to which we are depositing.

Moreover, from an engineering standpoint, it’s unfortunate that we depend on a specific manager operation (transfer_ticket ) for ticket transfers, even in cases where we can achieve the same result by leveraging the more general transaction manager operation.

The direction that we propose is to generalize the current transaction operation so that users can use it to transfer tickets along with any required data from the implicit account to the smart contract directly, like this:

{
  "kind": "transaction",
  "source": "tz1_source",
  "destination": "KT1_contract",
  "parameters": {
    "entrypoint": "receive_ticket",
    "value": {
      "prim": "Pair",
      "args": [
        { "string": "KT1_ticketer" }, // Ticketer
        { "prim": "Pair", "args": [ 
           {"string": "foo"}, // Ticket content
           { "int": "3" }     // Ticket amount
          ]}]}
  }
  ...
}

Security Concern

Enabling users to transfer tickets via transaction brings a security concern that users might not be aware of when they are sending tickets out of their account when signing operations.

In Michelson, tickets are represented as nested pairs. This is why the parameter in the above transaction, which is transferring a ticket, looks just like a nested pair:

 "prim": "Pair",
 "args": [
   { "string": "KT1_ticketer" },
   { "prim": "Pair", "args": [ {"string": "foo"}, { "int": "3" } ] }]

Due to the inability to differentiate tickets from nested pairs just by looking at the transaction payload, the following attack scenario emerges:

  1. You visit an airdrop website to claim some tokens.
  2. The DApp prompts you to sign a transaction with a payload with a bunch of Pair s.
  3. You sign the transaction without much thought about the Pair s.
  4. Actually, the Pair s you signed represented tickets! Now all the tickets you own have been drained.

Proposed Solution: New Michelson Ticket constructor

In order to reduce such risk, we propose to introduce a new Michelson constructor, Ticket , which would look like this:

  "prim": "Ticket",
  "args": [
    { "string": "KT1_ticketer" }, // Ticketer
    { "string": "foo" },          // Ticket content
    { "int": "3" },               // Ticket amount
    { "string": "string" }        // Type of the ticket.
   ]

Using this constructor, a transaction that involves a ticket transfer would look like this:

{
  "kind": "transaction",
  "source": "tz1_source",
  "destination": "KT1_contract",
  "parameters": {
    "entrypoint": "receive_ticket",
    "value": {
      "prim": "Ticket",
      "args": [
        { "string": "KT1_ticketer" },
        { "string": "foo" },
        { "int": "3" },
        { "prim": "string" } 
      ]
    }
  }
  ...
}

This would enable wallets to identify when users are sending tickets to others by scanning the parameter, allowing them to alert users about the transfer. We don’t need to stare at the general Pair structure and guess whether they are tickets or not.

Alternative solution: Ticket Receipts

An alternative solution would be to require wallets to inspect the ticket update field in receipts returned by simulations to warn users about ticket transfers. However, we believe that using the ticket constructor would better integrate into existing wallet workflows. Specifically, when wallets parse transactions for user display, they should extract not only the raw parameter and the amount in tez, but also all tickets included within the parameter.

We aim to have the above changes happen in the next Protocol proposal from the core team – P . Please let us know what you think and how this might be brought to the next level to fit community needs further. We are very keen on all kinds of inputs and feedback.

7 Likes