3.9.5 Post-Audit Remediation
The audit report has arrived. It contains findings — perhaps a handful, perhaps dozens — classified by severity, each with a description, proof-of-concept, and recommended fix. The development team's response to this report determines whether the audit was worth what it cost.
A common failure mode: the report is filed, a few findings are fixed, the protocol launches, and the team congratulates itself on being "audited." This is the worst outcome the audit can produce. The audit's findings are the output of the engagement — the value is captured only when those findings are actually addressed in the code, verified, and (where appropriate) disclosed publicly to inform the broader community.
This subsection covers the post-audit phase: triaging findings by severity, implementing fixes correctly, requesting fix verification, and the disclosure timing question. The mechanics are not glamorous, but the discipline applied here is what converts the audit from a credential into a security improvement.
The Report's Structure
A well-written audit report typically contains:
- Executive summary — high-level findings, overall risk assessment, key recommendations
- Scope summary — what was reviewed, what was excluded, the commit hash
- Methodology — how the review was conducted, tools used, areas of focus
- Findings — the substantive content, ordered by severity
- Appendices — gas analysis, code metrics, additional observations
Each finding includes:
- Title — short descriptive name
- Severity — Critical / High / Medium / Low / Informational (sometimes Gas Optimization as a separate category)
- Affected files/functions — where the issue is located
- Description — what the bug is and why it matters
- Proof of concept — code or transaction trace demonstrating exploitability
- Recommended fix — how to address the issue
- Discussion — additional context, related considerations, alternatives considered
Reports vary in format and depth across audit paths — firm reports are typically more polished; contest reports vary by platform; bug bounty disclosures are more terse. The structure above is generally applicable.
Triaging Findings by Severity
Not all findings warrant the same response. The severity classification is the auditor's guidance about how urgently each issue needs to be addressed. The team should engage with each finding deliberately, not reflexively.
Critical Severity
Definition: Directly exploitable for substantial loss, with no required preconditions an attacker cannot satisfy.
Examples: Re-entrancy that drains contract balance, missing access control on a critical function, broken signature verification.
Required response: Fix immediately, before any further deployment or update. If the contract is already in production, this is an incident — treat it as such (see Section 2.9 on incident response). The fix and its verification become the highest priority for the team.
High Severity
Definition: Exploitable with specific but achievable conditions, or causing significant but bounded loss. The exploit might require specific market conditions, governance interaction, or victim cooperation.
Examples: Front-running vulnerability with substantial financial impact, oracle manipulation requiring meaningful but feasible capital, privilege escalation under specific timing.
Required response: Fix before launch (if pre-production) or in the next release (if post-launch). Critical and High findings are non-negotiable — they get fixed.
Medium Severity
Definition: Real bug with limited exploitability or limited impact. The exploit may require unusual conditions, the impact may be partial, or the attack may be self-limiting.
Examples: Gas griefing that imposes cost but doesn't drain funds, edge-case rounding errors, governance weakness with timelock protection.
Required response: Fix in the next planned release. Most Medium findings should be fixed; the rare exception is when the fix introduces more risk than the finding itself, which should be documented explicitly.
Low Severity
Definition: Code-quality issues, deviations from best practice, minor inefficiencies. Often not exploitable but worth addressing.
Examples: Missing event emissions, inconsistent use of named constants, suboptimal data structures.
Required response: Fix at the team's convenience. Often batched into routine maintenance releases.
Informational
Definition: Observations that aren't strictly bugs but the auditor wanted to surface — suggestions, alternative approaches, documentation gaps.
Examples: "Consider extracting this constant," "The naming convention here differs from elsewhere," "NatSpec could be expanded for these functions."
Required response: Triage and address those that improve code quality. Many can be deferred or declined.
Gas Optimizations
Definition: Suggestions for reducing gas costs without functional impact.
Examples: "Cache this storage read," "Use unchecked block here," "Custom errors instead of revert strings."
Required response: Optional. Gas optimizations are a separate cost-benefit calculation; many teams ignore them entirely, others integrate the high-impact suggestions and skip the rest.
The Triage Workflow
For each finding, the team should produce a documented decision:
## Finding F-04: Reentrancy in withdraw()
**Severity:** High
**Status:** Fixing
**Owner:** alice
**Target commit:** audit-remediation-v1
**Approach:** Add nonReentrant modifier; reorder withdraw to apply CEI
[Notes from team discussion]
- The original implementation followed CEI for some paths but not the
emergencyWithdraw path. The fix unifies the pattern across both.
- Cross-references: Section 3.7.1 in our internal docs
**Fix verification:** Requested from auditor
**Disclosure:** Public after fix verified + 30 days
For findings the team disputes:
## Finding F-12: Missing zero-address check on setOperator()
**Severity:** Medium
**Status:** Disputed — accepted with mitigation
**Owner:** bob
[Notes]
- The auditor recommended require(_operator != address(0)) in setOperator.
- Our position: setting to address(0) is a deliberate "unset" pattern;
the operator is checked at use-site rather than at setting time.
- Mitigation: documented this in NatSpec.
**Disclosure:** Note in public response that we acknowledged but didn't change
The triage document becomes the team's formal response to the audit. Auditors who deliver well-structured reports appreciate receiving well-structured responses; the document also functions as institutional memory for future audits.
Implementing Fixes
A fix that introduces a new bug is worse than the original finding. The discipline that prevents this:
Each Fix Is a Separate Commit (or Small PR)
One finding, one fix, one commit message that references the finding ID:
Fix F-04: Add reentrancy guard to withdraw
The original withdraw() function followed CEI for the primary path but
not for emergencyWithdraw. This commit:
- Adds the nonReentrant modifier to both functions
- Reorders state updates in emergencyWithdraw to comply with CEI
- Adds a Foundry test that proves the guard works against a malicious recipient
Closes #F-04
This structure makes fix verification mechanical. The auditor reviewing the remediation can map each commit to a specific finding and verify the fix doesn't extend beyond what the finding required.
Each Fix Includes a Test
Section 3.8 emphasizes the test-that-would-have-caught-it pattern. The remediation phase is when those tests get written.
For every finding, write a test that:
- Demonstrates the original vulnerability (the test fails on the pre-fix code)
- Demonstrates the fix works (the test passes on the post-fix code)
These tests stay in the test suite indefinitely as regression protection. If a future refactor reintroduces the vulnerability, the test catches it.
function test_F04_reentrancyOnWithdraw_isBlocked() public {
// Set up an attacker contract that would re-enter withdraw
Attacker attacker = new Attacker(address(vault));
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
vault.deposit{value: 1 ether}();
// Attempt the reentrancy attack — should revert
vm.expectRevert("ReentrancyGuard: reentrant call");
vm.prank(address(attacker));
attacker.attack();
// Verify state is consistent
assertEq(vault.balanceOf(address(attacker)), 1 ether);
}
Don't Over-Fix
A common failure: the fix for one finding goes beyond what the finding requires, introducing changes that weren't reviewed. The auditor reviewed code at commit X. The fix should address the finding at commit X + minimal changes. Substantial refactoring as part of remediation creates new code that wasn't audited.
The discipline: the minimum change that resolves the finding is the right change. Refactoring opportunities surfaced during remediation should be tracked separately and addressed in a future engagement.
Don't Cluster Fixes
Resist the temptation to "fix several findings in one commit." Each finding's fix should be isolated, even if multiple findings touch the same file. The fix verification process needs to map cleanly from finding to commit; bundling breaks that mapping.
Branch Management
A typical remediation branch structure:
main # active development continues
└── audit-v1 (tag) # the audited commit
└── audit-v1-remediation # fixes for audit findings
├── fix/F-04
├── fix/F-07
├── fix/F-12
└── ...
Each fix is a small PR into audit-v1-remediation. When all fixes are merged, the remediation branch is itself merged into main (or whatever ongoing development branch exists). The fix verification round audits audit-v1-remediation against the original audit-v1 baseline.
Fix Verification
The audit's value is incomplete until the fixes are verified. A team that fixes findings without verification is gambling that their fixes are correct.
Scope of Fix Verification
The fix verification round typically covers:
- All Critical and High severity findings — must be verified
- Most Medium findings — usually verified
- Low and Informational findings — optionally verified; often skipped
The verifier reviews each fix in the context of the original finding: does this commit actually resolve the issue? Does it introduce any new issues? Are there related cases the fix should have covered but didn't?
How Fix Verification Differs from the Original Audit
The verification round is faster and narrower than the original review. The auditor isn't searching for new issues; they're checking specific patches against specific findings. A typical verification round takes 20-30% of the original audit's time.
The verifier should be the same researcher who found the issue, when possible. Continuity reduces context-rebuilding cost.
Verification Outcomes
For each fix, the auditor produces one of three judgments:
- Resolved. The fix correctly addresses the finding. No further work needed.
- Partially resolved. The fix addresses the main issue but missed a related case, or addresses the issue in some scenarios but not others. Additional fix work required.
- Not resolved. The fix doesn't actually address the finding (sometimes because the team misunderstood the issue, sometimes because the fix is broken). Back to the drawing board.
A finding marked "Partially resolved" or "Not resolved" requires another remediation cycle. The protocol shouldn't launch until all critical and high findings are marked "Resolved."
When the Fix Verification Surfaces New Issues
Sometimes the auditor reviewing a fix notices a new issue — either a bug introduced by the fix itself, or an adjacent issue that wasn't in the original report. The proper handling:
- If the new issue is introduced by the fix: Treat it as a fix verification failure. The team resolves it; verification continues.
- If the new issue is adjacent: Treat it as a new finding in a new mini-engagement. Document it formally; agree on remediation; verify separately. Don't try to fold it into the original audit's scope.
The boundary matters because audit reports become public documents. A new finding discovered post-audit should be in a post-audit communication, not retroactively inserted into the original report.
Public Disclosure
Once the audit is complete and the critical/high findings are fixed, the question becomes: when (and how) is the audit report made public?
The Standard Pattern
Most audit engagements include public release of the report. The standard timing:
- Initial private review — audit happens, findings shared with team, fixes implemented
- Fix verification — critical findings verified as resolved
- Disclosure window — team has 30 days to deploy fixes to production (if applicable) before public release
- Public release — report published on auditor's website and/or team's blog
The 30-day window protects users — if the report identifies a vulnerability that's been fixed, publishing the report immediately gives attackers a roadmap to exploit any contract that hasn't yet been patched. The 30 days gives ecosystem participants time to update.
Findings That Should Stay Private
Some findings warrant longer delays or permanent non-disclosure:
- Findings that affect contracts other than the audited one. If the bug exists in a widely-used pattern that other protocols copy, broader coordinated disclosure is appropriate. Reach out to those other protocols privately before going public.
- Findings that the audit revealed but the team chose not to fix. Public disclosure here can give attackers a roadmap to active vulnerabilities. The right approach depends heavily on context — sometimes the right answer is to fix it, sometimes the right answer is private acknowledgment without public publication.
- Sensitive operational details that don't affect security but were necessary for the audit. Multi-sig signer identities, internal team structure, etc. Redact these from public versions.
Working with the Auditor on Disclosure
The auditor often wants to publish the report on their own site (it builds their reputation and serves educational purposes). The team often wants to publish on their own channels. These aren't mutually exclusive — both versions can exist.
Negotiate the disclosure terms before the engagement starts (cover in the scope document, Section 3.9.2). Items to settle:
- When can the report be made public? (typically 30 days after fix verification)
- Where is it published? (auditor's site, team's site, both)
- What can be redacted? (operational details, dispute-resolution discussions)
- Who has approval rights over the final wording? (typically both parties)
Communicating with Users
When the report goes public, the team typically publishes a companion post on their channels. The post should:
- Acknowledge what was found. Don't bury the lede or downplay severity.
- Describe what was fixed. Specifics matter — vague reassurances don't.
- State what wasn't fixed and why. If a finding was disputed or accepted as known risk, explain.
- Reference the audit report. Link to it directly so anyone interested can read the full findings.
Honesty pays dividends. A team that publishes a balanced post about an audit (including both findings and remediations) builds credibility. A team that claims "passed audit with no issues" when the report shows multiple medium findings loses credibility immediately when readers check the actual report.
The Audit-to-Audit Cycle
For protocols that grow over time, each audit informs the next:
- Q&A from prior audits becomes documentation that the next audit doesn't need to re-ask
- Threat model is updated to reflect findings from the previous engagement
- Test suite is extended with regression tests for every prior finding
- Internal review checklists are expanded to cover patterns auditors have flagged
The team's security maturity compounds across engagements. The first audit may produce 30 findings; the third audit of similar code may produce 5. The reduction isn't because the auditors got worse — it's because the team got better. The art is treating each audit as an investment in security capacity, not just a snapshot of the codebase.
Cross-References
- Audit preparation — Section 3.9.2 covers what was set up before the engagement
- During the audit — Section 3.9.4 covers the engagement dynamics that produced this report
- Pre-audit checklist — Section 3.9.6 is the mechanical checklist version of the preparation phase
- Incident response — Section 2.9 covers what to do if a critical finding indicates active production risk
- Disclosure norms — Section 2.9 also covers the broader question of vulnerability disclosure
- Patterns — Section 3.7 covers the constructive defenses that should be applied during remediation
- Real-world examples — Section 3.10 covers cases where post-audit issues emerged