The Art of Writing Security Reports
Feb 6, 2025
15 Minutes
Table of Contents
Introduction
Report Structure
Correct Judgement of Severity
Spelling and Grammar
Polishing & Submission
Conclusion
Introduction
Greetings security researchers!
As auditors, it’s our job to aid our fellow developers in securing their protocols thus making Web3 a safer place for finance. However, no matter the quality of the source code review, if the report presented to the developer is subpar and unclear, the audit itself is useless.
The developer doesn’t oversee the actual source code review so remember: the way you write and showcase your findings is the product of your work. Take pride in this, as this is exactly what the developer is paying for.
Through the years we've read countless reports and provided a lot of feedback on them. Now it’s time to cover the most common issues found in reports, breaking down the various aspects of a finding, and explaining why writing a good report is a core requisite to becoming a skilled security researcher.
Report Structure
Title
This is the first thing developers and/or business people will read. It should be easily understandable after reading the entire issue, even for non-technical folks.
A structure that's used for this is (some examples):
“Root Cause In SomeContract May Lead To Some Impact/Vulnerability Resulting In Some Damage”
“Strict Equality in the Staking Contract May Lead To a Denial Of Service Resulting in Stuck Funds”
“Checks, Effects, and Interactions (CEI) Pattern Is Not Followed in the Withdrawal Function of the Staking Contract May Lead To Reentrancy Resulting in the Theft Of Funds”
Summary
A high-level overview of the issue and its impact, summarizing the entire report. At Zokyo, we primarily work with developers, so summaries are usually omitted. However, when included, they serve as an executive summary for business stakeholders.
Description
A more detailed technical description of the issue and where it can be found in the code, describing why it’s an issue. A good structure for this is “the code currently does this, but it's supposed to do that. This is an issue because…”. This is what developers will be interested in as this contains the technical bulk of the details for your bug.
For example:
The VotingEscrow contract is responsible for distributing weights depending on time and what users are voting for. Users can deposit and withdraw their tokens which will allow them to be checkpointed and thus determining which user voted for what. There exists a function deposit_for_admin which allows anybody including smart contracts to deposit for someone else but cannot extend their lock time and deposit for a new user.
The issue lies at the bottom of this function where it will assert that the balance of native tokens within the contract is zero. A malicious user can deploy a contract containing the self destruct opcode (whilst deprecated, still works) to bypass the checks when receive is triggered and ultimately force native token into the contract.
Impact
This will be an articulation of why your issue is important for the developer to fix. What would happen if some hacker exploited this issue or the issue was shipped to staging without a fix? Theft of funds? Bypassing of fees? Denial of service? Frontrunning? It’s up to you to articulate a meaningful impact in your finding so that the developer understands the severity of your issue and will be less likely to acknowledge it or refuse to fix it.
If you can't demonstrate or formulate impact, it's likely that your issue is invalid. It’s also recommended that you justify your severity rating under this section. For example:
The assertion will always fail causing functionalities (including various functionalities for the FeeDistributor contract) relating to deposit_for_admin to be bricked. This was rated a Critical in severity because it’s an incredibly easy and cheap attack to trigger where the absolute minimal amount needs to be forced into the target contract. In addition to this, the issue also affects other contracts as the deposit_for_admin function is relied upon for compounding.
Proof of Concept (Optional, but Highly Recommended)
A Proof of Concept (PoC) is essential to proving that a reported issue is valid. It helps developers quickly understand and verify the vulnerability. For critical issues, a PoC is mandatory unless the issue is self-explanatory or straightforward.
To maintain clarity in the report, create a unit test and host it in a GitHub Gist. Then, provide a high-level overview of what’s happening in your PoC and paste the link at the end of the description.
Make sure your PoC is:
Straightforward – Clear and easy to follow.
Runnable – Provide all necessary setup details.
Well-commented – Help developers quickly grasp the exploit.
If the developer is unsure about running your PoC, specify the framework used and include a full file with the required setup. Bonus points if the PoC is well-structured and documented.
Recommendations
This is probably the most important part of the finding. This is exactly what the developers are paying for when they hire you as a security researcher - to help secure their code by demonstrating what they can do to fortify their code further. The best security review money can buy for a set of contracts is of no use if the recommendations are poorly communicated.
Provide crystal clear recommendations on how the issue can be fixed (no beating around the bush) and some example code on what they can do to refactor the code to prevent the possibility of exploitation for medium to high complexity (NOT severity, complexity) bugs.
Developers may read your issue without code and interpret it incorrectly and thus accidentally implement the wrong fix, which may lead to an alternate exploit path. Providing a clear, working example of code or a clear articulation of how you would fix this issue prevents this possibility. For example:
It is recommended that the assertion statement is modified to store the balance before the execution of the deposit_for_admin and check against this value after execution in place of the original assertion. This can be done by refactoring the function to the following:
function deposit_for_admin(
address _addr,
uint256 _value,
address _deposit_token,
address _transfer_from
) external payable nonReentrant notUnlocked onlyRole(OPERATOR_ROLE) {
uint256 balanceBefore = address(this).balance;
============================== SNIP ==============================
assert(balanceBefore == address(this).balance);
}
Correct Judgement of Severity
A common issue in report writing is over-inflation of severity for a particular finding without sufficient justification. It is important for security researchers to present a finding with the correct judgement of severity (without over inflation of severity) so that developers can prioritise which issues require fixing first and give the correct gauge of their risks. The following definitions for each severity:
Critical
Very easy to exploit with a very high amount of economic damage caused to the protocol and/or its users. A reasonable protocol team must NOT accept this to be within their risk appetite under any circumstances. Proof of concept is desirable as this is an issue of the highest severity where there should not be a shadow of a doubt of its existence.
A direct loss or theft of funds (principal but NOT yield/fees) without limitations or influence of external conditions (edge cases). Denial of service conditions must have funds locked for more than or equal to 1 year without any chance of releasing those funds during this time period (including via admin functions). This may also include a gross violation of access controls which can cause the total loss of ownership of a contract or aid in the full takeover of the target contract or governance such as voting manipulation.
High
Results in the direct theft, loss, or freezing of funds (yield or fees, but NOT principle) with little to some limitations of external conditions where if such conditions exist, causes a high amount of economical damage to a protocol. Issues classed with this severity may also be slightly harder to exploit than critical issues.
High-severity issues include denial of service conditions that can interrupt or freeze funds in the protocol for more than or equal to a year without any chance of releasing those funds (use of admin rescue functions are acceptable to meet the criteria of high severity, but not critical). A violation of access controls that grant the attacker limited privileges resulting in some disruption to the protocol’s day-to-day operations, but not a full contract takeover, is also considered high severity.
Medium
A successful exploit results in the theft, loss, or freezing of funds but requires certain external or edgecase conditions to be met. This includes specific states or relies on human interaction/error where if the bug is triggered, it may result in major economic damage or gross violation of access controls. Any reasonable protocol team must not accept that this is within their risk appetite.
Core functionality for a contract could be broken due to poor coding practices resulting in reverts or logic errors rendering the contract useless if it’s deployed in its current state. Front-running issues that do not meet the impact criteria of High and Critical severity bugs or Denial of service conditions may also be classed as medium in severity if: funds are lost for less than a year (impermanent) or admin rescue functions are used to get the stuck contracts running again.
Low
Findings under the Low severity class are generally very rarely triggered or require significantly exceptional circumstances to be met. The impact of the vulnerability is minor or may only be just an inconvenience to the protocol team BUT still presents a security risk should the issue go unfixed.
The conditions that require exploitation to be successful may be significantly rare or require a trusted actor to behave maliciously, which includes centralisation issues. In addition to this, sanity checks in which invalid parameters are supplied to a function/constructor/initializer by an admin and result in the unlikely damage to the contracts may also be classed as low in severity.
Informational
This issue is not lower nor higher in severity than Low, Medium, High, or Critical issues, these are separate issues in their own right and do NOT represent a security risk. These issues are raised with the protocol team to recommend best engineering practices and suggest a more efficient way to refactor a function.
Informational findings may include an attack vector without a security risk (for example a reentrancy attack vector where no funds can be extracted from the contract), which will prompt the security team to make a suggestion. In this case, it could be a reentrancy guard that prevents the attack vector to be used by a malicious actor should there be an attackpath undiscovered by the auditing team. Gas inefficiencies may also fall under the informational rating.
Spelling and Grammar
When you’re writing your reports, please take into consideration your spelling and grammar as obvious mistakes make the report writer look careless. It’s recommended to utilize any tools you can get your hands on. This will help automate spelling and grammar correction, if this is something you struggle with, thus resulting in higher quality reports.
Form clear concise sentences to get your point across to the developer. The best way to think about this is to assume that the reader doesn’t know the context of your finding. For example:
Google spell checker (Available on Google Docs: right-click underlined red or blue and select the correct terminology or spelling)
ChatGPT (Please take care when pasting sensitive information)
Grammarly (Paid tool but very effective)
Polishing & Submission
Once you finish the audit, insert your issues straight into the main draft report, taking care of duplicate issues and styling. Should there be duplicates, paste in your issue and leave a comment referencing the original issue so they can be consolidated for the best possible issue.
The project manager will divide the issues amongst the auditors for review. For example: issues 1 - 10 will be reviewed by Alice, 11 - 20 reviewed by Bob, 21 - 30 by Charlie, etc. Alternatively, they could be distributed in a random order depending on what the project manager sees fit for the situation and amount of issues.
Review these issues in accordance with our standards to assess the validity of the issues and provide honest feedback to the auditor, in addition to a justification of your feedback.
Leave comments within the audit document once the client has provided their remarks. For resolved issues, include a reference to the resolution in GitHub. For unresolved issues, add a comment explaining why the issue remains unresolved or suggesting how it could improved further.
Styling
Please do try to keep consistent styling with the rest of the issues in the document seeing as we do not have different fonts for different issues. For example:
Font: Arial
Font Size: 11
In sentence code: using default backticks (``)
In line code: Roboto Mono (Normal), size 10.5, using light grey highlight (example below is green/black)
function deposit_for_admin(
address _addr,
uint256 _value,
address _deposit_token,
address _transfer_from
) external payable nonReentrant notUnlocked onlyRole(OPERATOR_ROLE) {
// Some code here
if(_value == 0) {
revert(“Value must be more than zero!”);
}
}
Conclusion
A well-structured audit report is key to getting vulnerabilities fixed and maximizing payouts in bug bounties.
The way you present findings determines how effectively developers can address vulnerabilities. By structuring reports clearly, providing actionable recommendations, and maintaining high professional standards, you ensure that your work has real-world impact.
At Zokyo, we take pride in not only identifying issues but also ensuring they are well-communicated and resolved. A great report today leads to a safer Web3 tomorrow.