At Tweag, we have been auditing Cardano smart contracts since 2021. We started
out reviewing contracts mostly by hand, but it did not take long before we began
building tooling to make our audits more efficient and systematic — most notably
cooked-validators, which we have maintained and improved
ever since. In its earliest form it could only handle one transaction at a time
and was built on top of plutus-apps — a library that has been
archived for a good while now. From this early reliance on a now deprecated
dependency, it grew over the years into the powerful framework it is today. At
its heart are convenient transaction bodies (or skeletons), focused solely
on what the user actually cares about, and several layers of automation that
build properly shaped and balanced transactions out of
them. On top of these sit fine-grained tweaks that alter skeletons in a
stateful way1 before submission (such as redirecting an output or
minting an additional token), an expressive LTL-inspired DSL that
schedules those tweaks at chosen points along a trace, and the automated attacks
built as instances of that DSL. The most recent improvement was to give this DSL
a proper semantics for negation and its derivatives, such as implication — the
subject of an upcoming post.
As painful as it is to admit, not every tool we built survived. Pirouette, an experimental static analyser for Plutus Core, was retired, and Graft, an attempt to generalize our LTL DSL to a wider range of domains, has been on hold for a long while, waiting for additional underlying logics and target domains. But even the tools that did not last taught us a great deal, and each of them contributed to improving our auditing expertise.
The smart-contract ecosystem did not stand still either. Developers were given access to more advanced languages, such as Plutarch, a monadic take on writing Plutus script through a high-level lambda calculus, sticking close to Plutus Core, or Aiken, a DSL living outside of the Haskell world specifically geared towards writing and testing smart contracts on Cardano. Cardano itself kept growing underneath us through its consecutive eras: Babbage brought reference inputs and inline datums, Conway introduced a full governance system, and Dijkstra, with nested transactions, is peeking over the horizon.
Through all of this we audited, we learned, and we occasionally stumbled — our first audit of Minswap, where a critical vulnerability slipped past us, is the case we keep coming back to. We also succeeded — more often than not with critical findings of our own, growing and learning in the process, while bringing increasing value to our customers. This post is a retrospective on those five years. It is not a client-by-client account, but a look at the recurring patterns we keep finding, the lessons we have drawn, and the way our methodology evolved. Whether you are a smart-contract developer, a fellow auditor, or simply someone who cares about high-assurance software in general (or on Cardano in particular), we hope you take away something useful from this retrospective.
Five years in numbers
Since our first Cardano audit in late 2021, we have carried out more than twenty audits across the ecosystem, covering products such as decentralized exchanges, stablecoins, lending protocols, oracles, governance rules, and even a constitution script. Several clients came back for more than one round as their protocols evolved, and not every engagement looked the same: most were standalone smart-contract audits, while some others came with extra layers. Marlowe, for instance, required not only the typical review of the on-chain portion of the project, but also one of the Isabelle formal model that came alongside it.
Across our standard smart-contract audits, we reported 276 findings. The table below breaks them down by severity, using the same five-level scale we apply in every report we deliver.
| Severity | Findings |
|---|---|
| Critical | 30 |
| High | 35 |
| Medium | 66 |
| Low | 95 |
| Lowest | 50 |
| Total | 276 |
The first noticeable element in the table is the 30 critical findings and 35 high-severity findings: issues severe enough that, left unaddressed, would have dire consequences either for the administrators or for the users of the protocols, if not both. Among those consequences, an attacker could, for instance, be mistakenly allowed to drain or lock user funds, cause delays in the life cycle of the product, or even fully empty pools of assets belonging to the contract.
The second is the shape of the distribution: the majority of reported findings sit at medium severity and below. These are smaller concerns that might seem harmless in isolation, but can pile up to form real risks.
Severity is only one axis. We also classify every finding by its nature — what kind of problem it is — which tells us as much about a codebase as the severity does. We use five categories:
- Vulnerabilities — directly exploitable flaws that an attacker can use to break the protocol’s safety.
- Implementation bugs — code that does not obey its specification, whether or not the discrepancy turns out to be exploitable.
- Design — issues rooted in the protocol’s architecture, which could cause serious flaws, such as unnecessary hoops in the contract flow or unnecessarily large transactions.
- Unclear specification — missing, ambiguous, or undocumented expected behaviour, which makes correctness impossible to assess. After all, a piece of software can only be deemed sound with respect to a complete and unambiguous specification.
- Code quality — lack of readability, maintainability, or usage of good coding practices. Theoretically low-impact issues, which could however hide or even be responsible for deeper flaws.
| Nature | Critical | High | Medium | Low | Lowest | Total |
|---|---|---|---|---|---|---|
| Vulnerabilities | 24 | 12 | 6 | 2 | 0 | 44 |
| Implementation bugs | 5 | 5 | 10 | 8 | 0 | 28 |
| Design | 0 | 6 | 18 | 16 | 3 | 43 |
| Unclear specification | 1 | 11 | 16 | 31 | 11 | 70 |
| Code quality | 0 | 1 | 16 | 38 | 36 | 91 |
| Total | 30 | 35 | 66 | 95 | 50 | 276 |
The two axes line up almost diagonally, which makes sense. Vulnerabilities are comparatively rare but concentrated at the top: 24 of our 30 critical findings are outright exploitable flaws. At the other end, code quality is our single largest category and sits almost entirely at low severity — each issue minor on its own, but collectively a good measure of how much care a codebase has had. In between, unclear specification stands out as the second-largest bucket, and it is a theme we keep returning to: a surprising number of concrete bugs ultimately trace back to behaviour that was poorly specified, or simply left out of the specification altogether.
The diagonal shape, however, is a tendency and not a law; the off-diagonal cases are what make the nature axis worth having. Notice the single occurrence of a critical finding falling into the unclear specification category. Because the contract in question relied on the order of its outputs to make decisions, an attacker could manipulate the output order to duplicate an entry and vote with more weight than they held. The only safeguard happened to be a legacy field that, almost by accident, kept the positions distinguishable, and nothing in the spec said it had that meaning; an unclear specification finding by nature, yet critical in severity. At the opposite corner, two audits surfaced vulnerabilities rated at low severity. One such occurrence was linked to the existence of a peer with high privilege on the contract (the operator), who could drain the protocol’s reserves at will. However dangerous this may be, the development team already knew of it and had accepted it as an explicit design trade-off, because the operator was assumed trusted (a pattern we see very often, and systematically point out).
Severity measures impact in context; nature measures the kind of flaw and how to prevent it. The two are correlated — hence the diagonal — but they answer different questions, and where they diverge tends to be insightful.
Not every audit ends with a long findings list. Our review of CTez, the only non-Cardano audit we ran, turned up nothing out of the ordinary — which, although a one-off, and quite surprising, is a valid audit outcome nonetheless. Marlowe sits at the other end of the spectrum: between the contract review and the audit of its Isabelle formal model, it yielded over a hundred findings on its own — which is why we did not include it in the totals of the tables above. That count, however, says more about the impressive size of the project than about its safety: the majority of those findings sat at medium severity or below.
Are smart contracts getting safer?
As we’ve already mentioned, a great deal has changed on the Cardano side over these five years. The chain has moved from era to era, each one adding capabilities while pushing further towards built-in safety. Plutus has gone through several versions, gaining new primitives along the way, and a variety of new languages have appeared, offering a far more pleasant developer experience while producing smaller, cheaper Plutus Core. Our own expertise, understanding, and tooling have grown accordingly. What we have not said yet is that the developers writing these contracts have improved too: the code we see is steadily wider in scope, cleaner, and safer, written with more attention to detail and care for the metrics that matter on Cardano, like transaction and UTxO sizes.
All of this has changed both the quantity and the nature of the findings we uncover.
Take the quantity first. There will be no table this time, and for a good reason. To measure the trend properly, we would want, for each audit, the ratio between its findings (ideally weighted by severity) and some measure of the codebase it covered, such as its line count. An exact figure for that ratio is out of reach: line counts are not comparable across the different languages we audit, all having different degrees of verbosity, and we do not keep client code once an engagement is over, so we would not even be able to dig up that number. But across every engagement, the trend is clear: slowly but surely, that ratio has come down over the years.
The nature of the findings has shifted too, and to see how, it helps to understand the model the chain is built on. Cardano runs on the extended UTxO (eUTxO) model, which differs from the account-based architecture familiar from most other blockchains. Rather than mutating a global, account-based state, a transaction consumes unspent outputs (UTxOs) and produces new ones, each carrying a value and a piece of data called a datum, while on-chain scripts — the smart contracts themselves — decide whether a transaction is allowed to spend a given output. Ownership, then, is not an entry in a global ledger but a question of which UTxOs sit at a given address. When that address corresponds to the hash of a script, thus belonging to it, “ownership” really means that the script’s logic runs — and gets to approve or reject the transaction — every time one of its UTxOs is consumed.
We see fewer and fewer vulnerabilities that are inherent to this model and that the community has since internalized, such as the well-known double satisfaction attack, which used to be a common find: when several instances of the same script run in one transaction, each could be tricked into treating a single output as its own, not realizing the others saw it the same way. Some other well-known vulnerabilities have disappeared outright as the platform matured, like the chicken-and-egg situation of minting tokens and sending them to a script in a single transaction — where the two scripts could not each be aware of the other’s hash — finally resolved by multipurpose scripts.
These classes did not simply fade on their own — we codified many of them. Attacks we once reconstructed by hand for every engagement, such as datum hijacking, double satisfaction, or token duplication, are now expressed once and explored systematically using cooked-validators, in the shape of tweaks scheduled along a transaction trace, branching to various possible outcomes. Each new class we ran into taught us something we folded back into the framework whenever possible, so the next audit could start from a stronger baseline. The findings shaped the tooling, and the tooling, in turn, increased our audit efficiency.
What remains tends to be of a different kind: mistakes specific to a contract’s own domain logic, subtle issues introduced by aggressive optimization — decoding builtin data by hand, or threading everything through continuation-passing style to shrink the compiled output — and, above all, design flaws. However much experience a team accumulates, something always seems to get in the way of an optimal design — if such a thing even exists. It might be disagreements between team members, turnover in the people involved, the sheer length of a project with its successive rewrites and redesigns, budget constraints, or new requirements surfacing halfway through development. Unfortunately, design issues are also arguably the hardest and most costly to fix, since doing so often means rewriting significant parts of a project — which is why they are left unaddressed more often than other kinds of findings. We hope these findings still prove useful — for future versions of the same project, for new projects, or simply for sharpening the community’s collective understanding of how to approach smart-contract design on Cardano.
Are we there yet?
While the story so far points to a clear improvement in the overall safety of smart contracts, the journey is far from over. A new Cardano era, Dijkstra, lies ahead, bringing new capabilities — and, almost certainly, new classes of vulnerability that have yet to be uncovered. Design issues remain common across freshly written contracts, and developers are still human, after all, and so prone to the occasional small, unexpected mistake…
Or are they? No retrospective written today can quite dodge the subject — every other article seems to mention AI, and, as you can see, this one is no exception. With the recent explosion in both the use and the capability of LLMs, smart contracts can increasingly be written with their help. It is tempting to imagine that they will eventually reshape how contracts are written and improve their safety to the point where external review becomes unnecessary. We are not there yet. Our most recent audits still turn up genuine mistakes, and LLMs, however capable, make mistakes of their own — all the more so given how complex and expressive the eUTxO model can be.
LLMs are reshaping the way we address audits too. They make us more efficient at building and maintaining our tooling, and at writing audit code. Although we value confidentiality and never feed them client code, we can point them at a contract’s interface — blind to the actual code of the on-chain scripts — to draft endpoints and traces within cooked-validators and enjoy a quicker mapping to its most advanced features. That pairing makes our audits faster and, often, better at surfacing the deep corner-case bugs that might otherwise have slipped through. So the technology cuts both ways: it may help developers build safer contracts, and at the same time it sharpens the audits that check them. Which of these effects will win out, we can’t yet say — but the trickiest findings, such as the design ones we keep running into, or the ones fully related to specific business logic, are exactly the kind no amount of automation is likely to catch reliably. On an eUTxO-based blockchain like Cardano, where elegant and expressive design meets intricacy and subtlety, we don’t see third-party review going away any time soon.
Conclusion
This is where the retrospective ends, and what five years and more than twenty audits have looked like from our side. We started out reviewing contracts by hand and gradually built cooked-validators into a framework that lets us schedule attacks along a transaction trace, exploring various possible outcomes; we wrote tools that lasted and tools that did not, learning from both; and we watched the ecosystem broaden, with higher-level languages like Plutarch and Aiken joining Plutus, while Cardano grew new capabilities underneath us. Along the way we caught a lot of bugs, missed a few, and got better.
The encouraging part is that things are improving. Whole classes of Cardano-specific vulnerabilities are fading as the platform and its developers mature, and the contracts we review are cleaner than they used to be. That does not make audits any less useful; it just changes what they catch. The next era, Dijkstra, will bring new capabilities, and with them new ways for things to go wrong. If these five years have taught us anything, it is that every step up in abstraction and capability opens new room for mistakes — and catching those mistakes early is exactly what an audit is for — an endeavour we expect to carry on for another five years at least.
- By stateful we mean that, when altering a skeleton, the tweak has read access to the current blockchain state — the set of available UTxOs and the values they carry, for instance — so its modifications can depend on that context. It cannot, however, modify the state itself; only the transaction skeleton it is applied to.↩
Behind the scenes
Mathieu is an engineer and doctor from France, with skills ranging from model driven engineering to functional programming and formal methods. Mathieu leads the High Assurance Software team.
If you enjoyed this article, you might be interested in joining the Tweag team.