Back to skills
SKILL.md
Smart Contract Dev
ASecurity'Use when skill: smart-contract-dev. See SKILL.md body for details. Use
- 12 stars
- 0 votes
- 0 copies
- 0 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add oyi77/1ai-skills --skill smart-contract-dev --agent claude-codeAre you the author of Smart Contract Dev?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/oyi77-smart-contract-dev)---
name: smart-contract-dev
description: 'Use when skill: smart-contract-dev. See SKILL.md body for details. Use
when this domain is relevant.'
domain: trading
author: oyi77
license: Apache-2.0
subdomain: trading
tags:
- algorithms
- contract
- dev
- markets
- smart
- trading
version: 1.0.0
category: trading
---
## Overview
Smart contract development for EVM-compatible chains (Ethereum, Polygon, Arbitrum, Optimism, Base) using Solidity 0.8.x. Covers the full lifecycle: architecting, implementing, testing, deploying, and upgrading production-grade contracts. Hardhat and Foundry are the primary toolchains.
The EVM is a single-threaded 256-bit state machine. Storage is persistent; every write costs gas proportional to the number of storage slots touched. Understanding gas economics, proxy storage layouts, and the Solidity compiler's optimization passes separates production contracts from playground code.
## When to Use
**Trigger phrases:**
- "smart contract dev"
- "DeFi protocols (DEX, lending, staking)"
- "NFT collections and marketplaces"
- "DAOs and governance"
**Applicable scenarios:**
- Token contract design and deployment (ERC-20, ERC-721, ERC-1155, ERC-4626)
- DEX AMMs, lending pools, staking protocols, and yield aggregators
- NFT collections with mint, reveal, royalty mechanics
- Upgradeable proxy architecture (UUPS, Transparent, Beacon, Diamond)
- On-chain governance systems (token voting, timelocks, multisig)
- Cross-chain bridge and messaging patterns
- Security audits and vulnerability remediation
## When NOT to Use
- Task is about portfolio management, not trading (use portfolio skills)
- Task is about financial analysis (use analysis skills)
- You need to analyze trade results (use analytics skills)
- Task is about risk management (use risk skills)
- You don't have trading capital
- Task requires financial advice (consult advisors)
- Pure off-chain infrastructure (use backend skills)
- Financial modeling or strategy design (use relevant domain skills)
## Development Toolchains
### Foundry (forge, cast, anvil)
Foundry is the dominant toolchain for Solidity-first development. It compiles with solc directly, runs tests in the Solidity VM (no JavaScript middleware), and provides cheatcodes for EVM manipulation.
```solidity
// Foundry test — forge test -vvvv
contract CounterTest is Test {
Counter public counter;
function setUp() public {
counter = new Counter();
counter.setNumber(0);
}
function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}
function testFuzzIncrement(uint256 x) public {
vm.assume(x > 0 && x < type(uint128).max);
counter.increment(x);
assertEq(counter.number(), x);
}
function testFail_CallByEOA() public {
// Only contract callers allowed — vm.prank sets msg.sender
vm.prank(address(0));
counter.restrictedFn();
}
}
```
Key Foundry cheatcodes:
```solidity
vm.prank(address) // set msg.sender for next call
vm.startPrank(address) // persist msg.sender across calls
vm.deal(address, uint) // set ether balance
vm.roll(uint256) // set block number
vm.warp(uint256) // set block timestamp
vm.store(address,bytes32,bytes32) // write arbitrary storage
vm.load(address,bytes32) // read arbitrary storage
vm.expectRevert(bytes) // assert next call reverts
vm.assume(bool) // filter fuzz inputs
vm.createSelectFork(url) // fork from a live chain
vm.broadcast() // sign and send as current msg.sender
vm.sign(uint256,bytes32) // raw ECDSA signing
```
```bash
# Foundry CLI workflow
forge build # compile
forge test -vvvv # run tests with traces
forge test --match-test testFuzz -vvv # filter by name
forge coverage --report lcov # line+branch coverage
forge snapshot # gas report
cast send $TOKEN "transfer(address,uint256)" $TO $AMT --rpc-url $RPC --private-key $PK
cast call $TOKEN "balanceOf(address)" $USER --rpc-url $RPC
cast sig "transfer(address,uint256)" # compute 4-byte selector
anvil # local dev node (port 8545)
```
### Hardhat
Hardhat is the JavaScript-centric framework. It provides the `hardhat-network` forked-node EVM, plugins for Ethers.js, and the `console.sol` debugging library. Preferred when the team is JS-heavy or when complex deployment scripting is needed.
```javascript
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@openzeppelin/hardhat-upgrades");
module.exports = {
solidity: {
version: "0.8.24",
settings: { optimizer: { enabled: true, runs: 200 } },
},
networks: {
sepolia: { url: process.env.RPC_URL, accounts: [process.env.PK] },
},
etherscan: { apiKey: process.env.ETHERSCAN_KEY },
};
```
```javascript
// Hardhat test — npx hardhat test
const { expect } = require("chai");
const { ethers, upgrades } = require("hardhat");
describe("Token", function () {
it("deploys and mints", async () => {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy("My", "MTK", owner.address);
await token.waitForDeployment();
expect(await token.balanceOf(owner.address)).to.equal(ethers.parseEther("1000000"));
});
it("reverts on insufficient balance", async () => {
const [owner, user] = await ethers.getSigners();
const token = await ethers.deployContract("MyToken", ["My", "MTK", owner.address]);
const errorName = "ERC20Insufficient" + "Balance";
await expect(
token.connect(user).transfer(owner.address, 1)
).to.be.revertedWithCustomError(token, errorName);
});
});
### When to Use Which
|Consideration|Foundry|Hardhat|
|---|---|---|
|Test speed|~10x faster (native Solidity)|Slower (JS EVM)|
|Fuzz + invariant|Native forge fuzz|Via plugin|
|Fork testing|`vm.createSelectFork()`|`network.forking` config|
|Deployment scripts|Solidity scripts + broadcast|ethers.js + upgrades plugin|
|Debugging|`forge test -vvvv` traces|console.sol + stack traces|
|Team language|Solidity-first|JS/TS-first|
|Ecosystem plugins|Minimal|Rich (hardhat-upgrades, tenderly, solidity-coverage)|
**Recommendation:** Use Foundry as primary for testing and development. Use Hardhat for upgradeable deployments (hardhat-upgrades plugin) and complex multi-step deployment orchestration. Many teams use both in the same repo.
## Solidity Deep Dive
### Data Locations
```solidity
// storage — persists on-chain, costs gas on read/write
// memory — temporary, scoped to function execution
// calldata — read-only function input, cheapest for external calls
contract DataLocations {
struct User {
address addr;
uint256 balance;
uint32 lastActive; // packed
}
User[] public users;
// storage ref — points into state, modifications persist
function _loadUser(uint256 id) internal view returns (User storage u) {
u = users[id];
}
// memory copy — snapshot, gas-intensive for large structs
function getUser(uint256 id) external view returns (User memory) {
return users[id];
}
// calldata — zero-copy, external functions only
function batchProcess(User calldata u) external pure returns (uint256) {
// u is read-only, no copy cost
return u.balance;
}
}
```
### Struct Packing
EVM storage slots are 32 bytes. Solidity packs adjacent elementary types smaller than 256 bits into one slot when they fit. Declaration order matters — pack tightly or waste gas.
```solidity
// BAD — 4 slots, 3 wasted bytes each slot
struct Loose {
uint256 id; // slot 0
uint128 amount; // slot 1 (16 bytes, 16 wasted)
uint64 count; // slot 2 (8 bytes, 24 wasted)
uint32 version; // slot 3 (4 bytes, 28 wasted)
}
// GOOD — 2 slots, everything packed
struct Tight {
uint64 count; // \_ packed into slot 0
uint128 amount; // /
uint32 version; // -- slot 0 tail
uint256 id; // slot 1
}
```
Rules: references (address, bytes32, uint256) start new slots. Smaller types after them waste space. Sort descending by size, or group same-sized types.
### Custom Errors
Solidity 0.8.4+ supports `error` types. They are cheaper than revert strings (no ABI-encoded string) and carry parameters.
```solidity
error Unauthorized(address caller);
error InsufficientBalance(uint256 available, uint256 required);
error ZeroAddress();
contract ErrorDemo {
function withdraw(uint256 amount) external {
if (amount > balances[msg.sender]) {
revert InsufficientBalance(balances[msg.sender], amount);
}
// Foundry test asserts:
// vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, 0, 100));
}
}
```
```javascript
// Hardhat/ethers.js
await expect(contract.withdraw(100)).to.be.revertedWithCustomError(
contract,
"InsufficientBalance"
);
```
### Events and Topics
Events are indexed (up to 3 indexed parameters) and their topics enable efficient off-chain filtering. The first topic is always the event signature hash.
```solidity
event Transfer(address indexed from, address indexed to, uint256 value);
event Staked(address indexed user, uint256 amount, uint256 unlockTime);
// Emit as struct for cleaner code
event OrderCreated(Order order);
```
```javascript
// Off-chain — ethers.js
contract.on("Staked", (user, amount, unlockTime, event) => {
console.log(`${user} staked ${amount} until ${unlockTime}`);
});
// Filter by indexed param
const filter = contract.filters.Staked(address); // null = any
contract.queryFilter(filter, 0, "latest");
```
## Testing
### Unit Tests
```solidity
// Foundry
function testDeposit() public {
vm.prank(alice);
vault.deposit{value: 1 ether}();
assertEq(vault.balanceOf(alice), 1 ether);
assertEq(address(vault).balance, 1 ether);
}
function testRevert_ZeroDeposit() public {
vm.prank(alice);
vm.expectRevert(Vault__ZeroDeposit.selector);
vault.deposit{value: 0}();
}
```
### Fork Tests
```solidity
// Test against mainnet state without deploying everything
function testFork_UniswapSwap() public {
string memory rpc = vm.envString("MAINNET_RPC_URL");
vm.createSelectFork(rpc, 19_500_000); // block number
// Use real Uniswap V3 pool
IUniswapV3Pool pool = IUniswapV3Pool(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
assertGt(pool.liquidity(), 0);
}
```
### Fuzz Tests
```solidity
// forge test — runs 256 random inputs by default (configurable)
function testFuzz_MintTotalSupply(uint256 amount) public {
vm.assume(amount > 0 && amount <= MAX_SUPPLY);
token.mint(address(1), amount);
assertEq(token.totalSupply(), amount);
}
// With handler — forge test with --fuzz-runs 10000
}
```
Foundry fuzzer config in `foundry.toml`:
```toml
[fuzz]
runs = 10000
max_test_rejects = 65536
dictionary_weight = 40
```
### Invariant Tests
Invariant tests run random sequences of function calls and verify properties never break. Requires a handler contract.
```solidity
// Invariant A: totalSupply = sum of all balances
contract VaultInvariants is Test {
VaultHarness harness;
function setUp() public {
harness = new VaultHarness();
targetContract(address(harness));
}
function invariant_totalSupply_eq_sumBalances() public {
uint256 totalBal;
for (uint256 i; i < harness.users(); i++) {
totalBal += harness.balanceOf(harness.userAt(i));
}
assertEq(harness.totalSupply(), totalBal);
}
function invariant_noNegativeBalance() public {
// forge invariant runs hundreds of random call sequences
assertGe(harness.minBalance(), 0);
}
}
```
Run: `forge test --inv-runs 1000 --fail-on-revert`
### Differential Testing
Compare Solidity output against a reference implementation (e.g., a Python model):
```solidity
function testFuzz_AMM_math(uint256 x, uint256 y) public {
vm.assume(x > 1e6 && y > 1e6 && x < 1e30 && y < 1e30);
uint256 k = x * y;
uint256 dy = amm.getOutput(x, y, x / 10);
// Compare: python model gives the same result?
// Off-chain: run forge snapshot, diff with python script
assertTrue(dy > 0);
assertLt(dy, y); // invariant: can't drain pool
}
```
## Upgradeable Patterns
### UUPS (Universal Upgradeable Proxy Standard)
Storage lives in the proxy; logic contract holds implementation. Upgrades call `upgradeTo()` on the proxy through the implementation's `_authorizeUpgrade`.
```solidity
// Proxy (deployed once): delegates all calls to implementation via delegatecall
// Implementation (upgraded): must inherit UUPSUpgradeable
contract VaultV1 is UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // prevent implementation self-destruct
}
function initialize(uint256 _value) external initializer {
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
value = _value;
}
function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
}
contract VaultV2 is VaultV1 {
function increment() external {
value++;
}
}
```
Upgrade script:
```javascript
// hardhat-upgrades
const { upgrades } = require("hardhat");
const v1 = await upgrades.deployProxy(
await ethers.getContractFactory("VaultV1"),
[100], { kind: "uups" }
);
await v1.waitForDeployment();
const v2 = await upgrades.upgradeProxy(
await v1.getAddress(),
await ethers.getContractFactory("VaultV2")
);
```
### Transparent Proxy
Proxy admin is a separate address (only admin can upgrade). Users call through the same proxy but never hit upgrade functions. More expensive per call (SLOAD for admin check) but simpler.
```solidity
// OpenZeppelin TransparentUpgradeableProxy
// Admin address gets admin functions; everyone else gets implementation
// Deployment hardhat-upgrades: { kind: "transparent" }
```
### Beacon Proxy
Multiple proxies point to a single beacon contract that stores the implementation address. Update once — all proxies upgrade.
```solidity
// Deploy beacon, then deploy proxies from beacon
// forge script
function run() external {
vm.startBroadcast(deployerPK);
UpgradeableBeacon beacon = new UpgradeableBeacon(address(v1), deployer);
BeaconProxy proxyA = new BeaconProxy(address(beacon), initData);
BeaconProxy proxyB = new BeaconProxy(address(beacon), initData);
// Upgrade both proxies atomically:
beacon.upgradeTo(address(v2));
vm.stopBroadcast();
}
```
### Diamond Proxy (EIP-2535)
Multi-facet upgradeable proxy where each function is routed to a facet contract. Enables upgrading individual functions instead of whole contracts. Storage is managed via diamond-2 storage pattern (typed mapping-based storage).
```solidity
// Facet: implements a slice of the total API
contract DiamondLoupeFacet {
// diamond storage — avoids storage collision
bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage");
struct DiamondStorage {
mapping(bytes4 => FacetAddressAndSelectorPosition) selectorToFacet;
}
function ds() internal pure returns (DiamondStorage storage s) {
bytes32 pos = DIAMOND_STORAGE_POSITION;
assembly { s.slot := pos }
}
}
```
**Use UUPS for most projects.** It's the cheapest per-call. Use Transparent only when the proxy admin role must be separated from the owner. Use Beacon for many identical copies (clone-like with upgradeability). Use Diamond only when the contract exceeds the 24KB contract size limit.
### Storage Gap Convention
Reserve unused storage slots in upgradeable base contracts so future versions can add state without corrupting existing storage:
```solidity
contract BaseV1 {
uint256 public value;
uint256[49] __gap; // reserve 49 slots
}
contract V2 is BaseV1 {
uint256 public newValue; // uses slot 1 (the first gap slot)
uint256[48] __gap;
}
```
## Access Control
### Ownable2Step
Replaces deprecated `Ownable` — the new owner must accept, preventing accidental transfers to an uncontrolled address.
```solidity
import "@openzeppelin/contracts/access/Ownable2Step.sol";
contract MyContract is Ownable2Step {
constructor(address owner) Ownable2Step(owner) {}
function adminMint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
```
### AccessControl (RBAC)
```solidity
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DAOContract is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
```
Foundry test for roles:
```solidity
function test_RevertIf_NotMinter() public {
vm.prank(alice); // alice has no MINTER_ROLE
vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, alice, MINTER_ROLE));
dao.mint(bob, 100);
}
```
### Timelock Controller
```solidity
// OpenZeppelin TimelockController — queued, delayed execution
// Deploy with admin, proposers, executors
TimelockController timelock = new TimelockController(
MIN_DELAY, // 2 days
proposers, // multisig address
executors, // multisig address
admin // deployer (revoked after setup)
);
```
Pattern: make the timelock the `onlyOwner` of the protocol contract. Proposals go through: propose → wait for delay → execute. This gives users time to exit if a malicious upgrade is proposed.
### Multisig (Gnosis Safe)
For production admin operations, use a Gnosis Safe (now Safe) with N-of-M signing. Typical: 2-of-3 or 3-of-5. The Safe address owns protocol admin roles.
Deployment via Safe{Wallet} app at https://app.safe.global. Programmatic interaction via `@safe-global/safe-core-sdk`.
## Gas Optimization
### SSTORE Rules
Writing to storage is the most expensive EVM operation. Key costs:
|Operation|Cost (London)|Notes|
|---|---|---|
|SSTORE zero → non-zero|20,000 gas|First write to slot|
|SSTORE non-zero → non-zero|5,000 gas|Overwrite existing value|
|SSTORE non-zero → zero|2,900 gas + 15,000 refund|But refund capped at 50% of gas used|
|SLOAD (warm)|100 gas|Already accessed this tx|
|SLOAD (cold)|2,100 gas|First access this tx|
```solidity
// Batch storage writes to save gas
function batchMint(address[] calldata to, uint256[] calldata amounts) external {
for (uint256 i; i < to.length; i++) {
_mint(to[i], amounts[i]); // each _mint does multiple SSTOREs
}
}
```
### Calldata vs Memory
```solidity
// BAD — copies array to memory (extra 3 gas per element + expansion)
function process(uint256[] memory data) external { }
// GOOD — reads directly from calldata (zero copy)
function process(uint256[] calldata data) external {
for (uint256 i; i < data.length; i++) {
// data[i] reads from calldata directly
}
}
```
### Unchecked Blocks
Solidity 0.8+ has built-in overflow checks that revert on overflow. When overflow is mathematically impossible (bounded by previous checks), wrap in `unchecked`:
```solidity
// BAD — overflow check every iteration
for (uint256 i = 0; i < n; i++) { ... }
// GOOD — unchecked when i < n is guaranteed to not overflow
for (uint256 i = 0; i < n; ) {
...
unchecked { i++; }
}
```
### ERC-20 Gas Efficient Patterns
```solidity
// Use ERC20Burnable instead of separate burn()
// Use ERC20FlashMint for flash loans (built-in)
// Pack balances in mapping(uint256 => uint256) for multi-account ops
// where lower 128 bits = balance, upper 128 bits = allowance
```
## Assembly (Yul)
### delegatecall
```solidity
address target = logicContract;
(bool success, bytes memory data) = target.delegatecall(
abi.encodeWithSignature("execute(bytes)", payload)
);
require(success, "DelegateCallFailed");
```
### Error Handling
```solidity
// Encode custom error in Yul
assembly {
// revert with selector + params
mstore(0x00, 0xb8e2f161) // Unauthorized.selector
mstore(0x04, caller())
revert(0x00, 0x24)
}
// Safe encoded revert
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x08c379a0) // Error(string) selector
mstore(add(ptr, 0x20), 0x20)
mstore(add(ptr, 0x40), 26)
mstore(add(ptr, 0x60), "Insufficient balance!!!")
revert(ptr, 0x80)
}
```
### Precompile Calls
```solidity
// ecrecover — address 0x01
function recoverSigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
address signer;
assembly {
let ptr := mload(0x40)
mstore(ptr, hash)
mstore(add(ptr, 0x20), v)
mstore(add(ptr, 0x40), r)
mstore(add(ptr, 0x60), s)
let success := staticcall(gas(), 0x01, ptr, 0x80, ptr, 0x20)
if success { signer := mload(ptr) }
}
return signer;
}
// sha256 — address 0x02
function hashSha256(bytes memory data) internal view returns (bytes32) {
bytes32 result;
assembly {
let ptr := mload(0x40)
let len := mload(data)
let dataPtr := add(data, 0x20)
let success := staticcall(gas(), 0x02, dataPtr, len, ptr, 0x20)
if success { result := mload(ptr) }
}
return result;
}
// modexp — address 0x05 (EIP-198)
```
## Security
### Reentrancy
```solidity
// WRONG — external call before state update
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] -= amount; // reentrancy: attacker calls withdraw again
}
// CORRECT — Checks-Effects-Interactions
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount; // effects first
(bool ok, ) = msg.sender.call{value: amount}(""); // then interaction
require(ok);
}
// ReentrancyGuard (OpenZeppelin)
// Use _reentrancyGuardEntered() check in modifiers
```
### Checks-Effects-Interactions (CEI)
Always: validate (checks) → update state (effects) → call external (interactions). This prevents reentrancy by construction.
```solidity
function claimReward() external {
// Check
require(block.timestamp >= rewards[msg.sender].unlockTime);
// Effect
uint256 amount = rewards[msg.sender].amount;
rewards[msg.sender].amount = 0;
// Interaction
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
```
### Pull Over Push
Never push Ether/ tokens to users in a loop — one recipient reverts and the whole tx fails. Instead, let users withdraw their share:
```solidity
// BAD — push
function distribute() external {
for (uint256 i; i < users.length; i++) {
(bool ok, ) = users[i].call{value: amounts[i]}(""); // one revert = all fail
require(ok);
}
}
// GOOD — pull
mapping(address => uint256) public pendingWithdrawals;
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
```
### Oracle Manipulation
```solidity
// WRONG — single-slot price
uint256 price = pool.sqrtPriceX96(); // flash loan can manipulate
// BETTER — TWAP (time-weighted average price)
// Uniswap V3 Oracle:
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30 min ago
secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
uint160 twapPrice = TickMath.getSqrtRatioAtTick(
int24((tickCumulatives[1] - tickCumulatives[0]) / 1800)
);
// BEST — Chainlink price feed for critical prices
// (AggregatorV3Interface with freshness check)
function getPrice() external view returns (uint256) {
(, int256 answer, , uint256 updatedAt, ) = feed.latestRoundData();
require(block.timestamp - updatedAt < 1 hours, "stale price");
require(answer > 0, "invalid price");
return uint256(answer);
}
```
### MEV Protection
```solidity
// Slippage protection
function swap(uint256 amountIn, uint256 minAmountOut) external {
uint256 amountOut = getAmountOut(amountIn);
require(amountOut >= minAmountOut, "slippage");
}
// Deadline
function swap(uint256 amountIn, uint256 amountOutMin, uint256 deadline) external {
require(block.timestamp <= deadline, "expired");
}
// Commit-reveal (for preventing frontrunning)
// submit hash, then reveal
```
## DeFi Patterns
### AMM Exact Math (Uniswap V2-style)
```solidity
// Constant product: x * y = k
function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
internal pure returns (uint256)
{
require(amountIn > 0, "zero in");
require(reserveIn > 0 && reserveOut > 0, "zero reserve");
uint256 amountInWithFee = amountIn * 997; // 0.3% fee
uint256 numerator = amountInWithFee * reserveOut;
uint256 denominator = reserveIn * 1000 + amountInWithFee;
return numerator / denominator;
}
```
### Lending Pool Accounting
```solidity
// Interest rate model: utilization-based
contract LendingPool {
uint256 public totalDeposits;
uint256 public totalBorrows;
uint256 public constant BASE_RATE = 0.05 ether; // 5% base
uint256 public constant SLOPE_1 = 0.10 ether; // up to optimal
uint256 public constant SLOPE_2 = 1.0 ether; // above optimal
uint256 public constant OPTIMAL_UTIL = 0.80 ether;
mapping(address => uint256) public depositIndex; // scaled balance
function getBorrowRate() public view returns (uint256) {
if (totalDeposits == 0) return 0;
uint256 util = totalBorrows * 1e18 / totalDeposits;
if (util <= OPTIMAL_UTIL) {
return BASE_RATE + (util * SLOPE_1 / OPTIMAL_UTIL);
} else {
uint256 excess = util - OPTIMAL_UTIL;
return BASE_RATE + SLOPE_1 + (excess * SLOPE_2 / (1e18 - OPTIMAL_UTIL));
}
}
}
```
### Compounding Strategy
```solidity
// Yearn-style: vault deposits earn yield, shares appreciate
contract Vault {
IERC20 public asset;
uint256 public totalAssets;
function deposit(uint256 amount) external returns (uint256 shares) {
shares = convertToShares(amount);
_mint(msg.sender, shares);
totalAssets += amount;
asset.transferFrom(msg.sender, address(this), amount);
}
function convertToShares(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? assets : assets * supply / totalAssets;
}
function convertToAssets(uint256 shares) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? shares : shares * totalAssets / supply;
}
}
```
## Token Standards
### ERC-20 with Permit (EIP-2612)
Gasless approvals via off-chain signature:
```solidity
// Inherent in OpenZeppelin's ERC20Permit
contract MyToken is ERC20, ERC20Permit {
constructor() ERC20("My", "MTK") ERC20Permit("My") {}
// permit() allows approvals via signed message (no tx from user)
// delegate the approve tx cost to a relayer
}
```
```javascript
// Off-chain permit signing
const { signature } = await signPermit(
token, owner, spender, amount,
await token.nonces(owner.address), deadline, owner
);
await token.permit(owner.address, spender, amount, deadline, v, r, s);
```
### ERC-721 with Enumerable
```solidity
// OpenZeppelin ERC721Enumerable adds tokenOfOwnerByIndex, totalSupply
contract MyNFT is ERC721Enumerable, Ownable {
using Strings for uint256;
string public baseURI;
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_ownerOf(tokenId) != address(0), "nonexistent");
return string.concat(baseURI, tokenId.toString());
}
function safeMint(address to) external onlyOwner {
uint256 tokenId = totalSupply() + 1;
_safeMint(to, tokenId);
}
}
```
### ERC-1155 (Multi-Token)
```solidity
contract My1155 is ERC1155 {
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external {
_mintBatch(to, ids, amounts, "");
}
}
```
### ERC-4626 (Tokenized Vaults)
Standardized yield-bearing vault interface. Compatible with ERC-20.
```solidity
contract YieldVault is ERC4626, Ownable {
constructor(IERC20 _asset)
ERC4626(_asset)
ERC20("Yield Vault", "yVLT")
{}
function totalAssets() public view override returns (uint256) {
return asset.balanceOf(address(this));
}
// Override _deposit to deploy capital into strategies
function _deposit(address caller, address receiver, uint256 assets, uint256 shares)
internal override
{
super._deposit(caller, receiver, assets, shares);
_deployToStrategy(assets);
}
}
```
## Important EIPs
|EIP|Purpose|Key Detail|
|---|---|---|
|EIP-1967|Proxy storage slots|Standardized storage slot for proxy admin (`0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`), implementation slot (`0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`), beacon slot|
|EIP-712|Typed structured data signing|Domain separator + struct hash for human-readable signatures. Used by EIP-2612 permits, meta-transactions|
|EIP-2612|ERC-20 Permit|Gasless approvals via EIP-712 typed signatures|
|EIP-2535|Diamond proxy|Multi-facet upgradeable contract|
|EIP-3156|Flash loans|Standardized flash loan interface (borrower callback)|
|EIP-4626|Tokenized vaults|Yield-bearing vault standard|
## Common Patterns
### Clone Factory (Minimal Proxies)
EIP-1167 — deploy cheap copies of a master contract. Each clone delegates to the master; storage is per-clone.
```solidity
contract CloneFactory {
event CloneCreated(address indexed clone, address indexed owner);
function createClone(address implementation) internal returns (address clone) {
// EIP-1167 bytecode: 3d602d80600a3d3981f3363d3d373d3d3d363d73..._IMPL_ADDR_...5af43d82803e903d91602b57fd5bf3
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d7300000000)
mstore(add(ptr, 0x14), shl(0x60, implementation))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
clone := create(0, ptr, 0x37)
}
}
function deployPool(address template) external returns (address pool) {
pool = createClone(template);
IPool(pool).initialize(msg.sender);
emit CloneCreated(pool, msg.sender);
}
}
```
### Emergency Pause
```solidity
contract Pausable is PausableUpgradeable {
function emergencyPause() external onlyOwner {
_pause();
}
function emergencyUnpause() external onlyOwner {
_unpause();
}
// Override _beforeTokenTransfer to check paused
}
```
### Circuit Breaker
```solidity
contract CircuitBreaker {
uint256 public constant MAX_SWAP_AMOUNT = 100_000e18;
uint256 public lastSwapTimestamp;
uint256 public swapCount;
modifier checkCircuitBreaker(uint256 amount) {
require(amount <= MAX_SWAP_AMOUNT, "amount too high");
require(block.timestamp > lastSwapTimestamp + 1 minutes, "rate limit");
_;
}
}
```
## Red Flags
- **tx.origin for auth** — Use `msg.sender`. `tx.origin` can be manipulated via intermediate contract calls.
- **Unchecked external calls** — Every `.call{value:...}("")` must check return value and follow CEI.
- **Missing slippage** — All DEX interactions must accept `minAmountOut`/`maxAmountIn` from caller.
- **Hardcoded addresses** — No mainnet address should be literal in contract code. Pass via constructor/initializer.
- **Uninitialized upgradeable contracts** — Proxies don't call constructors; must call `initialize()`.
- **Storage collision** — Upgradeable implementations must not change storage layout. Use `__gap`.
- **Missing `_disableInitializers()`** — Implementation contracts must disable initialization to prevent destruction via `initialize` + `selfdestruct`.
- **Unbounded loops** — Loops over dynamic arrays can hit block gas limit.
- **`delegatecall` to untrusted addresses** — Can destroy proxy state.
- **Wrong constructor visibility in upgradeable contracts** — Use `onlyInitializing` modifier.
- **Missing `whenNotPaused` on critical functions** — Bypasses emergency stop.
- **No freshness check on Oracle prices** — Must check `updatedAt`.
## Verification
All contracts must pass security verification before mainnet deployment:
- Checks-Effects-Interactions pattern enforced on all state-changing functions
- ReentrancyGuard on all external state-altering functions
- AccessControl or Ownable2Step for all admin functions
- Upgradeable contracts: `_disableInitializers()` in constructor, `__gap` arrays, UUPS `_authorizeUpgrade` override
- Fork tests validate against mainnet state; fuzz tests ≥1000 runs for core math
- Invariant tests validate protocol invariants (e.g., totalSupply == sum balances)
- All external calls use SafeERC20 or check return values
- Emergency pause mechanisms tested end-to-end
## Verification Checklist
- [ ] All external state changes follow Checks-Effects-Interactions pattern
- [ ] ReentrancyGuard applied to all external state-altering functions
- [ ] AccessControl or Ownable2Step used for all admin functions
- [ ] Upgradeable contracts have `_disableInitializers()` in constructor
- [ ] Proxy contracts use standardized EIP-1967 storage slots
- [ ] Upgradeable contracts include `__gap` arrays for future state
- [ ] UUPS implementations override `_authorizeUpgrade` with `onlyOwner`
- [ ] Unit tests cover happy path, revert conditions, and edge cases
- [ ] Fork tests validate behavior against mainnet state
- [ ] Fuzz tests run with at least 1000 runs for core math
- [ ] Invariant tests validate protocol invariants (totalSupply == sum balances)
- [ ] All `tx.origin` usages are justified (only cases: gas station patterns)
- [ ] External calls check return value or use SafeERC20
- [ ] No unbounded loops over user-controlled arrays
- [ ] Emergency pause mechanisms are tested end to end
- [ ] Deployment scripts include verification on Etherscan
- [ ] `foundry.toml` / `hardhat.config.js` optimizes for appropriate runs
- [ ] Gas snapshots compared before/after optimization changes
## Anti-Rationalization Table
| Rationalization | Reality |
|---|---|
| "I'll just use Ownable, it's simpler" | Ownable2Step prevents accidental transfer of ownership to a dead address. The one extra tx is worth it. |
| "No one will exploit reentrancy, we're small" | All reentrancy attacks target small protocols first. CEI + ReentrancyGuard is mandatory. |
| "Solidity 0.8 overflow checks make SafeMath obsolete" | True for arithmetic, but unchecked blocks are needed for gas optimization, and casting still overflows (`uint8(256)` = 0). |
| "I don't need tests — I'll verify on mainnet" | Every production exploit was preceded by a developer saying this. Foundry tests are faster than a post-mortem. |
| "Upgradeable proxies are too complex, I'll use immutable" | Immutable is fine for tokens. For protocols with evolving logic, the cost of migrating users and liquidity far exceeds the proxy overhead. |
| "Foundry is all I need" | Foundry is excellent for testing. Hardhat's upgrades plugin and deployment scripts are still superior. Use both. |
| "I'll add the emergency pause later" | Adding it later requires an upgrade. If the protocol is upgradeable, deploy it paused and un-pause after verification. If immutable, you can't add it later at all. |
| "Assembly is only for gas golfing" | Yul is essential for precompile calls, efficient error encoding, and patterns that Solidity can't express (e.g., EIP-1167 minimal proxies). |
| "EIP-2535 Diamond is over-engineering" | It is. Use UUPS unless your contract exceeds the 24KB deployment limit. If it does, consider modular architecture, not necessarily Diamond. |
| "Fuzz tests only find obvious bugs" | Foundry fuzz found critical bugs in Uniswap, MakerDAO, and Aave invariants that unit tests missed. They consistently find the edge case you didn't think of. |
## Process
1. **Analyze requirements** — Define contract interfaces, storage layout, access control model, and upgrade path before writing any code. Document the invariants that must always hold.
2. **Choose toolchain** — Prefer Foundry for Solidity-first development. Use Hardhat for upgradeable deployments and complex orchestration. Both can coexist.
3. **Implement storage layout** — Design the struct packing and slot assignment. For upgradeable contracts, plan the `__gap` array. For Diamond, design facet storage positions.
4. **Write tests first (Foundry)** — Unit tests for every external function. Fuzz tests for all arithmetic and boundary conditions. Invariant tests for protocol-level properties.
5. **Implement CEI pattern** — Checks-Effects-Interactions on every state-changing function. Add ReentrancyGuard. Add access control modifiers.
6. **Gas optimize** — Run `forge snapshot`, identify expensive functions, apply gas optimizations (unchecked, calldata, packed structs), verify gas reduction with diff.
7. **Deploy and verify** — Deploy proxy + implementation. Verify on Etherscan (blockscout). Set up multisig ownership.
8. **Post-deployment** — Configure monitoring (Tenderly, Dune), set up alerts on admin function calls, schedule periodic invariant checks.
Attribution
Comments
Loading comments…