skills/fix-review/SKILL.md
Verify that bug fixes correctly address reported vulnerabilities without introducing new issues. Use when reviewing protocol team fix submissions, during re-audit engagements, or in contest mitigation review phases on Sherlock and Code4rena.
npx skillsauth add 0x-shashi/web3-audit-skills skills/fix-reviewInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Verify that bug fixes correctly address reported vulnerabilities without introducing new issues. A fix review is not a rubber stamp — it requires the same rigor as the original audit, focused on the change boundary and its ripple effects.
Before reviewing the fix, re-read the original finding completely:
## Pre-Review Checklist
- [ ] Finding ID and severity confirmed
- [ ] Root cause fully understood (not just the symptom)
- [ ] Impact clearly documented
- [ ] Original PoC reviewed (if available)
- [ ] All variant instances listed
Apply the 5-Point Fix Validation:
Q: Does the fix address the ROOT CAUSE, or just the symptom?
GOOD FIX (root cause):
Finding: CEI violation in withdraw()
Fix: Added ReentrancyGuard + nonReentrant modifier
→ Prevents ALL reentrancy, not just the specific path
BAD FIX (symptom only):
Finding: CEI violation in withdraw()
Fix: Moved one state update before one external call
→ May still be vulnerable via a different code path
Q: Does the fix cover ALL instances of the vulnerability?
GOOD: Applied nonReentrant to withdraw(), claim(), AND liquidate()
BAD: Applied nonReentrant to withdraw() only (ignored variants)
Check against the variant analysis from the original finding.
Common patterns where fixes introduce new bugs:
| Original Fix | New Vulnerability Introduced |
|---|---|
| Added require(amount > 0) | Now users cannot withdraw dust amounts, funds permanently locked |
| Changed transfer to safeTransfer | ERC777 tokensReceived hook now enables reentrancy |
| Added nonReentrant modifier | Cross-contract reentrancy still possible if guard is per-contract |
| Added access control to function | Legitimate users now blocked from valid operations |
| Changed rounding direction | Opposite rounding error now created for different user type |
| Added deadline check | Deadline is in wrong units (milliseconds vs seconds) |
| Moved state update before call | Read-only reentrancy still returns stale state in view functions |
Q: Does the fix break any existing legitimate functionality?
Check:
- [ ] All existing tests still pass
- [ ] New tests added specifically for the vulnerability
- [ ] Edge cases still handled correctly (0 amount, max amount, empty array)
- [ ] Gas impact is acceptable (new modifier doesn't make function too expensive)
- [ ] Compatibility with existing integrations preserved
Q: Does the fix change only what is necessary?
RED FLAGS:
- Large refactoring alongside the fix (hides changes)
- Unrelated changes bundled in the same PR
- Storage layout changes in upgradeable contracts
- New dependencies added (expanded attack surface)
- Function signatures changed (breaks composability)
// BEFORE (vulnerable):
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient");
token.safeTransfer(msg.sender, amount); // external call first
balances[msg.sender] -= amount; // state update second
}
// AFTER (fixed):
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount; // state update first
token.safeTransfer(msg.sender, amount); // external call second
}
// VERIFICATION:
// ✅ nonReentrant modifier added — prevents re-entry
// ✅ CEI pattern applied — state before interaction
// ✅ Both mitigations applied — defense in depth
// ⚠️ Check: Are claim() and liquidate() also fixed?
// If original PoC was provided, run it against the fixed code
// Expected: PoC should FAIL (revert) after the fix
function test_reentrancy_fix() public {
// Setup attacker contract with reentrancy callback
AttackContract attacker = new AttackContract(vault);
vault.deposit{value: 10 ether}();
// Try the original attack
vm.expectRevert(); // Should revert now
attacker.attack();
// Verify vault funds are intact
assertEq(address(vault).balance, 10 ether);
}
// Test that legitimate functionality still works
function test_withdraw_still_works() public {
vault.deposit{value: 5 ether}();
vault.withdraw(3 ether);
assertEq(vault.balances(address(this)), 2 ether);
}
// Test edge cases
function test_withdraw_zero() public {
// Should this revert or succeed? Check spec.
vault.deposit{value: 5 ether}();
vault.withdraw(0);
}
function test_withdraw_full_balance() public {
vault.deposit{value: 5 ether}();
vault.withdraw(5 ether);
assertEq(vault.balances(address(this)), 0);
}
After reviewing the fix, update the finding status:
| Status | Meaning | |---|---| | Fixed | Root cause fully addressed, all instances covered, no regressions | | Partially Fixed | Some instances fixed, others remain; or fix is incomplete | | Not Fixed | Fix does not address the root cause at all | | Acknowledged | Team accepts the risk, chose not to fix (document reasoning) | | Disputed | Team disagrees with finding validity (document both positions) | | New Issue | Fix introduces a new, different vulnerability |
Template for each finding:
## Finding [ID]: [Title]
**Original Severity**: High
**Fix Status**: Fixed / Partially Fixed / Not Fixed / Acknowledged / Disputed
### Fix Summary
[One-sentence description of what the fix does]
### Fix Analysis
- Root cause addressed: Yes/No
- All instances covered: Yes/No (list missing instances if partial)
- New vulnerabilities: None found / [describe new issue]
- Regressions: None found / [describe regression]
- Tests added: Yes/No
### Verification
[How the fix was verified — PoC test results, code review notes]
### Recommendation (if partially/not fixed)
[What additional changes are needed]
| Fix Pattern | Risk Level | Notes |
|---|---|---|
| Add nonReentrant modifier | Low risk | Best approach — prevents all reentrancy in that function |
| Reorder to CEI | Low risk | Good but may miss cross-function reentrancy |
| Both CEI + nonReentrant | Lowest risk | Defense in depth — recommended |
| Add mutex lock | Medium risk | Custom implementation may have bugs |
| Use transfer() (2300 gas) | High risk | Breaks with EIP-1884 and future gas changes |
| Fix Pattern | Risk Level | Notes |
|---|---|---|
| Add onlyOwner modifier | Low risk | Standard, but check if owner is multisig |
| Add role-based access | Low risk | Use OpenZeppelin AccessControl |
| Add initializer modifier | Low risk | Also call _disableInitializers() in constructor |
| Add require(msg.sender == x) | Medium risk | Inline checks are error-prone vs modifiers |
| Fix Pattern | Risk Level | Notes |
|---|---|---|
| Switch to Chainlink TWAP | Low risk | Ensure heartbeat and staleness checks added |
| Add staleness check | Low risk | Use updatedAt from latestRoundData() |
| Add deviation threshold | Medium risk | Must be tuned per asset — too tight = DoS, too loose = manipulation |
| Switch from spot to TWAP | Low risk | Verify TWAP window is sufficient (30 min minimum) |
| Fix Pattern | Risk Level | Notes |
|---|---|---|
| Use mulDivUp/mulDivDown | Low risk | Explicit rounding direction |
| Add dead shares / virtual offset | Low risk | For ERC4626 first depositor — check amount is sufficient |
| Add minimum deposit | Medium risk | May block legitimate small deposits |
| Switch to SafeCast | Low risk | Reverts on overflow instead of silent truncation |
When the fix is deployed via proxy upgrade:
## Upgrade Safety Checklist
- [ ] Storage layout is backward-compatible (no variable reordering)
- [ ] New state variables added ONLY at the end
- [ ] No removed state variables (use `__gap` slots)
- [ ] Initializer version incremented (reinitializer(2), etc.)
- [ ] Old storage slots not reinterpreted as different types
- [ ] Immutable variables consistent between old and new impl
- [ ] Constructor calls `_disableInitializers()`
| Skill | How It's Used |
|---|---|
| variant-analysis/ | Check if fix covers all variant instances |
| differential-review/ | Compare old vs new code systematically |
| severity/ | Re-assess severity after fix |
| methodology/ | Apply standard verification methodology |
| checklists/ | Use protocol-specific checklist for regression |
development
Systematically hunt for every variant of a discovered vulnerability across the entire codebase. Use when a bug is found and all instances of the same root cause pattern must be identified, or when performing variant analysis during competitive audits on Code4rena or Sherlock.
testing
Use when the user wants to audit TON smart contracts for security vulnerabilities, scan FunC or Tact contracts for message chain replay, bounce handling, or gas issues, review TON DeFi protocols for actor-model concurrency flaws, or analyze asynchronous message passing security.
tools
Analyze ERC20/ERC721/ERC1155 token implementations for non-standard behavior, fee-on-transfer mechanics, rebasing logic, blacklists, pausability, and integration risks. Use when reviewing protocols that interact with external tokens or implementing token-related features.
testing
Use when the user wants to audit Sui Move smart contracts, scan Sui-specific patterns including object ownership, shared objects, or dynamic fields, review Sui DeFi protocols for object model security issues, or analyze Sui-specific transaction and consensus patterns.