Auditing Mental Model - preview
Note: This is not published yet, but if you are reaching here then that means i sent it to you for a review or for other reasons. Or else you crawled my site ;) .
This can be assumed as work in progress.
- Introduction
- Gather resources
- Smart contract higher overview
- Manual review
- Functional testing
- Automated review
- Report writing phase
- Fixed code review
Introduction:
This document is not a checklist of vulnerabilities, but rather a structured mental model for approaching smart contract security audits. Over time, I’ve found that adhering to this framework yields consistent results, both in identifying subtle bugs and in cultivating a higher degree of confidence in the robustness of the codebase.
it is most effective when applied with deliberate depth.
This mental model will continue to evolve as I refine my workflows and integrate new ideas.
Current state of this model :
Currenly....
Gather resources:
- Scope
- commit hash
- Documentation and specification links
- Invariant list, if present already.
Smart contract higher overview:
- Go through documentation and whitepaper.
- Take (simplified) math formulas from developers if there are any custom/fancy formulas implemented. it will help you going ahead in the audit without waiting much at critical point.
- Get a very high overview of what project is and idea behind it. e.g chain abstraction, staking application etc. Developer project walkthroughs are helpful.
- Check how smart contracts are working, what is a structure of the project and smart contracts. How they are getting integrated in other contracts. based on that create a diagram.
- Create a project diagram -
- that can be modified over a time as you get more understanding.
- The diagram can go more fancy and detailed depending on the time. I generally use Excalidraw and Lucidchart.
- Example: Staking project with some cross chain functionality:
- Create a project diagram -

Manual review:
- Understand the flow from unit tests:
Test cases are important. It gives you understanding of how the setup is, what state needs to be in place for the users to start using entry point of the system. also it gives you time to test more offensive scenarios (after unit test verification) instead of using all most of the time in testing general scenarios.- When there is math formulas are involved or complex interconnected calls are involved, debugging the values of variables helps sometime to see how the state changes are happening e.g what is the value of variable x/totalStakedEth before the specific internal/external call. or what it becomes after that call.
- Debugging can be done in many ways. depending on which framework you are using. Some examples are.
- Foundry - native debugger, console logs in the contracts, Tenderly debugger, simbolik (foundry cheat code support soon).
- Hardhat - console logs in the contracts, debuggers like hardhat-tracer (
outdated
example article here), Tenderly debugger (example article here).
- Choose least dependent contract to review one by one:
This can be done by looking into a diagram and current understanding of the smart contract functionality. For example in this case its Token contracts that is least dependent ( in fact the functionality from that contract is getting used in other contracts so other smart contracts are dependent on it). its highlighted with blue dotted line. - Checklist reviews:
- Known smart contracts vulnerabilities.
- Smart Contract Weakness Classification (SWC)
- Maintain list of invariants for every functions. (this is something that i have been doing in mind , sometimes it may not be possible in mind as code base grows to more complex and confusing functionality) when needed, time consuming sometime. But useful to check against functions.
- Overall analyze the code offensively in manual and in fixed review.
- Visit all paths:
Visiting all paths in a smart contract is crucial from a security perspective. For example, in the below withdraw() example, every path has its own potential security implications and behavior that needs to be verified. Sometimes these unexpected paths lead us to potential bugs.
Automated testing approaches like fuzzing and F.V can be useful here.
As can be seen below everything boil downs to each path. even the ternary expression is path obviously.
function withdraw(uint256 amount, bool emergency) public {
require(amount > 0, "Withdraw amount must be greater than 0");
uint256 balance = balances[msg.sender];
bool success;
if (emergency) {
success = (address(this).balance >= amount) ? payable(msg.sender).send(amount) : false;
} else {
if (balance >= amount) {
balances[msg.sender] -= amount;
totalBalance -= amount;
success = payable(msg.sender).send(amount);
} else {
success = false;
}
}
if (!success) {
// call other contract to handle failed withdrawal
}
emit Withdrawn(msg.sender, amount, success);
}
The second example below shows every value is dynamic
Because every value (_amount, minRequired) can go above and below the results can vary. If the result is used for some operations like decreasing the drivingScore in burnFuelAndReduce() for example.
E.g depending on the amounts are same (as they were while filling the fuel) or increased or decreased the value of fuelReduction will change and while subtracting it from the drivingScore[msg.sender] it needs to be handled accordingly. which also creates different paths.
If you can see that's the real chaos there!
function fillFuelAndCalculate(uint256 _amount) public {
require(_amount > minRequired, "ERR");
fuelTank[msg.sender] += _amount;
drivingScore[msg.sender] += (_amount * multiplier) / minRequired;
}
function burnFuelAndReduce(uint256 _amount) public {
fuelTank[msg.sender] -= _amount;
uint256 fuelReduction = (_amount * multiplier) / minRequired;
drivingScore[msg.sender] = drivingScore[msg.sender] > fuelReduction ? drivingScore[msg.sender] - fuelReduction : 0;
}
- Write down notes, doubts and edge cases.
One reason i like writing down doubts, ideas, possible issues is, when you start going through these issues as you encounter new path you can see more doubts, possible issues in your mind. so sometimes it becomes a loop where you will keep thinking about new ideas while exploring previous/current one. that's why i believe its good idea to have these things documented so you won't forget them in the chaos.- Take notes for understanding or for further checking.
- Doubts: Research on it, Ask applicable doubts to developers.
- Edge cases: for later testing. E.g: [test] Possible reentrancy in unstake() function.
- Try to break business logic while going through every code block. ( again note down the thing that needs to be tested)
- Write down things to revisit after the code is fixed. E.g: In the review you noticed that if specific functionality would be added based on doubts asked or based on the suggested fixes etc, there are chances of something to go wrong for example. Note it down and check in the Fixed code review.
- The minimal markdown file that i use for taking notes:
# Project Name
## Scope:
GH link:
Commit hash:
## Flow:
1. Manual review
2. Functional testing:
- Unit testing
- Edge case testing
3. Automated testing:
- ...
4. ...
## Resources:
1. Specification doc:
2. Whitepaper:
3. Any other documentation and links.
## ToDo:
*Things to do/ remaining things.*
1. High level things about audit
2. Developer project walk-through.
3. ...
## Common/Doubts/Research:
1. Relearn about specific cross chain service/protocol that's getting used.
## ContractName.sol
*This contains doubts/points/research/potential bugs regarding the ContractName.sol*
1. Confirmed issue 1.
2. Confirmed issue 2.
### Doubts/Research:
1. Checking specific formulas.
2. Checking specific doubt.
3. Check the subtraction on L453 can be a problem.
* Testcases:
1. [test] User should be able to stake.
2. [test] Multiple users should be able to stake.
3. [test] ...
## ContractName2.sol
*This contains doubts/points/research/potential bugs regarding the ContractName2.sol*
...
---
## Questions for developers:
1. Explanation for formulas.
2. Questions about intended logic.
3. ...
- Free time is research time: this is more like note to myself, If you/i get free time before the project deadline then use it for researching, visiting more paths, creating diagrams, scenarios.
Functional testing:
- Check any missing test cases that should be covered.
- Write and test edge cases from notes taken while manually reviewing contracts.
Automated review:
- Triage static analysis warnings from tools like Slither, Wake, aderyn etc.
- Some times warning from these tools are good to notice some leads.
- On the top of static analysis tools like Slither also provide other modules, printers and more.
- Provide written invariants to fuzzing/F.V team.
- Auditing agent, anyone ?
Report writing phase
- Discussing doubts and found issues with internal team assigned to the project.
- Whenever possible i write reports for each audited contracts ( at least for the issues i/we have found till that time. Later the report can be modified as we proceed through the audit and get more understanding and issues)
- Discussing issues with developers to give them overview (for every contract when its done): the goal is the technical dev. members of team should get issues so they can try to fix them. It saves time in big projects once the final report is delivered. (when possible)
- Writing full fledge report:
- Adding/collaborating all the issues.
- Formatting: Indentations, grammar, code snippets in same theme.
- Follow internal processes when working with team/company.
Fixed code review
- Code Comparison (helpful to know which things are updated and if there's something apart from what you are expected):
- Get overview of updated code if something is not expected according to the fixed suggested.
- Go through tests for updated code from developers.
- Write new tests for the updated code if required.
- Go through checklists for updated functionality (depends on what is updated)
- Check things that needs to be revisited from Manual review
- Update the issues status based on the updated code.
- Change status
- Add comments from audit team (when required).
- Add comments from the project team (when required).
While working with teams, going individually from every contract yields the better results. It also helps you to think from whole project point of view. where the approach can be collaborative where lot of brainstorming can happen and then the findings can be shared at the last. you can see many great teams following this structure.
Sometime auditing can be really parallel/live process. e.g. you just got something in your mind and you are excited to go through it etc.