Skip to main content

Overview of the Approach

Tokens are stored in individual UTXOs, like native satoshis. We use covenants to enforce a token to retain its distinguished form in all subsequent transactions. More specifically, we use recursive covenants to maintain token state.

We will construct CAT token smart contract step by step. We start with a straightforward yet insecure implementation.

First Attempt

In the diagram above, the current transaction curTx merges tokens from two parent transactions: parent0Tx and parent1Tx. Each token output’s state consists of:

  • addr: owner’s address
  • amt: amount of tokens in the output.

Their scripts are identical and must satisfy all conditions/assertions specified in token script T:1

Condition (1) ensures that owner A authorizes the merge. (2) and (3) ensure that script propagates from parents to the current spending/redeeming transaction, and later to its children and so on, that is, recursive covenants. (4) makes sure token amounts are preserved after merging and no new tokens are created out of thin air.

An Attack

Token script T has a security vulnerability: an adversary can join two different types of tokens. The attack is exemplified in the figure below. Transactions parent0Tx and parent1Tx mint 22 and 11 units of different tokens, respectively. This is similar to new bitcoins only emitted from a coinbase transaction. curTx joins them into a single unit of 33 of the same token.

Note all conditions in T are met.

To prevent this attack, we assign each token a unique identifier. One straightforward way is to use the outpoint the minting transaction spends as tokenId, i.e., txid_vout. We call the transaction the outpoint is in the token genesis transaction (e.g., T1 and T2). Since each genesis outpoint is globally unique, we have a unique tokenId. We add tokenId into its state and add two more conditions into T.

They ensure the tokenId is preserved from parents to children, grandchildren, etc. All subsequent spend of the output must embed tokenId for the entire lifecycle of the token. This technique effectively "colors" the token and ensures only the same type/color of tokens can be merged.

With these additions, the previous attack is thwarted since condition (2) is violated.

Final Piece of the Puzzle

We are not done yet. Note script S2 in T2 is evaluated, but script T is not, when minting a token in parent1Tx. T will only be evaluated when the minted token is spent. Consequently, an attacker could choose an arbitrary value for tokenId. For example, she can set tokenId to be T1_0, instead of T2_0, and forge 11 units of tokens with ID T1_0 in parent1Tx. These tokens can be merged with 22 valid tokens in parent0Tx, bypassing all previous conditional checks.

To address this issue, we add another conditional check in token script T, to validate tokenId is set faithfully as the genesis output.

To see how this works, let us look at the following diagram. Suppose we are at curTx, third from the top as indicated by the arrow on the left. Since script S is not T, we know grandparentTx is the genesis transaction and we enforce the tokenId in parentTx is set as T1_0. All descendant transactions from the genesis inherit the same tokenId after it is algorithmically set when minting. Suppose we are at the fourth transaction as indicated by the arrow on the right, we skip the conditional check above, since the script of grandparent and parent are identical. Same is true when we are at the 1000-th generation transaction. At any token transaction, its grandparent can only be one of the two:

  1. A token genesis transaction
  2. The same token transaction: same script and tokenId

The aforementioned attack is now prevented.

The attacker can still forge tokens, but they are unspendable, rendering the forgery futile.

Footnotes

  1. Txo is short for Tx.outputs.