Fix Review Skill

SkillSecurity

Lets your agent check whether a fix commit resolves audit findings without introducing new bugs.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Fix Review Skill skill

About this capability

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.

What this skill tells your AI

The instructions your AI receives, as published by 0x-shashi/web3-audit-skills in skills/fix-review/SKILL.md and read by ahel’s review.

Purpose

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.

When to Trigger

  • After a protocol team submits fixes for audit findings
  • During re-audit engagement for upgraded contracts
  • When reviewing a PR that addresses a security issue
  • Contest mitigation review phase (Sherlock, Code4rena)
  • Any code change to a previously-audited contract

Fix Review Framework

Phase 1: Understand the Finding

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

Phase 2: Analyze the Fix

Apply the 5-Point Fix Validation:

1. Root Cause Resolution
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
2. Completeness Check
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.

3. No New Vulnerabilities Introduced

Common patterns where fixes introduce new bugs:

Original FixNew Vulnerability Introduced
Added require(amount > 0)Now users cannot withdraw dust amounts, funds permanently locked
Changed transfer to safeTransferERC777 tokensReceived hook now enables reentrancy
Added nonReentrant modifierCross-contract reentrancy still possible if guard is per-contract
Added access control to functionLegitimate users now blocked from valid operations
Changed rounding directionOpposite rounding error now created for different user type
Added deadline checkDeadline is in wrong units (milliseconds vs seconds)
Moved state update before callRead-only reentrancy still returns stale state in view functions
4. No Regressions
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
5. Minimality
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)

Phase 3: Test the Fix

Manual Verification
// 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?
Reproduce Original PoC Against Fix
// 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);
}
Write Regression Tests
// 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);
}

Phase 4: Severity Re-Assessment

After reviewing the fix, update the finding status:

StatusMeaning
FixedRoot cause fully addressed, all instances covered, no regressions
Partially FixedSome instances fixed, others remain; or fix is incomplete
Not FixedFix does not address the root cause at all
AcknowledgedTeam accepts the risk, chose not to fix (document reasoning)
DisputedTeam disagrees with finding validity (document both positions)
New IssueFix introduces a new, different vulnerability

Phase 5: Report Fix Review Results

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]

Common Fix Patterns and Their Risks

Reentrancy Fixes

Fix PatternRisk LevelNotes
Add nonReentrant modifierLow riskBest approach — prevents all reentrancy in that function
Reorder to CEILow riskGood but may miss cross-function reentrancy
Both CEI + nonReentrantLowest riskDefense in depth — recommended
Add mutex lockMedium riskCustom implementation may have bugs
Use transfer() (2300 gas)High riskBreaks with EIP-1884 and future gas changes

Access Control Fixes

Fix PatternRisk LevelNotes
Add onlyOwner modifierLow riskStandard, but check if owner is multisig
Add role-based accessLow riskUse OpenZeppelin AccessControl
Add initializer modifierLow riskAlso call _disableInitializers() in constructor
Add require(msg.sender == x)Medium riskInline checks are error-prone vs modifiers

Oracle Fixes

Fix PatternRisk LevelNotes
Switch to Chainlink TWAPLow riskEnsure heartbeat and staleness checks added
Add staleness checkLow riskUse updatedAt from latestRoundData()
Add deviation thresholdMedium riskMust be tuned per asset — too tight = DoS, too loose = manipulation
Switch from spot to TWAPLow riskVerify TWAP window is sufficient (30 min minimum)

Arithmetic Fixes

Fix PatternRisk LevelNotes
Use mulDivUp/mulDivDownLow riskExplicit rounding direction
Add dead shares / virtual offsetLow riskFor ERC4626 first depositor — check amount is sufficient
Add minimum depositMedium riskMay block legitimate small deposits
Switch to SafeCastLow riskReverts on overflow instead of silent truncation

Upgrade-Specific Fix Review

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()`

Integration Points

SkillHow 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

Signals

GitHub stars
60
Forks
10
Last commit
Feb 2026
Advanced
Catalog kind
skill
Gateway key
fix-review
Source
github.com/0x-shashi/web3-audit-skills