Common Issues
This page contains a list of the most common issues you may encounter when developing applications in Chialisp. This list is by no means comprehensive, and we recommend putting each of your applications through a rigorous code review and audit before deploying them on mainnet. However, by following the guidelines laid out here, you will avoid some common pitfalls that can be easy to overlook.
Password Coin
Problem
Coins that are only locked with a password are not secure.
Cause
Farmers can see and modify coin solutions.
Example
A simple password coin would use this as its primary function:
(if (= (sha256 password) PASSWORD_HASH)
conditions
(x)
)
Keep in mind that each node has its own mempool. When a farmer creates a new block, they can:
- Choose to include or exclude any spend from the mempool
- View each coin spend and solution in the mempool
- Modify any of the coin solutions (provided that the modified solution is valid)
Let's say a coin has been secured using only the above if
statement. PASSWORD_HASH
has been curried into the coin, so it cannot be modified. Therefore, in order for this coin to be spent, the solution must contain the original password
, which must hash to PASSWORD_HASH
. The solution will also contain conditions
. This is a list of any Chialisp condition(s), one of which will presumably create a new coin to a puzzlehash the owner controls.
The problem with this puzzle is that whoever farms the coin will see the password
, which is required to be included in the solution. The farmer could then ignore the original solution and spend the coin with new conditions
that would create a new coin using a puzzlehash the farmer controls. In other words, the farmer could steal this coin as it was being spent.
How to avoid
Password coins are easy to create, so they are often used in training materials (see our documentation for more info). However, coins locked with a password (and nothing else) are not secure. Instead, use announcements and the AGG_SIG_ME
condition to secure your coins, as explained in the standard transaction puzzle.
Also keep in mind that a farmer can attempt to modify a coin's solution as it is being spent. If modifying any of the conditions from your coin's solution would result in the solution remaining valid, then you should assume the farmer will do exactly this.
Unchecked Hashing
Problem
Hash collisions could be used to modify coin values.
Cause
Concatenating and hashing without verifying the length of items to be concatenated.
Example
The CAT1 standard calculated coin IDs like this:
coin ID = sha256(parent coin ID + puzzlehash + amount)
This normally resulted in a correct coin ID
, but the length of each component was never checked. An attacker therefore could shift the divider between the puzzlehash
and the amount
to the left by one or two bytes. Depending on the value of the extra byte(s), there was a 50% chance that this would result in an inflated value for the coin, and a 50% chance that the coin's value would be negative. In the latter case the current coin could not be modified, but the attacker could simply spend it and try again.
The puzzlehash
would be shortened by one or two bytes, and the amount
would be lengthened by one or two bytes, but the resulting hash would be identical. Every coin using the CAT1 standard was vulnerable to this attack.
How to avoid
In the specific case of calculating a coin's ID, you are recommended to use the new coinid operator, introduced in CHIP-11. This operator validates the length of each component of the coin's ID, as well as the coin's value. In addition the CLVM cost of this operator is lower than that of using sha256
(800 versus 953), so there is no reason not to use it when calculating a coin's ID.
For use cases that involve hashing a concatenated string to do something other than calculate a coin's ID, you will need to use sha256
. In these cases, prior to running the sha256
command, be sure to validate that the length of each component is correct.
In the case of CAT1, the vulnerability was discovered prior to the existence of the coinid
operator. Therefore, sha256
was still required in the patch, which called the following function to calculate a coin's ID:
(defun calculate_coin_id (parent puzzlehash amount)
(if (all (size_b32 parent) (size_b32 puzzlehash) (> amount -1))
(sha256 parent puzzlehash amount)
(x)
)
)
This function verifies that the parent coin ID
and puzzlehash
are each 32 bytes, and that the amount
is at least zero (negative value coins are not allowed). With this function call in place, if an attacker attempts to shift any of the components, the Chialisp program will exit and raise an exception.
For more information, see our blog post discussing this issue.
Unprotected Announcements
Problem
Multiple coins can assert the same announcement.
Cause
Creating a coin announcement that does not include the ID of the coin being spent. This allows multiple coin spends to assert the same announcement, resulting in a replay attack.
Example
TibetSwap v1 was an AMM that stored liquidity for coin pairs in a singleton with a liquidity TAIL. Upon spending this coin, a CREATE_COIN_ANNOUNCEMENT
condition was created. This coin used a keyword of either "mint" or "burn" (and nothing else) as its announcement. In this case, an attacker could burn the same liquidity twice by asserting the same coin announcement twice. This attack could be repeated until all funds were drained.
The original liquidity TAIL with the vulnerability is available in GitHub's history.
How to avoid
Whenever you create a coin that uses a CREATE_COIN_ANNOUNCEMENT
condition, remember that the condition can be asserted multiple times in the same block when the coin is being spent.
You can prevent this replay attack by including the ID of the coin being spent in the announcement, as can be found in the patched version of the liquidity TAIL used in TibetSwap v2. In this case, the singleton_coin_id
is included with the CREATE_COIN_ANNOUNCEMENT
condition, which prevents the announcement from being asserted multiple times. TibetSwap v2 has been running since May 2023 without incident.
For more information about the vulnerability, how it was discovered, and how it was patched, see the postmortem.
Unprotected Solution
Problem
Anyone is free to change the solution provided in the spend bundle in any way they want that still satisfies the puzzle.
How to avoid
Ensure that all elements of the solution are either signed, or in some other way protected, so that only the desired spend is possible.
Replace By Fee
Feature description
"Replace by Fee" (RBF) is a technique that allows a new spend bundle to be added to the mempool that replaces an existing spend bundle. The rule is that all of the coins in the first spend bundle must exist in the replacement spend bundle, and that the fee attached to the new spend bundle must be greater than that in the original. In general, this is a very useful feature. For example, TibetSwap uses this feature to enable multiple swaps of a single pair in a single transaction block.
Problem
An attacker can "piggyback" some coin spends of their own on top of your spend bundle by resubmitting it with a greater fee. The solutions for the coins in the original spend bundle may be different in the replacement spend bundle, depending on how they were protected. And new coin spends can be introduced in the replacement spend bundle.
How to avoid
As stated above, ensure that if an attacker attempts to modify the solution to your spend bundle, the transaction will fail.
Flash Loans
Description
Ephemeral coins are created and spent in the same block. Because these coins are immediately spent, there is no output value, which means that the input can be any arbitrary amount.
Problem
Within a spend bundle, it is valid to create a coin of any XCH value, as long as the total XCH value of the unspent coins created in the spend bundle is less than or equal to the total XCH value of the spent coins. This is known as a "flash loan," and it leaves unprotected coins vulnerable to being attacked.
How to avoid
Ensure that if an ephemeral coin is added to your spend bundle, such as with the aforementioned RBF spend, the coin(s) you are attempting to spend cannot be stolen or spent in a nefarious way. Always assume an attacker will attempt to modify your solution and/or add an ephemeral spend, and protect your coin spends accordingly.
Puzzles containing ASSERT_COIN_ANNOUNCEMENT
Problem
If an ASSERT_COIN_ANNOUNCEMENT
condition is used in a coin's puzzle, the coin will be bricked (unable to be spent) if the coin being asserted has already been spent.
For example, say coin A
uses this condition in its puzzle, and it asserts a coin announcement from coin B
. In this case, coin A
requires coin B
to be spent in the same block as it is spent. If coin B
is spent before coin A
, then coin A
can never be spent.
In addition, if ASSERT_PUZZLE_ANNOUNCEMENT
is used in a coin's puzzle, a coin with the same puzzle must be spent in the same block. This assertion is less risky because it only relies on a coin with a specific puzzle, and many such coins might exist.
How to avoid
Only use ASSERT_COIN_ANNOUNCEMENT
and ASSERT_PUZZLE_ANNOUNCEMENT
in a puzzle's solution, and not in the puzzle itself. If one of these conditions are used in the solution for coin A
, and coin B
has already been spent, then coin A
can still be spent later, albeit with a different solution.
Spend Bundle Splitting
Problem
Spend bundles are not signed. So unless all of the coin spends in a spend bundle are linked together, the spend bundle can be split such that only part of the spend bundle is executed as submitted. The resulting smaller spend bundle can then be submitted, possibly with additional coin spends replacing the portion of the original spend bundle that was dropped.
Note that Replace By Fee cannot be used for spend bundle splitting. The spend bundle must be split before it gets to the mempool, or by a malicious node that splits it before gossiping it, or by a malicious farmer when farming the block that will include the spend bundle. A malicious node that splits a spend bundle before gossiping it will set up a race condition: each peer node will only accept one version of the spend bundle or the other, depending on which version it receives first.
How to avoid
If your spend bundle would be vulnerable to being split in a malicious way, you can link all coin spends together with announcements. This will ensure that the spend bundle must remain intact when spent.
Signature Replay / Signature Subtraction
Problem
Replay attacks on signatures are possible if a malicious farmer can find a way to isolate a useful signature.
Cause
All of the BLS signatures in a spend bundle are aggregated by arithmetically adding them together. Therefore, if a malicious farmer sees one aggregated signature that includes messages A
, B
and C
signed with public keys A'
, B'
and C'
, and then subsequently sees a spend bundle with an aggregated signature that includes messages B
and C
, with public keys B'
and C'
, then the farmer can subtract the latter aggregated signature from the former and derive the signature of message A
with public key A'
. The malicious farmer can then attempt to use that derived signature to initiate spends with a key they don't have.
Other scenarios where signature subtraction could be possible include:
- Given signatures
A
andA+B
, a malicious party could calculate the signature forB
- Given signatures
A+B
,B+C
, andA+C
, a malicious party could calculate the signatures forA
,B
, andC
Example
In a one-sided Offer, an asset is given in exchange for nothing. This can be a valuable technique, for example when air-dropping NFTs to unknown addresses. However, if the Offer maker's signature is accidentally revealed (in addition to the aggregated signature, which is always revealed), a malicious party could acquire A
and A+B
, as outlined above.
In this case, the malicious party could potentially receive the NFT; the honest Offer taker would pay the transaction fee.
In theory, individual signatures are not revealed to the broader network (only aggregated signatures must be revealed). However, signatures are not treated as private information. Some reasons a wallet may accidentally reveal an individual signature include:
- A coin spend is submitted, but the fee is too low and the transaction is evicted from the mempool
- A re-org causes a spent coin to reappear as unspent
- Replace By Fee (RBF) is used on a coin spend in the mempool
In each of these cases, a wallet's attempt to spend a coin is unsuccessful. Even if the wallet waits a long time before attempting to spend the same coin again, it should assume that at least one node has maintained a copy of the original signature.
How to avoid
Presume that all signatures are public. In cases where coins are only secured with signatures (and not with announcements), after an individual coin's signature has been revealed, the wallet should be careful never to submit an identical signature again.
One hypothetical way for the wallet to accomplish this would be to keep track of every signature it has ever published, as well as each aggregate that would be unsafe to publish. This would be impractical; the reference wallet does not attempt to do it.
Instead, the recommended method for preventing signature subtraction is to secure all coin spends with announcements. For example, in the case of a one-sided Offer, this could be accomplished by requesting 1 mojo from the taker. The Offer will then include an announcement, thereby making it secure. In other words, the safest way to secure a one-sided Offer is to make it two-sided.
Another technique for preventing signature subtraction is to add a nonce to each spend bundle. If the nonce is generated correctly and consistently, the same spend bundle will never be signed twice, thus preventing the possibility of signature subtraction.