From Bugs to Breakthroughs: Auditing Cardano Smart Contracts
What Are Smart Contracts?
Smart contracts consist of code deployed on a blockchain, designed to enforce agreements without intermediaries. On Cardano, these contracts can be written in many different languages: Plutus, Plutarch, Aiken, etc. By embedding logic directly on-chain, smart contracts enable trustless financial applications, decentralized exchanges, lending platforms, and more.
However, there are several risk factors. The rules enforced by the smart contract can be complex to understand, and there can easily be subtle interactions that lead to unexpected behavior, especially when different smart contracts are involved in the same transaction. Moreover, smart contracts can not easily be updated (unless the updating logic is built into them). Once the logic is deployed to the blockchain, it can be very hard or impossible to roll it back. In general, the security model differs from that of standard software, where bugs and patches can be fixed through successive releases, but resembles hardware more: if a faulty component is shipped out into the world, it can be very difficult, or even impossible, to recall it.
Why Audit Smart Contracts?
Auditing ensures that contracts behave as intended, are secure from malicious exploitation, and protect user funds. On Cardano, smart contracts often interact with several UTxOs, opening the potential for subtle vulnerabilities.
A vulnerable smart contract can lead to several issues, such as:
Misappropriation of funds - Funds can be stolen from the protocol or other users; this can also include being able to obtain staking rewards illegitimately.
Leaking protocol tokens - Tokens that should be exclusively managed by the protocol can be stolen by a malicious actor.
Funds locked indefinitely - Under some circumstances, funds can be locked in a way that does not allow spending them anymore.
Protocol stalling - Several possible DoS attacks can happen to protocols operating under the UTxO model.
Common Vulnerabilities in Cardano Smart Contracts
There are some issues that are common across many smart contracts. Some of them come from the languages used to implement the smart contracts, others from the nature of the UTxO model.
Some recurring issues include:
Multiple Satisfaction - A transaction may consume multiple UTxOs at the same time, even if the smart contract developer did not intend that. If the smart contract is not written carefully, the rules that allow spending one UTxO can be applied to several at the same time, leading to a multiple satisfaction attack.
Other Redeemer - A transaction can attempt to spend a UTxO with a different redeemer than what is expected. If the smart contract does not check this explicitly, it can lead to potential exploits.
Unbounded datum/value - A malicious user can create an unexpectedly large datum or UTxO value, leading to the protocol stalling or halting completely.
UTxO contention - A malicious user can repeatedly spend the same UTxO, preventing anyone else from interacting with it. This can slow down the protocol and prevent legitimate users from interacting with it
For a complete and detailed list of vulnerabilities, have a look at our previous article common Cardano security vulnerabilities
Example: Double Satisfaction
This section assumes a basic understanding of how the UTxO model works. Refer to this for an explanation on how it works.
Let's use a simple example to show how a multiple satisfaction attack can happen. Suppose we want to design a simple protocol where users can sell and buy NFTs from each other (the exact nature and contents of the NFTs are not important for this discussion).
To start, we can define the datum for the UTxOs that will be locked in our smart contract:
{
owner: Address,
price: Integer,
}
The NFT will be locked in a UTxO with the datum above. The owner
will be the address of the owner of the NFT, the one we should pay to buy it, and the price
is the ADA amount we must pay to successfully buy the NFT.
When a user wants to sell one of their NFTs, they can create a UTxO at our smart contract (a validator), which has the above datum. Our smart contract will then enforce that whoever is trying to spend this UTxO is paying the required price
to the owner
. Remember that a validator is a function that takes the datum (and some other data, omitted in the example below) as arguments, and returns True
or False
to respectively allow or prevent a transaction from spending the UTxO. The code for this validator might look something like this:
validator datum =
if tx sends >= datum.price ADA to datum.owner
then True
else False
This seems simple enough and apparently does the trick: the validator only allows spending the UTxO with the NFT if the correct amount of ADA is sent to the owner
.
However, what happens if the same owner
attempts to sell 2 (or more) different NFTs? The validator would get called several times, once for every UTxO that is being consumed (or once for each NFT being bought). Suppose someone is selling 2 NFTs: one for 50 and one for 100 ADA. Then an attacker could create a transaction that consumes both UTxOs, but only sends 100 ADA to the owner. Individually, each UTxO would satisfy the validator, as the tx sends an amount of ADA to the owner that is greater than or equal to both prices; however, the attacker is only paying for a single NFT.
This is a very simple example of how this vulnerability might occur. Smart contracts often require the user to spend several UTxOs and must check for several different conditions. Moreover, oftentimes the rules to spend UTxOs can be scattered across different smart contracts (validators, minting policies, staking validators), making it very hard to track all the possible interactions.
Preparing for a Successful Audit
When commissioning a smart contract audit, clients can greatly improve the process by providing the following documentation:
Specification of the intended functionality and use cases for the protocol.
An explanation of assumptions, invariants, and expected interactions within the smart contracts.
Well-structured Test Suite, including:
Unit tests covering core on-chain logic.
Property-based or scenario tests simulating realistic transaction flows.
Edge-case tests (e.g., minimal ADA requirements, unexpected datum values).
Reproducible scripts for compiling and deploying the contract.
All these are required so auditors can focus on deeper security and logic analysis rather than spending time reconstructing the project’s intent.
It is particularly important to provide a specification of the intended behavior that is independent of the smart contract code; otherwise, auditors are going to have a hard time distinguishing between intentional and non-intentional behavior.
Likewise, providing an extensive test suite, which the auditors can easily run and modify, will allow them to quickly verify the behavior of the protocol in several different scenarios.
Another thing that can be useful for auditors is to provide a simple and easy-to-use build process. With this, an auditor can make some quick changes to the on-chain code and run a test with those changes. This can be very beneficial when working on a complex vulnerability.
The Audit Process
The audit process is usually divided into four phases:
Understanding the codebase
Security analysis
Preparing the report
Reviewing the fixes
Let's have a deeper look at what happens in each of these phases.
Understanding the codebase
This is the phase where being appropriately prepared for the audit can make the greatest difference. The auditors need to learn the protocol's intended behavior in a short amount of time.
In this phase, the auditors will study both the documentation and the code, getting deeply familiar with how the protocol is intended to work and how it works under the hood.
The auditors will also make sure the test suite is running correctly and get familiar with how to modify and extend the existing tests to verify any vulnerability they find.
The length of this phase is usually inversely proportional to the quality of the documentation provided. It is in a client's best interest to make this phase as short as possible, as more time spent on the following phase can help uncover many more issues.
Security analysis
In this phase, the auditors will start testing for different vulnerabilities.
This process usually starts by checking if any of the common vulnerabilities described above would apply to the codebase under analysis. Again, being aware of these common patterns during development can make this process shorter, allowing the auditors to focus on more complex issues.
This usually allows the auditors to get even more familiar with the codebase, as this process usually involves writing some tests to verify different assumptions.
To confirm a vulnerability, an auditor will write a test that demonstrates the issue. Once the test is written and has been reviewed, the issue is confirmed. Finally, the auditor will write a recommendation on how to fix the issue. Recommendations often involve small code changes, but severe cases may require substantial refactoring or even a full redesign.
For this reason, throughout this phase, auditors will always communicate confirmed issues to a client as soon as possible. This allows them to start working on the changes required to fix any problem. Despite this, sometimes it can actually be better not to start implementing a fix for a reported issue right away. Vulnerabilities can often be combined to create even more damage than their single parts, and having a complete picture of how different vulnerabilities interact can lead to simpler and more efficient fixes. It's not easy to anticipate when to start working on a fix right away and when to wait; good communication between the audit and the development team usually leads to the best outcomes in this regard.
After testing for common vulnerabilities, auditors will focus on issues with the protocol design. This often involves complex interactions of several different smart contracts. In this phase, auditors will also focus on the mathematical assumptions that (when present) underlay the protocol. This includes verifying the incentives are always correct to keep the protocol operational, verifying that fees are being paid correctly in all circumstances, and other usually complex parts for most protocols.
Another important part of the audit that happens in this phase is the review of the parameters that will be used to instantiate the smart contracts on-chain. Often, there are invariants that must hold between the smart contract configuration, which may lead to issues when broken. The audit team will check the provided parameters and certify the resulting hashes for the smart contracts.
Preparing the report
This phase usually takes a short time at the end of the audit. All findings are compiled in a report for the client. By this phase, the vulnerabilities have most likely already been communicated informally with the client, but a compiled pre-report is provided to the client with a summary of all the issues that have been found and suggested fixes. It also includes other sections which briefly describe the protocol, provide some context and disclaimers, and a description for each issue type.
Reviewing the fixes
After all the communicated issues have been fixed, the audit team will have a last look at the codebase to:
Verify the implemented fixes are either the suggested ones or equally effective.
Verify that the changes introduced by the fixes do not cause any new issues.
Each issue in the report is updated with a description of the outcome: if the issue was fixed and how, or if it was not fixed and the reasons for that. This is the final step; at this point, the report is ready and can be publicly shared.
Conclusion
In this article we have provided a bird's-eye view of the audit process: we have discussed why audits are necessary, walked through common vulnerabilities and consequences of those vulnerabilities, also working through an example of a simple multiple satisfaction attack. In the last two sections, we described what happens during an audit, and which documents a client might need to provide to get the most out of an audit.
If you want to learn more about how audits work don't hesitate to get in touch.