web3-smart-contracts
Use this skill when writing, reviewing, auditing, or deploying Solidity smart contracts. Triggers on Solidity development, smart contract security auditing, DeFi protocol patterns, gas optimization, ERC token standards, reentrancy prevention, flash loan attack mitigation, Foundry/Hardhat testing, and blockchain deployment. Covers Solidity, OpenZeppelin, EVM internals, and common vulnerability patterns.
engineering soliditysmart-contractsdefiweb3securitygas-optimizationWhat is web3-smart-contracts?
Use this skill when writing, reviewing, auditing, or deploying Solidity smart contracts. Triggers on Solidity development, smart contract security auditing, DeFi protocol patterns, gas optimization, ERC token standards, reentrancy prevention, flash loan attack mitigation, Foundry/Hardhat testing, and blockchain deployment. Covers Solidity, OpenZeppelin, EVM internals, and common vulnerability patterns.
web3-smart-contracts
web3-smart-contracts is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Writing, reviewing, auditing, or deploying Solidity smart contracts.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill web3-smart-contracts- The web3-smart-contracts skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Smart contract development on EVM-compatible blockchains requires a unique discipline - code is immutable once deployed, bugs can drain millions, and every computation costs gas. This skill covers Solidity best practices, security-first development, DeFi protocol patterns, gas optimization, and audit-grade code review. It equips an agent to write, review, and audit smart contracts the way a professional auditor at Trail of Bits or OpenZeppelin would approach the task.
Tags
solidity smart-contracts defi web3 security gas-optimization
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair web3-smart-contracts with these complementary skills:
Frequently Asked Questions
What is web3-smart-contracts?
Use this skill when writing, reviewing, auditing, or deploying Solidity smart contracts. Triggers on Solidity development, smart contract security auditing, DeFi protocol patterns, gas optimization, ERC token standards, reentrancy prevention, flash loan attack mitigation, Foundry/Hardhat testing, and blockchain deployment. Covers Solidity, OpenZeppelin, EVM internals, and common vulnerability patterns.
How do I install web3-smart-contracts?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill web3-smart-contracts in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support web3-smart-contracts?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Web3 Smart Contracts
Smart contract development on EVM-compatible blockchains requires a unique discipline - code is immutable once deployed, bugs can drain millions, and every computation costs gas. This skill covers Solidity best practices, security-first development, DeFi protocol patterns, gas optimization, and audit-grade code review. It equips an agent to write, review, and audit smart contracts the way a professional auditor at Trail of Bits or OpenZeppelin would approach the task.
When to use this skill
Trigger this skill when the user:
- Writes or reviews Solidity smart contracts
- Asks about smart contract security vulnerabilities (reentrancy, flash loans, front-running)
- Wants to implement DeFi patterns (AMM, lending, staking, vaults)
- Needs gas optimization for contract deployment or execution
- Asks about ERC standards (ERC-20, ERC-721, ERC-1155, ERC-4626)
- Wants to set up Foundry or Hardhat testing for contracts
- Needs an audit checklist or security review of a contract
- Asks about upgradeable contracts, proxy patterns, or storage layout
Do NOT trigger this skill for:
- Frontend dApp development with ethers.js/wagmi (use frontend-developer instead)
- General cryptography concepts unrelated to smart contracts (use cryptography instead)
Key principles
Security over cleverness - Every line of Solidity is an attack surface. Prefer well-audited OpenZeppelin implementations over custom code. "Don't be clever" is the cardinal rule - clever code hides bugs that drain funds.
Checks-Effects-Interactions (CEI) - Always validate inputs first (checks), update state second (effects), and make external calls last (interactions). This is the primary defense against reentrancy.
Gas is money - Every opcode has a cost paid by users. Optimize storage reads/writes (SSTORE is 20,000 gas), pack structs, use calldata over memory for read-only params, and batch operations where possible.
Immutability demands perfection - Deployed contracts cannot be patched. Use comprehensive testing (100% branch coverage), formal verification where feasible, and always get an independent audit before mainnet deployment.
Composability is a feature and a risk - DeFi's power comes from composability, but every external call is an untrusted entry point. Assume all external contracts are malicious. Use reentrancy guards and validate return values.
Core concepts
The EVM execution model determines everything in Solidity. Storage slots cost 20,000
gas to write (SSTORE) and 2,100 gas to read (SLOAD). Memory is cheap but ephemeral.
Calldata is cheapest for function inputs. Understanding this cost model is essential for
writing efficient contracts. See references/gas-optimization.md.
Solidity's type system and storage layout directly affect security. Storage variables are laid out sequentially in 32-byte slots. Structs can be packed to share slots. Mappings and dynamic arrays use keccak256 hashing for slot computation. Proxy patterns depend on storage layout compatibility between implementations.
DeFi building blocks are composable primitives: AMMs (constant product formula),
lending protocols (collateralization ratios, liquidation), yield vaults (ERC-4626),
staking (reward distribution), and governance (voting, timelocks). Each has well-known
attack vectors. See references/defi-patterns.md.
The security landscape includes reentrancy, flash loan attacks, oracle manipulation,
front-running (MEV), integer overflow (pre-0.8.0), access control failures, and storage
collisions in proxies. A single missed check can drain an entire protocol.
See references/security-audit.md.
Common tasks
Write a secure ERC-20 token
Always inherit from OpenZeppelin. Never implement token logic from scratch.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(uint256 initialSupply)
ERC20("MyToken", "MTK")
Ownable(msg.sender)
{
_mint(msg.sender, initialSupply * 10 ** decimals());
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}Prevent reentrancy attacks
Apply CEI pattern and use OpenZeppelin's ReentrancyGuard:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
// CHECKS
require(balances[msg.sender] >= amount, "Insufficient balance");
// EFFECTS (update state BEFORE external call)
balances[msg.sender] -= amount;
// INTERACTIONS (external call last)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}Optimize gas usage
Key patterns for reducing gas costs:
contract GasOptimized {
// Pack structs - these fit in one 32-byte slot (uint128 + uint64 + uint32 + bool)
struct Order {
uint128 amount;
uint64 timestamp;
uint32 userId;
bool active;
}
// Use immutable for constructor-set values (avoids SLOAD)
address public immutable factory;
uint256 public immutable fee;
// Cache storage reads in memory
function processOrders(uint256[] calldata orderIds) external {
uint256 length = orderIds.length; // cache array length
for (uint256 i; i < length; ) {
// process order
unchecked { ++i; } // safe: i < length prevents overflow
}
}
// Use custom errors instead of require strings (saves deployment gas)
error InsufficientBalance(uint256 available, uint256 required);
function withdraw(uint256 amount) external {
uint256 bal = balances[msg.sender]; // cache SLOAD
if (bal < amount) revert InsufficientBalance(bal, amount);
balances[msg.sender] = bal - amount;
}
}See references/gas-optimization.md for the full optimization checklist.
Implement an ERC-4626 tokenized vault
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
contract YieldVault is ERC4626 {
constructor(IERC20 asset_)
ERC4626(asset_)
ERC20("Yield Vault Token", "yvTKN")
{}
function totalAssets() public view override returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
}
}Set up Foundry testing
// test/Vault.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Vault.sol";
contract VaultTest is Test {
Vault vault;
address alice = makeAddr("alice");
function setUp() public {
vault = new Vault();
vm.deal(alice, 10 ether);
}
function test_deposit() public {
vm.prank(alice);
vault.deposit{value: 1 ether}();
assertEq(vault.balances(alice), 1 ether);
}
function test_withdraw_reverts_on_insufficient_balance() public {
vm.prank(alice);
vm.expectRevert("Insufficient balance");
vault.withdraw(1 ether);
}
// Fuzz testing - Foundry generates random inputs
function testFuzz_deposit_withdraw(uint96 amount) public {
vm.assume(amount > 0);
vm.deal(alice, amount);
vm.startPrank(alice);
vault.deposit{value: amount}();
vault.withdraw(amount);
vm.stopPrank();
assertEq(vault.balances(alice), 0);
}
}Audit a contract for common vulnerabilities
Walk through the contract checking for these in priority order:
- Reentrancy - Any external call before state update? Any missing ReentrancyGuard?
- Access control - Are admin functions properly gated? Is the owner set correctly?
- Integer overflow - Using Solidity < 0.8.0 without SafeMath?
- Oracle manipulation - Using spot prices from DEX pools? Use TWAP or Chainlink.
- Flash loan attacks - Can state be manipulated within a single transaction?
- Front-running - Can transaction ordering affect outcomes? Use commit-reveal.
- Unchecked return values - Are low-level call return values checked?
- Storage collisions - In proxy patterns, does the implementation share storage layout?
See references/security-audit.md for the full audit checklist.
Anti-patterns / common mistakes
| Mistake | Why it's dangerous | What to do instead |
|---|---|---|
| Rolling your own token logic | Subtle edge cases in transfer/approve lead to exploits | Use OpenZeppelin's battle-tested implementations |
Using tx.origin for auth |
Phishing attacks can relay transactions through malicious contracts | Always use msg.sender for authentication |
| External call before state update | Enables reentrancy - the attacker re-enters before balance is deducted | Follow CEI pattern: checks, effects, then interactions |
| Spot price from a DEX pool | Flash loans can manipulate pool reserves in a single tx | Use time-weighted average prices (TWAP) or Chainlink oracles |
| Unbounded loops over arrays | Loops that grow with user count will eventually exceed block gas limit | Use pull-over-push patterns, pagination, or off-chain computation |
Using transfer() or send() |
Hardcoded 2300 gas stipend breaks when receiver has logic | Use call{value: amount}("") with reentrancy guard |
| Magic numbers in code | Makes auditing impossible and introduces misconfiguration risk | Use named constants: uint256 constant MAX_FEE = 1000; |
Gotchas
CEI pattern is violated by modifier usage - A common mistake is putting a reentrancy guard or balance check in a modifier that runs before state updates, then making an external call in the modifier. Modifiers execute around the function body, which means the external call in the modifier runs before
effectsin the function body. Keep the CEI pattern entirely within the function, not split across modifiers.address.transfer()andaddress.send()are deprecated but still taught - Both have a hardcoded 2300 gas stipend that will fail if the recipient is a contract with non-trivial receive logic. The correct pattern is(bool success, ) = addr.call{value: amount}("")combined with a ReentrancyGuard. New code should never usetransfer()orsend().Proxy storage collisions silently corrupt state - In upgradeable proxy patterns (TransparentProxy, UUPS), if the implementation contract declares state variables that overlap with the proxy's admin slot (slot 0), state corruption occurs on every write. Use OpenZeppelin's unstructured storage pattern for admin variables and verify storage layout with
forge inspectbefore upgrading.Foundry fuzz testing hits the default seed repeatedly without corpus expansion -
forge test --fuzz-runs 256uses pseudo-random inputs that may not cover edge cases near integer boundaries. Always definevm.assume()guards for valid ranges and increasefuzz.runsinfoundry.tomlfor security-critical functions. Use invariant testing for stateful properties.Block timestamp is miner-manipulable within ~15 seconds - Using
block.timestampfor time-sensitive logic (token vesting cliffs, auction deadlines) allows miners to shift outcomes by up to ~15 seconds. This is rarely exploitable in practice but becomes significant in high-value time-lock contracts. Useblock.numberwith expected block time for coarser timing.
References
For detailed content on specific topics, read the relevant file from references/:
references/security-audit.md- Full audit checklist, common vulnerability catalog with real exploit examplesreferences/gas-optimization.md- Complete gas optimization guide with opcode costs and storage layoutreferences/defi-patterns.md- DeFi building blocks: AMM, lending, vaults, staking, governance patterns
Only load a references file if the current task requires deep detail on that topic.
References
defi-patterns.md
DeFi Protocol Patterns
AMM (Automated Market Maker)
The constant product formula x * y = k is the foundation of Uniswap V2-style AMMs.
Core mechanics:
- Two tokens in a pool with reserves
xandy - Product
k = x * ymust remain constant after every trade - Buying token A increases its price (less A, more B in pool)
- Liquidity providers (LPs) deposit both tokens proportionally
contract SimpleAMM {
IERC20 public tokenA;
IERC20 public tokenB;
uint256 public reserveA;
uint256 public reserveB;
function swap(address tokenIn, uint256 amountIn)
external
returns (uint256 amountOut)
{
require(amountIn > 0, "Zero amount");
bool isA = tokenIn == address(tokenA);
(uint256 resIn, uint256 resOut) = isA
? (reserveA, reserveB)
: (reserveB, reserveA);
// 0.3% fee
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * resOut) /
(resIn * 1000 + amountInWithFee);
// Update reserves
if (isA) {
reserveA += amountIn;
reserveB -= amountOut;
} else {
reserveB += amountIn;
reserveA -= amountOut;
}
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
(isA ? tokenB : tokenA).transfer(msg.sender, amountOut);
}
}Key risks:
- Impermanent loss for LPs when prices diverge
- Flash loan manipulation of reserves for price oracle attacks
- Sandwich attacks (front-running + back-running user swaps)
Mitigation: Use TWAP oracles, enforce slippage protection (minAmountOut), add
deadline parameters.
Lending protocol
Lending protocols (Aave, Compound) allow users to supply collateral and borrow against it.
Core concepts:
- Collateral factor: Maximum borrow value as % of collateral (e.g., 75%)
- Liquidation threshold: Borrow/collateral ratio triggering liquidation (e.g., 80%)
- Interest rate model: Algorithmic rate based on utilization (borrowed / supplied)
- Health factor:
(collateral * liquidation_threshold) / borrow_value- below 1.0 = liquidatable
contract SimpleLending {
struct UserAccount {
uint256 collateral;
uint256 borrowed;
uint256 lastUpdate;
}
uint256 public constant COLLATERAL_FACTOR = 7500; // 75% in basis points
uint256 public constant LIQUIDATION_THRESHOLD = 8000; // 80%
uint256 public constant BASIS_POINTS = 10000;
mapping(address => UserAccount) public accounts;
function borrow(uint256 amount) external {
UserAccount storage account = accounts[msg.sender];
accrueInterest(account);
uint256 maxBorrow = (account.collateral * COLLATERAL_FACTOR) / BASIS_POINTS;
require(account.borrowed + amount <= maxBorrow, "Exceeds collateral factor");
account.borrowed += amount;
// transfer borrowed tokens to user
}
function isLiquidatable(address user) public view returns (bool) {
UserAccount memory account = accounts[user];
uint256 threshold = (account.collateral * LIQUIDATION_THRESHOLD) / BASIS_POINTS;
return account.borrowed > threshold;
}
}Key risks:
- Oracle manipulation to trigger false liquidations
- Bad debt from rapid price drops exceeding liquidation capacity
- Interest rate model exploits (manipulating utilization ratio)
ERC-4626 tokenized vault
The standard interface for yield-bearing vaults. Users deposit an underlying asset and receive vault shares representing their proportional claim.
Core formula:
shares = assets * totalSupply / totalAssets(on deposit)assets = shares * totalAssets / totalSupply(on withdrawal)
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
contract YieldVault is ERC4626 {
constructor(IERC20 asset_)
ERC4626(asset_)
ERC20("Yield Vault", "yVAULT")
{}
function totalAssets() public view override returns (uint256) {
// Include earned yield in total
return IERC20(asset()).balanceOf(address(this)) + _pendingYield();
}
function _pendingYield() internal view returns (uint256) {
// Calculate yield from strategy
return 0; // placeholder
}
}Key risks:
- First depositor attack: attacker inflates share price to steal from next depositor
- Mitigation: seed vault with initial deposit, or use virtual shares (OpenZeppelin default)
- Donation attacks: sending assets directly to inflate totalAssets
Staking and reward distribution
Distributing rewards proportionally to stakers without iterating over all stakers.
Reward-per-token accumulator pattern (used by Synthetix):
contract StakingRewards {
IERC20 public stakingToken;
IERC20 public rewardToken;
uint256 public rewardRate; // rewards per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public balances;
uint256 public totalSupply;
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) return rewardPerTokenStored;
return rewardPerTokenStored +
((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / totalSupply;
}
function earned(address account) public view returns (uint256) {
return (balances[account] *
(rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
+ rewards[account];
}
function stake(uint256 amount) external updateReward(msg.sender) {
totalSupply += amount;
balances[msg.sender] += amount;
stakingToken.transferFrom(msg.sender, address(this), amount);
}
function withdraw(uint256 amount) external updateReward(msg.sender) {
totalSupply -= amount;
balances[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
}
function getReward() external updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.transfer(msg.sender, reward);
}
}
}Key insight: The accumulator pattern avoids iterating over all stakers. Each user's
reward is calculated as balance * (currentRewardPerToken - userPaidRewardPerToken).
Governance
On-chain governance typically follows the Governor pattern (OpenZeppelin Governor):
Components:
- Token: ERC-20 with voting power (ERC20Votes)
- Governor: Propose, vote, execute lifecycle
- Timelock: Delay between vote passing and execution (gives users time to exit)
Typical flow:
- User creates proposal (requires minimum token threshold)
- Voting delay (e.g., 1 day) for users to delegate/prepare
- Voting period (e.g., 3 days)
- If passed: queued in timelock (e.g., 2 day delay)
- After timelock: anyone can execute
Key risks:
- Flash loan governance attacks (borrow tokens, vote, return in same block)
- Mitigation: Require token balance at a past snapshot block, not current balance
- Low voter turnout allowing minority capture
- Timelock bypass via emergency functions
Common ERC standards reference
| Standard | Purpose | Key functions |
|---|---|---|
| ERC-20 | Fungible tokens | transfer, approve, transferFrom, balanceOf |
| ERC-721 | Non-fungible tokens (NFTs) | ownerOf, transferFrom, approve, safeTransferFrom |
| ERC-1155 | Multi-token (fungible + NFT) | balanceOf, safeTransferFrom, safeBatchTransferFrom |
| ERC-4626 | Tokenized vault | deposit, withdraw, totalAssets, convertToShares |
| ERC-2612 | Permit (gasless approval) | permit - approve via signature, no separate tx |
| ERC-3156 | Flash loans | flashLoan, flashFee, maxFlashLoan |
gas-optimization.md
Gas Optimization Guide
EVM opcode cost reference
Understanding gas costs at the opcode level is essential for meaningful optimization.
| Operation | Gas Cost | Notes |
|---|---|---|
| SSTORE (new value) | 20,000 | Most expensive - writing to new storage slot |
| SSTORE (update) | 5,000 | Updating existing non-zero slot |
| SSTORE (zero -> zero) | 2,900 | EIP-2929 cold access |
| SLOAD (cold) | 2,100 | First read of a slot in a transaction |
| SLOAD (warm) | 100 | Subsequent reads of the same slot |
| MSTORE / MLOAD | 3 | Memory is cheap but ephemeral |
| CALLDATALOAD | 3 | Reading function arguments |
| CALL (external) | 2,600+ | Minimum for calling another contract |
| LOG (event) | 375 + 375/topic | Events are cheap compared to storage |
| CREATE | 32,000 | Deploying a new contract |
Key insight: Storage operations dominate gas costs. A single SSTORE costs more than hundreds of memory or calldata operations combined.
Storage layout optimization
Struct packing
EVM storage uses 32-byte (256-bit) slots. Variables smaller than 32 bytes can share a slot if packed correctly. Solidity packs variables in declaration order.
// BAD: 3 slots (96 bytes of storage)
struct UserBad {
uint8 age; // slot 0 (wastes 31 bytes)
uint256 balance; // slot 1 (full slot)
uint8 level; // slot 2 (wastes 31 bytes)
}
// GOOD: 2 slots (64 bytes of storage)
struct UserGood {
uint256 balance; // slot 0 (full slot)
uint8 age; // slot 1 (1 byte)
uint8 level; // slot 1 (1 byte, packed with age)
}Rule: Group smaller-than-32-byte types together. Place uint256/bytes32/address types on their own.
Packing booleans
Each bool uses a full storage slot by default. Pack multiple bools into a uint256 using bitwise operations, or use a bitmap.
// BAD: 3 storage slots
bool public paused;
bool public initialized;
bool public locked;
// GOOD: 1 storage slot using bit flags
uint256 private _flags;
uint256 constant PAUSED = 1 << 0;
uint256 constant INITIALIZED = 1 << 1;
uint256 constant LOCKED = 1 << 2;
function isPaused() public view returns (bool) {
return _flags & PAUSED != 0;
}Calldata vs memory vs storage
// BAD: copies array to memory (expensive for large arrays)
function sum(uint256[] memory values) external pure returns (uint256) {
uint256 total;
for (uint256 i; i < values.length; ++i) {
total += values[i];
}
return total;
}
// GOOD: reads directly from calldata (no copy)
function sum(uint256[] calldata values) external pure returns (uint256) {
uint256 total;
for (uint256 i; i < values.length; ++i) {
total += values[i];
}
return total;
}Rule: Use calldata for external function parameters that are read-only. Use
memory only when you need to modify the data within the function.
Loop optimization
// BAD: reads .length from storage each iteration, uses checked arithmetic
function processAll() external {
for (uint256 i = 0; i < items.length; i++) {
process(items[i]);
}
}
// GOOD: cached length, unchecked increment, pre-increment
function processAll() external {
uint256 length = items.length; // cache storage read
for (uint256 i; i < length; ) {
process(items[i]);
unchecked { ++i; } // safe: i < length guarantees no overflow
}
}Savings per iteration: ~100 gas (SLOAD for .length) + ~60 gas (overflow check).
Constants and immutables
// BAD: stored in storage (SLOAD on every read)
address public owner;
uint256 public fee = 300;
// GOOD: embedded in bytecode (no SLOAD)
address public immutable owner; // set once in constructor
uint256 public constant FEE = 300; // compile-time constantconstant: value known at compile time, embedded in bytecodeimmutable: value set in constructor, embedded in deployed bytecode- Both avoid SLOAD (2,100 gas saving per read)
Custom errors vs require strings
// BAD: stores string in bytecode, expensive to deploy and emit
require(balance >= amount, "InsufficientBalance: balance too low for withdrawal");
// GOOD: 4-byte selector, minimal bytecode, cheaper to deploy and revert
error InsufficientBalance(uint256 available, uint256 required);
if (balance < amount) revert InsufficientBalance(balance, amount);Custom errors save ~50 gas per revert and significantly reduce deployment cost for contracts with many require statements.
Event optimization
Events are 10-100x cheaper than storage for data that only needs to be read off-chain.
// BAD: storing historical data in storage
mapping(uint256 => Transfer) public transferHistory;
// GOOD: emit events, index off-chain with The Graph or similar
event TransferRecorded(
address indexed from,
address indexed to,
uint256 amount,
uint256 timestamp
);Use indexed on parameters you need to filter by (up to 3 per event). Each indexed
parameter adds 375 gas (LOG topic cost).
Deployment cost reduction
| Technique | Savings | Notes |
|---|---|---|
| Use custom errors | 10-30% deployment | Eliminates long revert strings from bytecode |
| Remove unused imports | Variable | Dead code still compiles into bytecode |
| Use clone/proxy patterns | 80-90% | Minimal proxy (EIP-1167) is ~45 bytes |
| Optimize constructor | Variable | Constructor code is run once but stored in initcode |
| Use optimizer (runs=200) | 5-20% | Foundry: optimizer = true, optimizer_runs = 200 |
Quick reference: optimization checklist
- Storage variables packed into minimal slots
- Constructor-set values use
immutable - Compile-time values use
constant - External function params use
calldatanotmemorywhere possible - Storage reads cached in local variables inside loops
- Array
.lengthcached before loops - Loop counters use
unchecked { ++i; } - Custom errors replace require strings
- Events used instead of storage for off-chain-only data
- Solidity optimizer enabled with appropriate runs count
security-audit.md
Smart Contract Security Audit Guide
Audit checklist
Work through each category in order. For each finding, classify severity as Critical / High / Medium / Low / Informational.
1. Reentrancy
- All external calls happen AFTER state updates (CEI pattern)
-
ReentrancyGuard(nonReentrant) used on functions with external calls - Cross-function reentrancy checked (function A calls B which re-enters A)
- Read-only reentrancy checked (view functions reading stale state during callback)
Real exploit: The DAO hack (2016) - $60M drained because withdraw() sent ETH
before updating the balance. The attacker's fallback function re-entered withdraw()
repeatedly before the balance was set to zero.
// VULNERABLE
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}(""); // external call first
balances[msg.sender] = 0; // state update after - too late
}
// FIXED (CEI pattern)
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // state update first
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}2. Access control
- All privileged functions have proper access modifiers (onlyOwner, role-based)
- Constructor sets initial owner/admin correctly
- No unprotected
selfdestructordelegatecall - Ownership transfer uses two-step pattern (propose + accept)
- No functions accidentally left
publicthat should beinternalorprivate
3. Integer arithmetic
- Solidity >= 0.8.0 used (built-in overflow/underflow checks)
-
uncheckedblocks are truly safe (loop counters, verified math) - Division before multiplication avoided (precision loss)
- Casting between types checked for truncation (uint256 -> uint128)
4. Oracle manipulation
- Spot prices from DEX pools are NOT used for critical decisions
- TWAP (time-weighted average price) used with sufficient window (30 min+)
- Chainlink price feeds use
latestRoundData()with staleness checks - Oracle failure mode handled (stale price, zero price, negative price)
// Chainlink oracle with proper validation
function getPrice(address feed) internal view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = AggregatorV3Interface(feed)
.latestRoundData();
require(price > 0, "Invalid price");
require(block.timestamp - updatedAt < 3600, "Stale price");
return uint256(price);
}5. Flash loan attack vectors
- No single-transaction price manipulation possible
- Governance voting requires token locking (not just balance snapshot)
- Reward calculations use time-weighted mechanisms
- No reliance on
balanceOf(address(this))for accounting (use internal tracking)
6. Front-running / MEV
- Slippage protection on swaps (minAmountOut parameter)
- Commit-reveal schemes for sensitive operations (auctions, governance)
- Deadline parameters on time-sensitive transactions
- No information leakage in pending transactions that can be exploited
7. External call safety
- Return values of
call,delegatecall,staticcallare checked - No use of
transfer()orsend()(2300 gas limit breaks receivers) - External contract addresses validated (not zero address)
- Callbacks from untrusted contracts handled safely
8. Proxy and upgradeability
- Storage layout is append-only between upgrades (no reordering)
- Implementation contract has
initializerinstead ofconstructor -
initialize()can only be called once (use OpenZeppelin's Initializable) - No storage collision between proxy and implementation
- UUPS proxies have
_authorizeUpgradeproperly restricted
9. Token integration
- ERC-20 tokens with fee-on-transfer handled (check actual received amount)
- ERC-20 tokens with rebasing handled or explicitly blocked
-
approverace condition mitigated (useincreaseAllowance/decreaseAllowance) - ERC-777 callback hooks accounted for (reentrancy via token transfer)
10. Denial of service
- No unbounded loops that grow with user count
- Pull-over-push pattern for payments (users withdraw, not contract pushes)
- No single point of failure (owner key compromise doesn't freeze all funds)
- Emergency withdrawal mechanisms for edge cases
Common vulnerability catalog
| Vulnerability | Severity | Detection | Mitigation |
|---|---|---|---|
| Reentrancy | Critical | External call before state update | CEI pattern + ReentrancyGuard |
| Unprotected selfdestruct | Critical | Missing access control on selfdestruct | Remove selfdestruct or add onlyOwner |
| Oracle manipulation | Critical | Using spot price for liquidation/collateral | TWAP or Chainlink with staleness check |
| Unchecked return value | High | address.call() without checking bool |
Always check return value of low-level calls |
| Front-running | High | Transactions visible in mempool | Commit-reveal, slippage protection, deadlines |
| Integer truncation | Medium | Unsafe casting uint256 to smaller types | Use SafeCast library or explicit bounds checking |
| Centralization risk | Medium | Single owner can drain/pause protocol | Multisig, timelock, governance |
| Missing zero-address check | Low | Constructor/setter accepts address(0) | require(addr != address(0)) |
| Floating pragma | Low | pragma solidity ^0.8.0 instead of fixed |
Use exact version: pragma solidity 0.8.20; |
Tools for auditing
| Tool | Purpose | Usage |
|---|---|---|
| Slither | Static analysis - detects common vulnerabilities | slither . in project root |
| Mythril | Symbolic execution - finds deeper bugs | myth analyze src/Contract.sol |
| Foundry invariant tests | Property-based testing | forge test --match-test invariant |
| Echidna | Fuzzer for smart contracts | Define invariants, let it find violations |
| Certora Prover | Formal verification | Write specs in CVL, prove mathematical properties |
Frequently Asked Questions
What is web3-smart-contracts?
Use this skill when writing, reviewing, auditing, or deploying Solidity smart contracts. Triggers on Solidity development, smart contract security auditing, DeFi protocol patterns, gas optimization, ERC token standards, reentrancy prevention, flash loan attack mitigation, Foundry/Hardhat testing, and blockchain deployment. Covers Solidity, OpenZeppelin, EVM internals, and common vulnerability patterns.
How do I install web3-smart-contracts?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill web3-smart-contracts in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support web3-smart-contracts?
web3-smart-contracts works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.