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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- solidity-contracts Installs to ~/.agents/skills/solidity-contracts/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install solidity-contracts The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project solidity-contracts Installs to .agents/skills/solidity-contracts/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/solidity-contracts.zipunzip -o solidity-contracts.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) solidity-contracts PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are reviewing someone else's contract for vulnerabilities: use smart-contract-audit
- The task is token standard implementation specifically: use erc-token-standards
- You need a proxy and upgrade path: use upgradeable-contracts
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.
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
- Read
AGENTS.md,foundry.tomlorhardhat.config.*, andremappings.txtto learn the toolchain, pinned compiler, optimizer runs andevm_version. Match them. - Check the target chains.
evm_versionmust not exceed what the slowest target supports (cancunis safe on major L2s;pragueis the solc 0.8.30 default and some chains lag). - List installed libraries:
ls lib/(Foundry) orpackage.json(@openzeppelin/contractsversion). OpenZeppelin 4.x and 5.x APIs differ; never mix their idioms. - 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
| Decision | Default | Escape hatch |
|---|---|---|
| Pragma | pragma solidity 0.8.28; exact in contracts you deploy, ^0.8.20 in libraries | Match the repo’s pinned version |
| Errors | Custom errors (error NotOwner(address caller);) | require(cond, Err()) works on 0.8.27+ |
| Access control | Ownable2Step for a single admin; AccessControl for several roles | AccessManager for many contracts sharing roles |
| Reentrancy | CEI ordering plus nonReentrant on functions that call out | ReentrancyGuardTransient (OZ 5.1+) on chains with EIP-1153 |
| Sending ETH | Pull pattern: record a balance, let the user withdraw() | Direct call only to trusted or known recipients |
| ERC-20 transfers | SafeERC20.safeTransfer/safeTransferFrom | None: never call transfer on an arbitrary token raw |
| Upgradeability | Immutable contract | Proxy only with a documented reason: see $upgradeable-contracts |
| Math | Checked arithmetic (default); Math.mulDiv for a*b/c | unchecked 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
nonReentrantmodifier does not protect against read-only reentrancy into view functions that other protocols consume. - Never use
tx.originfor authorization. Usemsg.sender. - Never send ETH with
transferorsend. 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
ownerorassetis permanent; a zerotoin a transfer is usually already checked by the token. block.timestampis fine for hours-scale deadlines, not for randomness or second-precision ordering. Never useblockhashorprevrandaofor lotteries with value: use$chainlink-oraclesVRF.- Signatures: use EIP-712 typed data with a nonce and deadline (
EIP712,ECDSA,Noncesfrom OZ). Rawecrecoverreturnsaddress(0)on bad input and allows malleable signatures. delegatecallandselfdestruct: do not use in new code without an audit-grade reason. Since Cancun,selfdestructno 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/catchand bound gas where a griefing revert would block others.
Visibility and data location
| Situation | Choose |
|---|---|
| Called only from outside | external with calldata arrays and strings |
| Called internally too | public, or an internal helper plus an external wrapper |
| Set once in constructor | immutable (stored in bytecode, no SLOAD) |
| Compile-time literal | constant |
| Large structs read once | Struct storage s = map[id]; to avoid copying |
| Value copied and modified locally | memory |
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 ownIVault) so tests can mock them. - Never assume a token has 18 decimals; read
decimals()once at construction and store it as animmutableif you need it. - Return values: decode with
abi.decodeonly after checkingreturndata.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.yaml
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.