agentleFS
kaynetikkaynetik/skills/solidity-security/SKILL.md

solidity-security

Skill1 starsChanged 8 months agoMITView on GitHub

Solidity smart contract security: vulnerability prevention, secure coding patterns, gas-safe optimizations, and audit preparation. Use when writing or reviewing Solidity code for security, auditing contracts, preventing reentrancy/overflow/access-control issues, optimizing gas safely, or preparing contracts for professional audits. Keywords: solidity security, smart contract audit, reentrancy, access control, CEI pattern, front-running, slither, invariant, vulnerability, exploit, secure solidity.

9 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. 0 are common, 1 is distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never use Unicode box-drawing characters (─, ━, ═, etc.) as comment decorators or section separators in generated code.

    line 25
  2. Very specific:

    Never tx.origin for auth - only msg.sender

    line 112
  3. Very specific:

    Pre-0.8.0: Use SafeMath. There is no reason to target < 0.8.0 for new contracts.

    line 128
  4. Very specific:

    Always SafeERC20 for token operations.

    line 183
  5. Very specific:

    Use EIP-712 typed data + nonce + block.chainid in the domain separator.

    line 247
  6. Very specific:

    Never sacrifice correctness for gas.

    line 311
  7. Distinctive:

    Only persist to storage what on-chain logic actually reads.

    line 356

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Solidity Security

Vulnerability prevention, secure patterns, gas-safe optimizations, audit preparation.

A codebase audit, pen test, or findings report belongs to security-audit. Use this skill for Solidity patterns, secure fixes, and audit preparation.


Code Style Rules

No Unicode Separator Comments

Never use Unicode box-drawing characters (─, ━, ═, etc.) as comment decorators or section separators in generated code. This includes patterns like:

// ── State ─────────────────────────────────────────
// ══ Errors ═════════════════════════════════════════

These are AI slop. They carry no semantic value, are invisible noise in diffs, and mark generated code as low-quality. Use plain labels or nothing at all:

// State
mapping(address => uint256) public balances;

// Errors
error InsufficientBalance();

Vulnerabilities & Secure Patterns

1. Reentrancy

External call before state update lets an attacker re-enter mid-execution.

Vulnerable:

function withdraw() public {
    uint256 amount = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    balances[msg.sender] = 0; // state update after call
}

Secure - CEI + ReentrancyGuard:

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract Vault is ReentrancyGuard {
    // Errors
    error InsufficientBalance();
    error TransferFailed();

    function withdraw(uint256 amount) external nonReentrant {
        if (balances[msg.sender] < amount) revert InsufficientBalance();

        balances[msg.sender] -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        if (!ok) revert TransferFailed();
    }
}

Cross-function reentrancy: attacker re-enters a different function that reads stale state. Apply nonReentrant to all functions sharing mutable state, not just the one with the external call.

2. Access Control

Vulnerable:

function withdraw(uint256 amount) public {
    payable(msg.sender).transfer(amount);
}

Secure:

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

// Two-step transfer prevents accidental ownership loss
contract SimpleAccess is Ownable2Step {
    function emergencyWithdraw() external onlyOwner { /* ... */ }
}

// Role-based for multi-actor systems
contract RoleAccess is AccessControl {
    bytes32 public constant OPERATOR = keccak256("OPERATOR");
    function sensitiveOp() external onlyRole(OPERATOR) { /* ... */ }
}
  • Never tx.origin for auth - only msg.sender
  • Ownable2Step over Ownable
  • Validate address(0) on all address parameters

3. Integer Overflow / Underflow

Solidity >= 0.8.0 has checked arithmetic by default. For unchecked blocks, the surrounding logic must prove bounds:

uint256 len = arr.length;
for (uint256 i; i < len; ) {
    // i < len < type(uint256).max, so ++i cannot overflow
    unchecked { ++i; }
}

Pre-0.8.0: Use SafeMath. There is no reason to target < 0.8.0 for new contracts.

4. Front-Running / MEV

Vulnerable:

function swap(uint256 amount, uint256 minOutput) public {
    uint256 output = calculateOutput(amount);
    require(output >= minOutput, "Slippage");
}

Secure - Commit-Reveal:

// State
mapping(bytes32 => uint256) public commitBlock;
uint256 public constant REVEAL_DELAY = 1;

// Errors
error NoCommitment();
error RevealTooEarly();

function commit(bytes32 hash) external {
    commitBlock[hash] = block.number;
}

function reveal(uint256 amount, uint256 minOutput, bytes32 secret) external {
    bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, minOutput, secret));
    if (commitBlock[hash] == 0) revert NoCommitment();
    if (block.number <= commitBlock[hash] + REVEAL_DELAY) revert RevealTooEarly();
    delete commitBlock[hash];
}

Other mitigations: Flashbots Protect / MEV Blocker, slippage + deadline params, batch auctions (CoW Protocol).

5. Unchecked External Calls

Some tokens (USDT) don't return bool - raw .transfer() silently fails.

import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenVault {
    using SafeERC20 for IERC20;

    function send(IERC20 token, address to, uint256 amount) internal {
        token.safeTransfer(to, amount);
    }
}

Always SafeERC20 for token operations.

6. Oracle Manipulation

RiskMitigation
Spot price manipulationTWAP over multiple blocks
Single oracle failureMultiple independent oracles, median
Stale dataFreshness check on updatedAt
Flash loan attackChainlink latestRoundData + sanity bounds
error InvalidPrice();
error StaleOracle();

uint256 public constant MAX_STALENESS = 1 hours;

function getPrice(AggregatorV3Interface feed) internal view returns (uint256) {
    (, int256 price, , uint256 updatedAt, ) = feed.latestRoundData();
    if (price <= 0) revert InvalidPrice();
    if (block.timestamp - updatedAt > MAX_STALENESS) revert StaleOracle();
    return uint256(price);
}

7. Proxy / Upgrade Pitfalls

RiskPrevention
Storage collisionEIP-1967 slots, OZ upgrades plugin
Uninitialized proxyinitialize() in same tx as deploy
Selector clashTransparentUpgradeableProxy or UUPS
Re-initialization_disableInitializers() in constructor
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}

8. Signature Replay

error InvalidSignature();
error NonceAlreadyUsed();

mapping(bytes32 => bool) public usedNonces;

function executeWithSig(
    address signer, uint256 amount, bytes32 nonce, bytes calldata sig
) external {
    if (usedNonces[nonce]) revert NonceAlreadyUsed();

    bytes32 digest = keccak256(abi.encodePacked(
        "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(signer, amount, nonce))
    ));

    if (ECDSA.recover(digest, sig) != signer) revert InvalidSignature();

    usedNonces[nonce] = true;
}

Use EIP-712 typed data + nonce + block.chainid in the domain separator.


Design Patterns

Pull Over Push

// State
mapping(address => uint256) public pending;

// Errors
error NothingToWithdraw();
error TransferFailed();

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

function withdraw() external {
    uint256 amount = pending[msg.sender];
    if (amount == 0) revert NothingToWithdraw();
    pending[msg.sender] = 0;
    (bool ok, ) = msg.sender.call{value: amount}("");
    if (!ok) revert TransferFailed();
}

Emergency Stop

import {PausableUpgradeable} from
    "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";

contract Protocol is PausableUpgradeable, OwnableUpgradeable {
    function deposit() external payable whenNotPaused { /* ... */ }
    function pause() external onlyOwner { _pause(); }
    function unpause() external onlyOwner { _unpause(); }
}

Input Validation

error ZeroAddress();
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);

function transfer(address to, uint256 amount) external {
    if (to == address(0)) revert ZeroAddress();
    if (amount == 0) revert ZeroAmount();
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance(balances[msg.sender], amount);
    }
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

Gas Optimization

Never sacrifice correctness for gas. Every unchecked block must have a provable safety invariant.

Storage Packing

// 1 slot (32 bytes)
struct Packed {
    uint128 balance;
    uint64  lastUpdate;
    uint64  nonce;
}

Calldata Over Memory

function process(uint256[] calldata data) external pure returns (uint256) {
    return data[0];
}

Custom Errors Over Revert Strings

Custom errors (Solidity >= 0.8.4) are cheaper than string reverts and encode structured data.

error WithdrawalExceedsBalance(uint256 requested, uint256 available);

function withdraw(uint256 amount) external {
    if (amount > address(this).balance) {
        revert WithdrawalExceedsBalance(amount, address(this).balance);
    }
}

Events for Off-Chain Data

event DataStored(address indexed user, uint256 indexed id, bytes data);

function storeData(uint256 id, bytes calldata data) external {
    emit DataStored(msg.sender, id, data);
}

Only persist to storage what on-chain logic actually reads.


Security Tooling

CategoryToolPurpose
Static analysisSlitherDetector suite for common vulns
Static analysisAderynRust-based, Foundry-native
FuzzingEchidnaProperty-based Solidity fuzzer
FuzzingMedusaGo-based alternative to Echidna
Formal verificationCertoraProver for critical invariants
Formal verificationHalmosSymbolic execution for Foundry
SMTSMTCheckerBuilt-in bounded model checker

Minimum CI Pipeline

slither . --filter-paths "node_modules|lib"
forge test --fuzz-runs 10000
forge snapshot --check

Testing for Security (Foundry)

import "forge-std/Test.sol";

contract SecurityTest is Test {
    Vault vault;
    address attacker = makeAddr("attacker");

    function setUp() public {
        vault = new Vault();
        vm.deal(address(vault), 10 ether);
    }

    function test_RevertWhen_ReentrancyAttempted() public {
        ReentrancyAttacker exploit = new ReentrancyAttacker(address(vault));
        vm.deal(address(exploit), 1 ether);
        vm.expectRevert();
        exploit.attack();
    }

    function test_RevertWhen_UnauthorizedWithdraw() public {
        vm.prank(attacker);
        vm.expectRevert(Vault.Unauthorized.selector);
        vault.emergencyWithdraw();
    }

    function testFuzz_TransferNeverExceedsBalance(uint256 amount) public {
        vm.assume(amount > 0 && amount <= vault.balanceOf(address(this)));
        vault.transfer(attacker, amount);
        assertEq(vault.balanceOf(attacker), amount);
    }
}

Audit Preparation

Code Quality

  • NatSpec on all public/external functions (@notice, @dev, @param, @return)
  • CEI on every state-changing function with external calls
  • nonReentrant on functions sharing mutable state
  • SafeERC20 for all token operations
  • Custom errors - no revert strings
  • No tx.origin, no block.timestamp randomness, no on-chain secrets

Testing

  • Unit tests: every function, every revert path
  • Fuzz tests: property-based for numeric/state edges
  • Invariant tests: global properties that must always hold
  • Fork tests: integration against mainnet state
  • Static analysis clean (Slither + Aderyn, zero high/medium)

Documentation

  • Architecture overview with contract interaction diagram
  • Threat model: what is trusted, what is adversarial
  • Known limitations and design trade-offs
  • Deployment and upgrade runbook
  • Previous audit reports (if any)

Deployment

  • Access control verified and documented
  • Upgrade path tested end-to-end (if proxy)
  • Testnet deployment validated
  • Emergency pause mechanism tested
  • Multi-sig or timelock on admin functions

NatSpec Template

/// @title  Vault - Collateralized lending vault
/// @notice Accepts collateral deposits and issues vault shares.
/// @dev    UUPS-upgradeable. Tiered fee schedule per ADR-018.
contract Vault {
    /// @notice Deposit collateral into the vault.
    /// @param  token   Collateral token address.
    /// @param  amount  Deposit amount (must be > 0).
    /// @return shares  Vault shares minted.
    function deposit(address token, uint256 amount) external returns (uint256 shares) {
        // ...
    }
}

Quick Reference

VulnerabilityFix
ReentrancyCEI + ReentrancyGuard
Missing access controlOwnable2Step / AccessControl
Unchecked ERC20 returnSafeERC20
Oracle manipulationTWAP + freshness check + sanity bounds
FrontrunningCommit-reveal, slippage + deadline params
Proxy storage collisionEIP-1967, OZ upgrades plugin
tx.origin authmsg.sender
On-chain randomnessChainlink VRF
Unbounded loop DoSPagination or pull pattern
Signature replayEIP-712 + nonce + block.chainid
Flash loan price manipulationTWAP, multiple oracles
Push-payment DoSPull-over-push
Delegatecall to untrustedNever; or restrict target via allowlist

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.