Skip to main content

Token Security

Native tokens, introduced in the Mary protocol upgrade, represent value that can be created and traded on Cardano in addition to ADA. They can also be more than a representation of value: many tokens exist purely as technical devices inside protocols. Handling either kind safely means knowing how tokens behave inside transactions and where the pitfalls lie, which is what this page covers from a smart contract developer's point of view.

Throughout, the word token covers both ADA and native tokens. ADA is a special case whose handling sometimes differs, but it behaves like a native token in most contexts.

Basics

A token on the Cardano blockchain is an asset that can be stored inside an Unspent Transaction Output (UTxO). Tokens can be minted (new tokens are created), burned (existing tokens are destroyed), or transferred. Any token is defined by two parameters, a policy ID and an asset name sometimes called its token name. The rules detailing how and if new tokens can be minted or old tokens burned is written in a minting policy. A minting policy is a smart contract and it is linked to the tokens through the tokens' policy ID (the first identifier of a token). More specifically, the policy ID of a token is a cryptographic hash of its minting policy's code. Tokens that have different minting policies are very different. Tokens that are different only in the token name are somewhat related, they are governed by the same smart contract.

The policy specifies the conditions under which tokens can be minted or burned. If any token is minted or burned in a transaction, the minting policy of that token must be part of the transaction and must successfully validate the operation. However, tokens' transfer alone does not execute the minting policy and the transfer therefore can't be controlled by the token's code.

Token names are additional data associated with tokens. They are set by the minting party in the minting transactions. The minting policy governs the minting of all tokens with the same policy ID, even though they can have different token names. More often than not, it governs what token names can look like and under which circumstances.

Note that in a single transaction, each minting policy is run only once. If the transaction mints and/or burns several tokens with the same policy ID but different token names, the corresponding minting policy is run only once and it needs to validate all the policy's minted and burned tokens.

validator {
fn mint_and_burn(_redeemer: Void, ctx: ScriptContext) -> Bool {
let ScriptContext { transaction, purpose } = ctx
expect Mint(own_policy_id) = purpose
let Transaction { mint, ..} = transaction

let mint_value = value.from_minted_value(mint)
let own_tokens = dict.to_list(value.tokens(mint_value, own_policy_id))

// Allowing only mint, not burn
list.all(
own_tokens,
fn(token) {
let (asset_name, amount) = token
amount > 0
},
)
}
}

The code above shows a simple minting policy that lets anyone mint any tokens, but does not allow burning them at all. Note that the transaction.mint contains all the tokens minted or burned in the transaction. The goal is therefore to filter out only tokens governed by this minting policy whose id was extracted to the policy_id variable. The resulting list can contain a number of records. Each is a pair consisting of an asset name and an amount of tokens of that asset name that were minted (representing a positive number) or burned (representing a negative number). Finally, the final check makes sure that all the amounts are positive, meaning that no token was burned. Note that it does not restrict anything else and you could mint tokens of any token name under this policy. However, naturally, you can not mint tokens of a different policy using this code. For that, you need the other policy to validate.

It is therefore possible to create tokens that:

  • can be minted only with a specific token name.
  • can be minted only once, this can be used to create NFTs.
  • can be minted only if another token of the same policy ID and a different name is burned.
  • can be minted only if enough ADA is deposited into a specified UTxO.
  • can be burned only if the transaction is signed by a specified key.
  • cannot be burned under any circumstances.

Note that the policy ID of ADA is an empty string. Therefore, there is no minting policy that corresponds to it. As a consequence, it is not possible to mint or burn ADA this way.

Tokens as value

Many tokens represent value. Whether it is ADA, stablecoins, NFTs or tokens minted by promising projects, they have value and are often stored and transferred between various UTxOs.

As a developer, you need to ensure that you handle these tokens correctly when designing your dApp. Assume you have created a simple treasury to which value can be freely added by anyone, but can only be redeemed in a correctly signed multi-signature transaction. For example, such treasury could be used by a DAO that's selling NFTs on a marketplace. Users buying the NFTs would pay into this treasury, and they can withdraw the tokens only if multiple members of the DAO agree on how they want to use the funds received. This protocol can be demonstrated in these two simple transactions.

TS-1

What are the security risks you want to prevent when implementing this treasury?

Focus on the first transaction type, a transaction in which additional value is added to the treasury. The user submitting such a transaction should not be able to withdraw anything from the UTxO so you implement a simple check that "Amount of ADA present in the output UTxO is at least the amount of ADA present in the input UTxO".

This works just fine for ADA (double satisfaction vulnerability aside), but there are no restrictions placed on any other tokens. In the example with the DAO selling NFTs, the DAO decides to sell an NFT for some tokens other than ADA, and someone buys such an NFT. Suppose that the trade went through and the DAO balance was updated correctly. Anyone can retrieve those tokens simply by spending the DAO's UTxO and withdrawing the tokens. As shown above, the validator only checks that no ADA is stolen. This is an example of a vulnerability in the token handling. Here is the fix:

Value size and execution limits

The treasury validator can be edited to check the following condition:

For each token type present in the input treasury UTxO, at least that amount of the same tokens must be present in the output treasury UTxO.

This is better, as it guarantees that the value in the treasury UTxO only accumulates, until the DAO members withdraw from the treasury via a multi-signature scheme.

But there are glaring security issues with this approach which are apparent in a broader context of the Cardano blockchain. Each transaction needs to go through two validation phases. In the second phase, the scripts are executed. To avoid unproportional spending of resources there are several limits that Cardano enforces.

The first important limit in this context is the maximum value size limit. This is a protocol parameter, which is currently set to 5000 bytes. All the tokens in the value contribute towards the value size, and therefore this limits the amount of different tokens that can be held in a single UTxO. To exploit this limit, an attacker could DDoS the treasury by minting many different worthless tokens into it, thus disabling other people from contributing valuable tokens. This attack is called "dust tokens".

The multi-signature parties could mitigate this issue by withdrawing the worthless tokens and thus allowing others to contribute. However, this does not prevent the attacker from repeating the attack again.

The other relevant limits are the execution limits. Execution limits limit the amount of time and memory units for each transaction's script execution. Again, these limits can not be exceeded. For example, parsing the value in the transaction validation counts towards both the time and the memory execution limits.

By attacking these execution limits, an attacker could block even the withdrawal operation, depending on its complexity. The withdrawal transaction can be more complicated than the depositing transaction, as it needs to find and run two validators instead of one. If the validator runs code in which the resource consumption depends on the number of tokens (e.g. for each token type as in this case), the artificially enlarged UTxO can potentially stretch the execution limits to such an extent that the withdrawal validation can not fit in the execution limits. That effectively makes it impossible for the value contained within to be withdrawn unless an (unlikely) protocol change is made, e.g. increasing the limits.

Correct value handling

Theory is cleaner than practice. How close a transaction comes to the execution limits depends on the validators it runs, how complex and efficient their implementations are, and on the transaction itself: its structure and size.

There is no simple catch-all solution for token handling. The best course of action is to first think about the types of tokens that can be deposited into the UTxO. If you only really care about ADA, enforce that no other tokens are added. If you need to support multiple policies, specify the set of policy IDs in the datum and test that the validator is efficient enough that it validates even when it contains all of them at the same time. If you need to be able to support any tokens, you could limit how many different tokens can be in a single UTxO. You might require a different solution altogether, though.

Rigorous testing should be in place to test whether even the most extreme edge cases allowed by your validator fit into the execution limits. Remember to try enlarging the transaction as well when testing, e.g. increase the number of normal inputs and outputs, put tokens into those artificial inputs and outputs as well, etc.

Value normalization and zero quantities

Comparing values is another place handling goes wrong. An on-chain Value is a normalized, canonically sorted map that by construction never holds a zero-quantity entry. The transaction's mint field is not normalized the same way, which is why Aiken historically exposed it as a distinct type converted with from_minted_value (that separate type has since been merged into the unified cardano/assets Value, but the underlying distinction remains). The risk is trusting value structure you did not normalize: an externally supplied value carrying a zero-quantity token, or a non-canonical ordering, can make a structural equality (==) or subset check behave wrongly, so two economically equal values compare as unequal (bypassing a guard) or a required equality can never be met (locking the UTxO). Do not compare raw value maps for equality when any part is attacker-controlled; check specific quantities with quantity_of, and normalize before comparing.

Beyond value: technical tokens

Value is tokens' primary purpose, but not their only one; native tokens also serve as technical building blocks inside protocols.

To demonstrate one use case, consider on-chain oracles. The main goal of an oracle is to reliably bring real-world data onto the Cardano blockchain. For example, you could want to know the value of a currency, a particular stock or the results of a real-world event. To do so, a special UTxO is often created and this UTxO contains the required information in its datum. The script validator of the UTxO can easily govern how updates are performed, usually, this is by allowing only certain trusted entities to spend the oracle UTxO, often requiring multiple of them to agree on the update. However, there could be multiple UTxOs created on the same script address and they could be created by anyone with any data (see the Missing UTxO Authentication page). Therefore, there is a need to recognize the correct current oracle UTxO.

Suppose the oracle's purpose is to bring just a single data point to the chain, meaning there's always just a single up-to-date UTxO valid. To make a UTxO uniquely recognizable, you can create a special kind of an NFT (without an image, just for development purposes) and lock it into the correct oracle UTxO. As there can be only one such NFT (by definition, a non-fungible token), there is always at most one oracle UTxO containing this NFT. When this UTxO is used in a transaction, either as a normal input or as a reference input, any validator can easily check whether the correct NFT is present. When the oracle information needs to be renewed, the old oracle UTxO is spent and the token is sent into the newly created oracle UTxO.

This is probably the most simple technical use case for tokens. This token does not hold any value, it is part of the technical design of the protocol. Implemented correctly, the above-mentioned NFT can not be withdrawn from the protocol, and thus traded, or otherwise interacted with. The only purpose this NFT serves is to uniquely identify a UTxO carrying the correct data.

Validation tokens

A fairly common use case of native tokens is to provide UTxO validation: making sure a UTxO was created correctly, wasn't tampered with, or proving authenticity of some kind.

The crux of the problem is that a validator is run only when a UTxO at its address is spent, not when it is created. It is therefore very simple to create malicious UTxOs with invalid data and present them as a real thing. Without further protection, it's impossible to distinguish between them on-chain.

Validation tokens fill in this missing piece of the puzzle as they can validate the initial transaction. You can enforce that the tokens can only be minted during the UTxO creation, validating that the created UTxO is correct and in the proper initial state. This validation is part of the corresponding minting policy. Note, that the minting policy has access to the whole script context, and it can therefore look at the whole transaction, including all the transaction inputs, outputs, signatories, datums, other minted tokens, etc.

The minting policy of the validation token ensures that it is placed into the correct UTxO. As a result, the validator can assume that any UTxO that contains the validation token was created according to the specification. Malicious UTxOs without the validation token may still exist, so the protocol must refuse to interact with them.

The idea is simple but not yet complete. As described, it creates several security pitfalls. As stated above, the validation token serves as a proof that the UTxO that holds it is correct. But this would only be the case if the UTxO was unspendable, once it's spent, the validation token could freely move away, potentially into a malicious UTxO.

The spending of the UTxO is solely in the hands of the UTxO validator. The mint of the token alone is thus only a part of this design pattern. It is essential for any validator that can hold a validation token to control where and how those tokens can be transferred.

Consider a situation in which the validation token is correctly minted and after a series of transactions, due to a faulty implementation of the validator holding it, it ends up in a user's wallet. That user can create a fake UTxO that pretends to belong to the protocol and include this old validation token in it. For all purposes of the validator, it will appear as a valid UTxO. Similar attacks are common in more complex smart contracts and are often critical in nature, possibly leading to a complete drain of all protocol funds.

It is therefore worth repeating that it is paramount for any smart contract with a validation token to take care of these tokens. The actual implementation varies depending on the use case. However, follow these two general recommendations:

  1. Count the number of validation tokens across all inputs and outputs to prevent double satisfaction attacks that could result in a "loss" of a token. The code should check that the number of input tokens and output tokens is as expected. For example, this way, you can not merge two UTxOs with validation tokens in them into a single one and claim the other validation token for nefarious uses. Remember that any single token lost to an unknown contract or to a public key address can ultimately break the whole protocol. You really need to validate every single token. Note also that tokens can be minted or burned in the same transaction if you don't restrict it. These double satisfaction attacks can be especially tricky when the minting policy is combined with the validator, an example of such an attack is as described previously above.

  2. Lock the validation tokens into very specific UTxOs. Make sure to check the whole UTxO (address, value, datum) of all output UTxOs holding your validation tokens. By checking the address, you can be sure that a script you trust keeps the control of the token. By checking the datum, you make sure that you do not validate malicious data. You need to be careful of double satisfaction attacks again here (e.g. do not validate two of the same correct outputs if only one should be validated). Lastly, you should check the value of the UTxO as well; e.g. to verify that there are no dust tokens present that could potentially prevent you from further spending the UTxO by maliciously increasing the execution units.

Unless the design requires it, do not allow minting new validation tokens in a transaction that already has some on its inputs; it keeps the double satisfaction reasoning simple.

Implementation

The validation token needs to end up locked in the correct UTxO. Therefore, the minting policy of the validation token needs to know the script hash of the correct validator. It can be either written directly into the minting policy or used as a parameter of it. The parametrization is a better code practice. The compiled code, however, is almost identical.

Additionally, the validator needs to check that the correct validation token is part of it in later transactions. Therefore, the validator needs to know the policy ID (hash of the minting policy) so that only that special validation token is supported. Once again, it can be known by the script by putting it as its parameter.

As the hashes of both the validator and the minting policy depend on each other, there is a new problem, a cyclic dependency. To know the policy ID, the script hash needs to be known and for that, the policy ID is needed. This cycle needs to be broken as the code can not be compiled as described.

There are several options for how to fix this. Perhaps the most elegant is to use the token name. Recall that the token name can be set arbitrarily by the minter, assuming the minting policy allows it. Therefore, for one minting policy, there are a lot of different token names, each possibly creating a unique token type. Keep the UTxO validator parametrized by the policy ID.

The minting policy won't be parametrized and it will contain the following change: the validation token can be minted into any address, but its token name needs to match the payment credential part of the address of that UTxO. The script validator then needs to check that the validation token of the policy ID set as a parameter that is present in the UTxO has the corresponding name, that it matches its own script hash. By doing that, the validator can be sure that the token was minted to the same script. Different token names are untrustworthy and need to be considered invalid.

use aiken/dict.{to_list}
use aiken/transaction.{
Mint, ScriptContext, Spend, Transaction, find_input, find_script_outputs,
}
use aiken/transaction/credential.{ScriptCredential}
use aiken/transaction/value.{from_minted_value, quantity_of, tokens}


// Minting policy of the validation token
validator {
fn validation_token_policy(_redeemer: Void, ctx: ScriptContext) -> Bool {
let ScriptContext { transaction, purpose } = ctx
expect Mint(policy_id) = purpose
let Transaction { outputs, mint, .. } = transaction

expect [(asset_name, amount)] =
to_list(tokens(from_minted_value(mint), policy_id))
when amount is {
1 -> {
expect [token_output] =
find_script_outputs(outputs, asset_name)

and {
quantity_of(token_output.value, policy_id, asset_name) == 1,
// ... other checks, including:
// ... double satisfaction prevention
// ... initial state validation (datum, value)
}
}
-1 -> True
_ -> False
}
}
}


// Validator expects to have the correct validation token
validator(validation_token_policy: ByteArray) {
fn multisig(_datum: Void, _redeemer: Void, ctx: ScriptContext) -> Bool {
let ScriptContext { transaction, purpose } = ctx
let Transaction { inputs, outputs, .. } = transaction
expect Spend(output_reference) = purpose

expect Some(own_input) = find_input(inputs, output_reference)
expect ScriptCredential(own_hash) =
own_input.output.address.payment_credential

// expect to have the correct validation token
expect
quantity_of(
own_input.output.value,
validation_token_policy,
own_hash, // expected token name == this script's hash
) == 1

expect [expected_token_output] =
find_script_outputs(outputs, own_hash)

expect
quantity_of(
expected_token_output.value,
validation_token_policy,
expected_token_name,
) == 1

// ... other checks, including:
// ... double satisfaction prevention
// ... correct state transition validation (datum, value)
}
}

The result of this is that the validation tokens can be minted freely into other script UTxOs, but such tokens are worthless as they don't have the correct name. So even if they are later deposited to the original script address, the name won't match and the validator will recognize it and invalidate any following transactions. The only way to mint a valid validation token is to mint it into the correct script address straight away.

Transaction token

There is one more common use of token minting and it is frequently used when dealing with batching or any other kind of running multiple validations in the same transaction. Assume you have a simple marketplace, anyone can create an offer and this offer can be filled by anyone. This marketplace would like to support batching in a sense that more than one offer can be filled in a single transaction.

The code is secured against double satisfaction attacks. The validator checks the whole transaction, gathers all the offer UTxOs and computes the overall value that needs to be sent to each seller.

While this double satisfaction prevention is secure, it is inefficient. Assume there are 5 offers that are put as inputs of the transaction. The aforementioned validator is run 5 times, once for each UTxO. However, the computation is always (almost) the same as the validator looks at the whole transaction. This puts a strain on the execution limits, making the transactions more expensive and limiting the number of offers that can be filled in a single transaction before hitting the limits.

It would be much more efficient if this expensive validation could be run just a single time. This can be achieved by delegating the whole transaction-level validation to a minting policy of an artificially created token. The transaction token is a special token whose minting policy contains the validation logic. The actual validator now only needs to check that a correct transaction token is indeed minted, meaning that the validation is run in the transaction. It therefore really just delegates its validation to the transaction token's minting policy.

Note that it is just an artificially created token for only this purpose. The token itself is not important, it can be freely moved or burned. The only important aspect of the token is that its mint provides the validation so that in this example, 5 inputs just check that the minting policy is run and the policy validates the whole transaction thoroughly. This is the transaction-level minting design pattern; Plutonomicon, which first described it, calls it the transaction token pattern.

Every route to transaction-level validation is a workaround of one kind or another; this is one of them, and the alternatives below lean on other conventions.

Another way would be to forward the validation to a single script input, e.g. the first one found in the transaction inputs as the order is set for the transaction and all the scripts see it in the same order. That would mean checking whether the current validation run is of the first script input, if so, run the transaction-level validation, otherwise validating straight away.

Yet another approach is the stake validator (withdraw-zero) pattern. Be aware of the convention it stands on: it relies on the ledger running a withdrawal script even for a zero-amount withdrawal, which is arguably an accident of the rules rather than a designed feature (minting zero tokens, by contrast, is filtered out and never runs a policy). A ledger-level effort to change the behavior was dropped precisely because deployed protocols already depend on it, so the pattern is widely used in practice, but it validates through a quirk, not a purpose-built mechanism.

As token security is an integral part of overall Cardano smart contract security, the Capture the Flag game features not one, but two levels focused on it. Check the levels 07_multisig_treasury_v2 and 09_multisig_treasury_v3. Both of these tasks feature validation tokens. You can look at their implementation, find in what way they break the general recommendations, and try to exploit the vulnerabilities.