Ai solidity-security

Smart contract security patterns, vulnerability prevention, gas optimization, and audit preparation for Solidity development. Use when writing, auditing, or hardening smart contracts against reentrancy, overflow, access control, oracle manipulation, and front-running attacks.

install
source · Clone the upstream repo
git clone https://github.com/wpank/ai
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/devops/solidity-security" ~/.claude/skills/wpank-ai-solidity-security && rm -rf "$T"
manifest: skills/devops/solidity-security/SKILL.md
source content

Solidity Security

Smart contract security patterns, vulnerability prevention, and secure development practices for Solidity.

When to Use

  • Writing or reviewing smart contracts
  • Auditing contracts for vulnerabilities
  • Implementing DeFi protocols
  • Preventing reentrancy, overflow, and access control issues
  • Optimizing gas while maintaining security
  • Preparing contracts for professional audits

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install solidity-security

Critical Vulnerabilities

1. Reentrancy

Attacker calls back into your contract before state is updated.

VariantDescriptionFix
Single-functionRe-enters the same function mid-executionCEI pattern +
ReentrancyGuard
Cross-functionRe-enters a different function sharing state
nonReentrant
on all state-mutating functions
Cross-contractRe-enters through an intermediary contractTreat every external call as re-entry vector
Read-onlyView functions return stale state mid-operationGuard on externally-consumed view functions

Vulnerable:

function withdraw() public {
    uint256 amount = balances[msg.sender];
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] = 0;  // Too late!
}

Secure (Checks-Effects-Interactions + Guard):

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureBank is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        balances[msg.sender] = 0;  // EFFECT before INTERACTION

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

2. Integer Overflow/Underflow

Pre-0.8.0 vulnerability — arithmetic silently wraps around.

// Solidity 0.8+ has built-in overflow/underflow checks
contract SecureToken {
    mapping(address => uint256) public balances;

    function transfer(address to, uint256 amount) public {
        // Automatically reverts on overflow/underflow
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

For Solidity < 0.8.0, use

SafeMath
from OpenZeppelin. Be especially cautious with multiplication before comparison — the BEC Token exploit used
cnt * _value
overflow to mint infinite tokens.

3. Access Control

PatternUse Case
Ownable
/
Ownable2Step
Single-admin contracts
AccessControl
with roles
Multi-role systems
Multisig +
TimelockController
DeFi protocol admin
import "@openzeppelin/contracts/access/Ownable.sol";

contract SecureContract is Ownable {
    function withdraw(uint256 amount) public onlyOwner {
        payable(owner()).transfer(amount);
    }
}

Critical rules:

  • Every
    external
    /
    public
    state-changing function needs access control
  • Never use
    tx.origin
    for authentication — use
    msg.sender
  • Use two-step ownership transfer (
    Ownable2Step
    ) to prevent accidental transfers
  • No single key should control enough power to drain the protocol

4. Front-Running

Attackers observe pending transactions in the mempool and submit their own first.

Mitigation — Commit-Reveal:

contract SecureDEX {
    mapping(bytes32 => bool) public usedCommitments;

    function commitTrade(bytes32 commitment) public {
        usedCommitments[commitment] = true;
    }

    function revealTrade(
        uint256 amount, uint256 minOutput, bytes32 secret
    ) public {
        bytes32 commitment = keccak256(
            abi.encodePacked(msg.sender, amount, minOutput, secret)
        );
        require(usedCommitments[commitment], "Invalid commitment");
        // Perform swap
    }
}

5. Oracle Manipulation

BadGood
AMM spot price (flash-loan manipulable)Chainlink or TWAP oracle
No freshness check on oracle dataValidate
updatedAt
,
answeredInRound
, price > 0
Single oracle with no fallbackPrimary + fallback oracle with circuit breaker
(uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound) =
    priceFeed.latestRoundData();
require(price > 0, "Invalid price");
require(updatedAt > 0, "Round not complete");
require(answeredInRound >= roundId, "Stale round");
require(block.timestamp - updatedAt <= MAX_STALENESS, "Price too old");

6. Signature Replay

Signatures without nonce, chain ID, or contract binding can be replayed.

Use EIP-712 typed data:

import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract SecureRelay is EIP712 {
    mapping(address => uint256) public nonces;

    bytes32 private constant ACTION_TYPEHASH = keccak256(
        "ExecuteAction(address to,uint256 amount,uint256 nonce,uint256 deadline)"
    );

    constructor() EIP712("SecureRelay", "1") {}

    function executeAction(
        address to, uint256 amount, uint256 deadline,
        bytes calldata signature
    ) external {
        require(block.timestamp <= deadline, "Signature expired");
        uint256 currentNonce = nonces[msg.sender]++;
        bytes32 structHash = keccak256(
            abi.encode(ACTION_TYPEHASH, to, amount, currentNonce, deadline)
        );
        bytes32 hash = _hashTypedDataV4(structHash);
        require(ECDSA.recover(hash, signature) == msg.sender, "Invalid sig");
        payable(to).transfer(amount);
    }
}

Security Patterns

Checks-Effects-Interactions (CEI)

The foundational pattern — validate, update state, then interact externally.

function withdraw(uint256 amount) public {
    require(amount <= balances[msg.sender], "Insufficient");  // CHECK
    balances[msg.sender] -= amount;                           // EFFECT
    (bool success, ) = msg.sender.call{value: amount}("");    // INTERACTION
    require(success, "Transfer failed");
}

Pull Over Push

Let users withdraw funds instead of pushing payments to them. Prevents one failed transfer from blocking all others.

contract SecurePayment {
    mapping(address => uint256) public pendingWithdrawals;

    function recordPayment(address recipient, uint256 amount) internal {
        pendingWithdrawals[recipient] += amount;
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "Nothing to withdraw");
        pendingWithdrawals[msg.sender] = 0;
        payable(msg.sender).transfer(amount);
    }
}

Emergency Stop (Circuit Breaker)

import "@openzeppelin/contracts/security/Pausable.sol";

contract EmergencyStop is Pausable, Ownable {
    function criticalFunction() public whenNotPaused { /* ... */ }
    function emergencyStop() public onlyOwner { _pause(); }
    function resume() public onlyOwner { _unpause(); }
}

Gas Optimization

TechniqueWhy
Use
uint256
over smaller types
Smaller types still use 256-bit slot + extra conversion gas
Pack storage variablesMultiple small vars in one 32-byte slot
Use
calldata
over
memory
for args
Avoids copying data to memory
Emit events instead of storingEvents are cheaper than storage for read-only data

Storage packing example:

// 3 variables in 1 slot (gas efficient)
contract PackedStorage {
    uint128 public a;  // Slot 0
    uint64 public b;   // Slot 0
    uint64 public c;   // Slot 0
    uint256 public d;  // Slot 1
}

Proxy & Upgrade Safety

RuleWhy
Use EIP-1967 storage slotsPrevents storage collision between proxy and implementation
Call
_disableInitializers()
in constructor
Prevents attacker from initializing the implementation contract
Use
uint256[50] __gap
in base contracts
Reserves storage slots for future upgrades
Test upgrades on fork before mainnetCatches storage layout incompatibilities

Audit Preparation Checklist

CategoryCritical Items
ReentrancyCEI pattern followed,
ReentrancyGuard
on external-calling functions
Access ControlAll privileged functions protected, no
tx.origin
, initializers guarded
External CallsReturn values checked, no
delegatecall
to untrusted addresses
Token Handling
SafeERC20
used, fee-on-transfer handled, ERC-777 hooks considered
MathSolidity 0.8+,
unchecked
blocks audited, multiply-before-divide
OraclesNo spot price, freshness validated, fallback configured
UpgradesEIP-1967 slots, storage gaps, no re-initialization
Gas/DoSBounded loops, pull payments, no single-user blocking
DocumentationNatSpec on public/external functions, events for state changes
Testing95%+ coverage on critical paths, fuzz testing, invariant testing

Security Analysis Tools

ToolPurpose
SlitherStatic analysis — reentrancy, access control, unchecked calls
MythrilSymbolic execution — overflow, reachability
EchidnaProperty-based fuzz testing
FoundryFuzz testing, invariant testing, fork testing
SecurifyAutomated security scanning

References


NEVER Do

Anti-PatternWhy It Kills
External call before state updateReentrancy — The DAO lost $60M this way
tx.origin
for auth
Phishing through intermediary contracts
AMM spot price as oracleFlash loans make single-block manipulation trivial
Unchecked
delegatecall
to user address
Attacker overwrites your contract storage
Floating pragma (
^0.8.0
)
Pin exact version for reproducible builds
Unbounded loops over storage arraysBlock gas limit DoS — contract becomes unusable
selfdestruct
in shared libraries
Parity froze $150M permanently this way
Signatures without nonce + chain IDReplay attacks across chains and transactions
Push payments in loopsOne failed transfer blocks all subsequent ones
unchecked
blocks without proof of safety
Re-enables the overflow bugs Solidity 0.8 fixed