DEV Community

Cover image for The Brief for the Judge: Writing Audit-Ready Documentation
Obinna Duru
Obinna Duru

Posted on

The Brief for the Judge: Writing Audit-Ready Documentation

We have spent the last four posts building MilestoneCrowdfundUpgradeable protocol. We architected an upgradeable proxy, designed a mathematically rigorous escrow engine, fuzz-tested its invariants, and locked the doors against reentrancy attacks. We built a fortress.

But there is one final step. A secure fortress is incredibly dangerous if you don't leave behind a blueprint explaining exactly how the traps and locks are supposed to work.

Most developers hate writing documentation. In Web2, bad documentation means a new developer joins your team and spends three days figuring out an API endpoint instead of one hour. Frustrating, expensive, recoverable.

In Web3, bad documentation is catastrophic. When you hand your code to a security auditor, bad documentation means they might misunderstand the intended behavior of a function. They might classify a deliberate design choice as a vulnerability, forcing you to waste weeks fixing a non-issue. Worse, they might see a real bug, assume it was your intended logic, and let it pass into production where user funds get drained.

In this final post, I want to share exactly how I approached documenting MilestoneCrowdfundUpgradeable from NatSpec comments to architectural READMEs and why I believe documentation is the most underrated security tool in Web3.

The Mindset: Preparing the Brief

When I prepare a protocol for an audit, I think of the documentation as the brief I am handing to a judge before a trial.

The auditor is the judge. The code is the evidence. But without the brief, without the written explanation of intent, the documented invariants, and the explicit threat model, the judge is left to interpret the evidence alone. And interpretation without intent is where dangerous misreadings happen.

But here is the deeper truth: documentation isn't just for auditors. It is for the version of you that comes back to this codebase in six months after working on three other projects. That person has forgotten everything. The comments and the threat model are the only things standing between future-you and a catastrophic misunderstanding of your own protocol.

My rule is simple: If I have to think for more than five seconds about what a piece of code is doing, that piece of code needs a comment. Not because the code is bad, but because the next person reading it shouldn't have to spend those same five seconds. In a paid audit, those five seconds across a thousand lines become expensive hours of confusion.

NatSpec: Before, During, and After

Solidity uses a comment format called NatSpec (Ethereum Natural Language Specification). My approach to writing it happens in three distinct phases:

1. Before (The Specification)
I write the @notice and @param descriptions before I write the function body. This forces me to articulate what the function is supposed to do in plain English before I write a single line of logic. If I cannot describe it clearly in one sentence, I do not understand it well enough to code it yet. The comment becomes the specification; the code is just the implementation.

2. During (The "Why")
While writing the body, I add inline comments at critical decision points. In the last post, we looked at this exact code block:

// CEI: Effects - Update the ledger first, before money moves!
_pledges[_id][msg.sender] = 0;
Enter fullscreen mode Exit fullscreen mode

Any competent developer can read _pledges[_id][msg.sender] = 0 and know what it does. What they cannot know without a comment is that this line is here specifically to prevent a reentrancy attack. My personal rule: The code says "what". Only a comment can say "why".

3. After (The Edge Cases)
After the function is complete, I write the @dev block. This is where I document the non-obvious: the edge cases I considered and rejected, the invariants this function must maintain, and the assumptions that must be true for it to be safe.

The Power of Visual State Machines

A smart contract is a state machine. It has states (Fundraising, Succeeded, Failed, Abandoned) and transitions between those states (finalize, haltCampaign).

You can write all of that in prose, but the human brain does not reason about state machines in paragraphs. It reasons about them visually. The moment I drew the state diagram for MilestoneCrowdfundUpgradeable using PlantUML, a massive gap became visible.

Campaign Lifecycle Diagram

Looking at the diagram, I immediately noticed there was no direct transition from Fundraising to Abandoned. A campaign could only be Abandoned after it Succeeded. That was a correct design decision, you shouldn't be able to abandon a project that never even got funded but seeing it drawn forced me to go back and verify that the Solidity code actually enforced this explicitly. The diagram asked a question the code never explicitly answered.

Diagrams also communicate to people who cannot read Solidity. Your investors, your community, or a tokenomics specialist auditing your math might struggle with the raw code, but they can instantly follow a visual map.

The README Architecture

A lot of Web3 repositories just leave the default Foundry or Hardhat README file. Alternatively, they throw every piece of information into one giant, 3,000-word file.

One giant file is a red flag. It tells an auditor that the author did not think carefully about who would be reading it. I split the MilestoneCrowdfundUpgradeable documentation into specific hubs tailored to specific readers:

  • Protocol_Architecture.md: Written for the Auditor. Their primary question is: What is this protocol supposed to do, and what rules must it never break? This document answers that immediately.
  • Integration_Guide.md: Written for the Frontend Developer. Their primary question is: How do I call these functions correctly without breaking anything? They need the function signatures and error codes, not the threat model.
  • Incident_Response.md: Written for the Security Researcher. If they find a live vulnerability, their primary question is: Who has the authority to pause this, and how do I reach them right now? If they have to dig through installation instructions to find your emergency contact, you are wasting critical seconds during a hack.
  • Threat_Model.md: Written for the Skeptical Reviewer. This is the document most developers skip. It says: Here are the attacks we considered, here is why we are protected, and here are the residual risks we knowingly accepted. You cannot write a threat model without thinking like an attacker, and you cannot think like an attacker without finding things you missed.

A well-structured repository is a signal of engineering maturity. Before an auditor reads a single line of your Solidity, they have already formed a judgment about how seriously you take correctness based on your file structure.

The Hardest Part: Combating Drift

The hardest part is not writing documentation. It is keeping it true.

Code changes. Documentation does not change automatically. The gap between them is one of the most dangerous states a codebase can be in.

I experienced this directly. Early in the design of the fiat bridge, pledgeFiatOnBehalf was intended to apply a reduced platform fee. The NatSpec reflected that. Later, the stakeholder and I decided fiat pledges would be completely fee-exempt on-chain, with fees handled off-chain by Stripe.

I changed the code. I didn't immediately update the comment.

I caught it during a final review pass, but imagine if I hadn't. An auditor reading that comment would have expected fee logic that didn't exist, flagged its absence as a high-severity bug, and forced us to waste days in remediation meetings arguing over a phantom issue.

The discipline I developed from that experience is strict: Every time you change a function body, you update the NatSpec before you close the file. Not before you commit. Immediately. The comment is a structural part of the function, not a separate chore to do later.

The BinnaDev Takeaway

When a junior developer tells me, "My code is clean, it's self-documenting, I don't need to write a README," my response is always the same:

"Show me the part of your code that explains why you made that design decision."

Clean code tells you what. Only documentation tells you why. And in smart contract engineering, the why is where all the security lives. "Self-documenting code" makes sense in Web2 where the worst-case scenario is a confused colleague. It does not make sense in Web3 where the worst-case scenario is a misunderstood intent that costs your users everything they trusted you with.

Here is the practical test: If a professional auditor had to judge whether a specific line in your contract was an intentional design choice or a bug, would your code give them enough information to judge correctly without asking you a single question? If the answer is no, you need documentation.

And I will be completely honest with you on how I execute this practically.

English is not my first language. Documentation is fundamentally an asynchronous communication tool, you are writing for a stranger who cannot ask you a follow-up question, they have to wait for your response. In this context, a grammatical error doesn't just look careless; it creates genuine ambiguity. Did the developer mean this or that? To solve this, I use AI. Agile engineering tells us to prioritize working software over comprehensive documentation. That doesn't mean skip the docs; it means be smart about where your mental energy goes. The architecture, the invariant math, the security decisions, those required 100% of my focus. I used AI to help draft NatSpec suggestions and catch grammatical ambiguity so my thinking wasn't limited by the mechanics of expressing it in a second language. The ideas are mine entirely. The precision of communicating them is a collaboration.

Your code being clean means you are a careful developer. Your protocol being documented means you respect the people who will put their money into what you built. Both are required. You have the tools. Use them.

This concludes our five-part journey building the MilestoneCrowdfundUpgradeable protocol. From upgradeable proxies to defensive math, stateful fuzzing, reentrancy guards, and audit-ready documentation.

I hope this series has given you a real-world look at what security-first Web3 engineering actually looks like. Keep building, stay paranoid, and write excellent code.

Top comments (0)