50 Ways to Lose Your Love...laces
Updated: Nov 15, 2021
Smart contracts are great. We love them at Canonical. It is pretty much all we write, but they can be dangerous. Without proper care and precautions, you're going to have a bad time and end up poorer.
You don't have to look too far to find an example of an exploitable smart contract. If you use the `alwaysSucceeds` contract shown in the "How to write Plutus transactions" documentation, you will lose funds, because a bot will immediately steal them.
`alwaysSucceeds` is a trivially exploitable contract, so it is not surprising that an attacker can steal the funds. However, what users might not realize is how smart contracts engender an adversarial environment. Plutus smart contracts have been live for a little over a month, and already a hostile environment has developed, where bots are running wild to steal funds.
Sadly, as more money is locked in smart contracts we can expect attackers to become more sophisticated and more prevalent.
The Missing Input Exploit
A less obvious exploit can occur if a contract checks the outputs of a transaction but not the inputs.
Consider the case of a NFT standing offer contract.
A standing offer affair takes two transactions.
In the first transaction, the NFT buyer makes an offer for an NFT by sending Ada to script, thereby locking it there for the NFT owner to accept.
Or, at least, that is the theory.
For the exploit to work, the user does not send Ada, they merely set the transaction output hash.
When the NFT owner decides to accept the offer, the transaction building code of their wallet or browser tends to work the following way:
The outputs of the transactions are specified.
The script input UTxO is specified.
The UTxOs at the user's wallet address are used to fill in the missing inputs.
Even though the script address is missing the expected Ada for purchasing the NFT, the transaction building code will just use the Ada from the NFT owner's wallet.
Since the smart contract only checks that the required outputs are present in the transaction, everything will succeed.
The attacker will get the users NFT, and the former NFT owner will get as payment the Ada from their wallet.
The Defaults Are Not Your Friends
Exploits are the most exciting way to lose funds; however, they are not the most common.
The most common way funds are lost, is when they become trapped forever.
For typical wallet addresses this occurs when someone loses their private keys.
For script addresses more information is needed to unlock the funds, so there is more room for error.
By default, the datum is not stored when locking assets, only the datum hash (learn more about his in Plutus 102). However, the full datum is needed to unlock assets. Because the assets will be lost forever if the datum is lost, it is safest to store the datum as metadata, although this is not the default.
Another gotcha with datums: you must add a datum hash. If you forget, your funds are locked forever.
Additionally, the smart contract code is not stored when locking assets. However, it is needed to run the validator to unlock the assets. The safest method to ensure that the script is always available to unlock the assets, is to store it as metadata as well.
Storing additional information in the transaction will increase the fees, but the alternative can be devastating.
Assume the Worst
Many smart contracts that swap assets between users have a cancel operation. The locker sends asset to the script's address, and if they later decide they no longer want to trade assets, they can create a cancel transaction to return their assets.
A smart contract developer might decide, having the best of intentions, to ensure a user sends assets back to their wallet when canceling.
On the surface, this makes sense: the smart contract is ensuring that a good thing happens. The user will get their assets back to their wallet.
The problem is, it's easy for the transaction creation code to have bugs. So if the asset output check is based on the datum, and the datum is invalid, the funds could be lost.
A simple check for only the signature of the owner might have prevented the loss of funds.
The Limits of Computation
The scariest way funds can be locked with smart contracts is when memory limits are hit.
This can happen even with the simplest validator possible: a constant validator that returns `True`.
If enough assets are sent to a script address, during validation, the protocol memory limit will be hit. There is no way to free these assets, short of begging IOG to increase the limits or bribing SPOs. It is unclear how to know in advance this will happen. See this github issue for more details.
Be Safe Out There
If you're starting out as a Plutus developer, please, please use the testnet. You will still lose real assets on mainnet (it is a Plutus rite of passage), but it is much easier to lose assets than most new developers realize.
If you're looking for guidance on how to avoid these issues, or would like help with Plutus development in general, don't hesitate to contact us at Canonical.
Stay safe frens.