Solidity Contracts skill for Codex

Write contracts as if there is no second release. Codex starts from a spec table of state, roles, events and invariants, then writes Solidity that follows checks-effects-interactions, uses custom errors, SafeERC20 and two-step ownership, and avoids traps like transfer(), tx.origin and unbounded loops. Every function ships with tests for its reverts and events, a contract-size check and a static analysis pass.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- solidity-contracts

Installs to ~/.agents/skills/solidity-contracts/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $solidity-contracts, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and refactors Solidity 0.8.x smart contracts with checks-effects-interactions, custom errors, explicit access control, events for every state change, pull payments and OpenZeppelin 5.x building blocks, then proves them with Foundry or Hardhat tests. Use when asked to write a new contract, add a function to an existing one, design storage and roles, handle ETH or token transfers, or turn a spec into deployable Solidity for Ethereum or an EVM L2.

Use it when

  • You need a new Solidity contract or a new function on an existing one
  • A contract moves ETH or tokens and must be safe against reentrancy
  • Roles, events and storage layout need designing before code
  • You are porting a spec or whitepaper mechanic into deployable Solidity

Use something else when

How to check the result

forge build --sizes stays under 24,576 bytes, every function has passing tests for success, each revert and each event, and slither reports no unexplained high or medium findings.

The mistake it prevents

Making the external call before updating balances, or sending ETH with transfer(), which fails for smart contract wallets because of the 2300-gas stipend.

~/.agents/skills/solidity-contracts/SKILL.md

--- name: solidity-contracts description: Writes and refactors Solidity 0.8.x smart contracts with checks-effects-interactions, custom errors, explicit access control, events for every state… license: MIT ---

A contract is done when every external function states who may call it, every state change emits an event, no external call happens before state is final, every revert has a named error, and a test proves each path including the failure ones. Code is immutable once deployed: write as if there is no second release.

Before writing anything

  1. Read AGENTS.md, foundry.toml or hardhat.config.*, and remappings.txt to learn the toolchain, pinned compiler, optimizer runs and evm_version. Match them.
  2. Check the target chains. evm_version must not exceed what the slowest target supports (cancun is safe on major L2s; prague is the solc 0.8.30 default and some chains lag).
  3. List installed libraries: ls lib/ (Foundry) or package.json (@openzeppelin/contracts version). OpenZeppelin 4.x and 5.x APIs differ; never mix their idioms.
  4. Write the spec as a table before code: state variables, roles, functions, who can call, what changes, which events fire, which invariants must hold.

Defaults

DecisionDefaultEscape hatch
Pragmapragma solidity 0.8.28; exact in contracts you deploy, ^0.8.20 in librariesMatch the repo’s pinned version
ErrorsCustom errors (error NotOwner(address caller);)require(cond, Err()) works on 0.8.27+
Access controlOwnable2Step for a single admin; AccessControl for several rolesAccessManager for many contracts sharing roles
ReentrancyCEI ordering plus nonReentrant on functions that call outReentrancyGuardTransient (OZ 5.1+) on chains with EIP-1153
Sending ETHPull pattern: record a balance, let the user withdraw()Direct call only to trusted or known recipients
ERC-20 transfersSafeERC20.safeTransfer/safeTransferFromNone: never call transfer on an arbitrary token raw
UpgradeabilityImmutable contractProxy only with a documented reason: see $upgradeable-contracts
MathChecked arithmetic (default); Math.mulDiv for a*b/cunchecked only with a proof in a comment

Layout of a contract

Order members the way the style guide does so reviewers find things: pragma, imports, errors, events, types, constants and immutables, state, modifiers, constructor, receive/fallback, external, public, internal, private, view and pure last within each group.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract Vault is Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

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

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event PausedSet(bool paused);

    IERC20 public immutable asset;
    bool public paused;
    mapping(address user => uint256) public balanceOf;
    uint256 public totalDeposits;

    constructor(IERC20 asset_, address owner_) Ownable(owner_) {
        asset = asset_;
    }

    function deposit(uint256 amount) external nonReentrant {
        if (paused) revert DepositsPaused();
        if (amount == 0) revert ZeroAmount();
        // Measure what actually arrived: fee-on-transfer tokens deliver less.
        uint256 before = asset.balanceOf(address(this));
        asset.safeTransferFrom(msg.sender, address(this), amount);
        uint256 received = asset.balanceOf(address(this)) - before;
        balanceOf[msg.sender] += received;
        totalDeposits += received;
        emit Deposited(msg.sender, received);
    }

    function withdraw(uint256 amount) external nonReentrant {
        uint256 bal = balanceOf[msg.sender];
        if (amount > bal) revert InsufficientBalance(amount, bal);
        // Effects before interactions.
        balanceOf[msg.sender] = bal - amount;
        totalDeposits -= amount;
        emit Withdrawn(msg.sender, amount);
        asset.safeTransfer(msg.sender, amount);
    }

    function setPaused(bool p) external onlyOwner {
        paused = p;
        emit PausedSet(p);
    }
}

Notes on this example: deposits read the balance delta because fee-on-transfer tokens exist; withdrawals are never paused so users can always exit; Ownable(owner_) is required in OZ 5.x.

Workflow

- [ ] 1. Spec table: state, roles, functions, events, invariants
- [ ] 2. Interfaces and errors first (I<Name>.sol), then implementation
- [ ] 3. Unit tests for every function: happy path, each revert, each event
- [ ] 4. Fuzz tests for arithmetic and invariant tests for accounting ($foundry-testing)
- [ ] 5. forge build --sizes (24,576-byte runtime limit, EIP-170)
- [ ] 6. Static analysis: slither . (if installed) and fix or justify each finding
- [ ] 7. Self-review against the hard rules below; for value-holding code run $smart-contract-audit
- [ ] 8. Deployment script with constructor args from config, never hard-coded keys

Hard rules

  • Checks, effects, interactions. Validate, update storage, emit, then call out. A nonReentrant modifier does not protect against read-only reentrancy into view functions that other protocols consume.
  • Never use tx.origin for authorization. Use msg.sender.
  • Never send ETH with transfer or send. Their 2300-gas stipend breaks smart wallets. Use (bool ok, ) = to.call{value: amt}(""); if (!ok) revert TransferFailed();.
  • Every privileged function carries a modifier or an explicit check and emits an event. Two-step ownership transfer (Ownable2Step) prevents handing control to a typo.
  • No unbounded loops over user-growable arrays. A loop that grows with users becomes uncallable once it exceeds the block gas limit. Paginate or switch to pull patterns.
  • Rounding favours the protocol. Round down what users receive, round up what they owe. Use Math.mulDiv(a, b, c, Math.Rounding.Ceil) where it matters and say why in a comment.
  • Validate addresses that are stored, not every argument: a zero owner or asset is permanent; a zero to in a transfer is usually already checked by the token.
  • block.timestamp is fine for hours-scale deadlines, not for randomness or second-precision ordering. Never use blockhash or prevrandao for lotteries with value: use $chainlink-oracles VRF.
  • Signatures: use EIP-712 typed data with a nonce and deadline (EIP712, ECDSA, Nonces from OZ). Raw ecrecover returns address(0) on bad input and allows malleable signatures.
  • delegatecall and selfdestruct: do not use in new code without an audit-grade reason. Since Cancun, selfdestruct no longer deletes code except in the same transaction as creation.
  • External calls to untrusted contracts can revert, consume all gas or return garbage. Wrap optional ones in try/catch and bound gas where a griefing revert would block others.

Visibility and data location

SituationChoose
Called only from outsideexternal with calldata arrays and strings
Called internally toopublic, or an internal helper plus an external wrapper
Set once in constructorimmutable (stored in bytecode, no SLOAD)
Compile-time literalconstant
Large structs read onceStruct storage s = map[id]; to avoid copying
Value copied and modified locallymemory

Events and errors

  • Index (indexed) the fields people filter by: user addresses, token IDs, order IDs. At most three indexed parameters per event.
  • Emit after the state change so off-chain indexers ($thegraph-subgraphs) see final values.
  • Error parameters should carry what a caller needs to fix the problem (InsufficientBalance(requested, available)), not prose strings.

Interfaces and integration

  • Code against interfaces (IERC20, your own IVault) so tests can mock them.
  • Never assume a token has 18 decimals; read decimals() once at construction and store it as an immutable if you need it.
  • Return values: decode with abi.decode only after checking returndata.length.
  • For anything touching prices, use an oracle with staleness checks, never a spot DEX price that a flash loan can move.

Verification

forge build --sizes
forge test -vvv
forge coverage --report summary    # aim for every branch of value-moving functions
slither . --filter-paths "lib|test" # needs slither-analyzer; pip install may need network

In Hardhat projects use npx hardhat test and npx hardhat compile; see $hardhat-projects. If a test fails, fix the contract, not the expectation, unless the spec changed; say which.

Output contract

When delivering a contract, report:

**Contract:** src/Vault.sol (solc 0.8.28, OZ 5.x, runtime size 6.1 KB)
**Roles:** owner (Ownable2Step): setPaused. Users: deposit, withdraw.
**Invariants:** sum(balanceOf) == totalDeposits; withdraw never paused
**Tests:** 18 unit, 3 fuzz, 1 invariant suite; all pass; branch coverage 96%
**Static analysis:** slither: 0 high, 1 low (timestamp, justified)
**Not covered / needs review:** rebasing tokens unsupported (documented in NatSpec)

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Solidity Contracts"
  short_description: "Write contracts as if there is no second release"
  default_prompt: "$solidity-contracts Write a staking vault contract in Solidity that accepts any ERC-20 and lets users withdraw anytime"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem