solidity-guide
SKILL.md
Solidity Guide
Applies to: Solidity 0.8.20+, Ethereum, EVM-compatible chains, DeFi, NFTs
Core Principles
- Security First: Every function is a potential attack surface. Assume adversarial callers.
- Gas Efficiency: On-chain computation is expensive. Optimize storage access and minimize state changes.
- Explicit Over Implicit: Use explicit visibility, explicit types, and named return values.
- Immutability by Default: Prefer
immutableandconstantfor values that do not change after deployment. - Standards Compliance: Use OpenZeppelin for ERC standards. Do not roll your own token logic.
Guardrails
Compiler Version
- ALWAYS specify pragma version range:
pragma solidity ^0.8.20; - Do NOT use floating pragmas in production (e.g.,
>=0.8.0). Pin to a minor range. - Enable the optimizer with at least 200 runs for production deployments.
Security
- NEVER use
tx.originfor authorization. Usemsg.sender. - NEVER use
selfdestructin new contracts (deprecated in Dencun). - ALWAYS use
ReentrancyGuardfrom OpenZeppelin for functions that make external calls. - ALWAYS validate external inputs: check zero addresses, bounds, and array lengths.
- Use
SafeERC20for token transfers (handles non-standard return values). - Emit events for every state-changing operation (required for off-chain indexing).
Gas Optimization
- Pack storage variables: group
uint128,uint64,boolinto single 256-bit slots. - Use
calldatainstead ofmemoryfor read-only external function parameters. - Use custom errors instead of
requirestrings (saves ~50 bytes per error site). - Cache storage reads in local variables when accessed more than once.
- Use
uncheckedblocks for arithmetic that provably cannot overflow. - Prefer
mappingoverarrayfor large datasets (O(1) vs O(n) lookups).
Access Control
- Use OpenZeppelin
AccessControlorOwnable2Step(not plainOwnable). - Separate admin roles: deployer, upgrader, pauser, minter. No single god key.
- Consider a timelock for sensitive admin operations (
TimelockController).
Upgradability
- Use UUPS proxy pattern (preferred) or Transparent proxy when upgradability is required.
- NEVER change storage layout ordering in upgraded implementations.
- Use
@openzeppelin/contracts-upgradeablewithinitializerinstead of constructors. - ALWAYS include a storage gap:
uint256[50] private __gap; - Disable initializers in the constructor:
_disableInitializers();
Key Patterns
Checks-Effects-Interactions (CEI)
Prevents reentrancy by ordering operations: validate, update state, then call externally.
function withdraw(uint256 amount) external nonReentrant {
// 1. CHECKS
if (amount == 0) revert ZeroAmount();
if (balances[msg.sender] < amount) revert InsufficientBalance();
// 2. EFFECTS: update state BEFORE external calls
balances[msg.sender] -= amount;
// 3. INTERACTIONS: external calls last
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) revert TransferFailed();
emit Withdrawn(msg.sender, amount);
}
Pull Over Push
Let users withdraw funds instead of pushing payments to them.
mapping(address => uint256) public pendingWithdrawals;
function claimPayment() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
if (amount == 0) revert NothingToClaim();
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) revert TransferFailed();
emit PaymentClaimed(msg.sender, amount);
}
Guard Checks with Custom Errors
Custom errors are cheaper than require strings and support typed parameters.
error Unauthorized(address caller);
error InsufficientBalance(uint256 requested, uint256 available);
error ZeroAddress();
function transfer(address to, uint256 amount) external {
if (to == address(0)) revert ZeroAddress();
if (balances[msg.sender] < amount) {
revert InsufficientBalance(amount, balances[msg.sender]);
}
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
UUPS Proxy Pattern
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyProtocol is UUPSUpgradeable, OwnableUpgradeable {
uint256 public protocolFee;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() { _disableInitializers(); }
function initialize(address owner, uint256 fee) external initializer {
__Ownable_init(owner);
__UUPSUpgradeable_init();
protocolFee = fee;
}
function _authorizeUpgrade(address) internal override onlyOwner {}
uint256[49] private __gap;
}
Security Checklist
- Reentrancy: Apply
nonReentrantto external-call functions. Follow CEI even with the guard. Watch for cross-function reentrancy on shared state. - Integer Safety: Solidity 0.8+ has overflow checks. Use
uncheckedonly when provably safe. - Front-Running: Use commit-reveal for auctions/voting. Add deadline params to swaps. Use
block.timestamp, notblock.number. - Access Control: Never rely on
tx.origin. UsePausablefor emergency stops.
Testing
Foundry (forge) -- Recommended
// test/Vault.t.sol
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Vault} from "../src/Vault.sol";
contract VaultTest is Test {
Vault public vault;
address public alice = makeAddr("alice");
function setUp() public {
vault = new Vault();
vm.deal(alice, 10 ether);
}
function test_deposit_updates_balance() 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(Vault.InsufficientBalance.selector);
vault.withdraw(1 ether);
}
// Fuzz testing: forge generates random inputs automatically
function testFuzz_deposit_and_withdraw(uint96 amount) public {
vm.assume(amount > 0 && amount <= 10 ether);
vm.startPrank(alice);
vault.deposit{value: amount}();
vault.withdraw(amount);
vm.stopPrank();
assertEq(vault.balances(alice), 0);
}
}
Hardhat (alternative)
Use npx hardhat test with Chai matchers and ethers.js for JS/TS projects.
Tooling
forge build && forge test # Compile and test (Foundry)
forge test -vvvv # Verbose with stack traces
forge test --fuzz-runs 10000 # Extended fuzz testing
forge coverage # Coverage report
forge fmt # Format Solidity files
forge snapshot # Gas snapshot for benchmarking
slither . # Static analysis (Slither)
myth analyze src/Contract.sol # Symbolic execution (Mythril)
npx hardhat compile && npx hardhat test # Hardhat alternative
Pre-Deployment Checklist
-
forge testpasses, Slither reports zero high/medium findings - Gas snapshot compared (
forge snapshot --diff) - Storage layout verified for upgradeable contracts
- Deploy script tested on fork:
forge script --fork-url $RPC_URL - Admin keys secured (multisig, timelock)
References
- references/patterns.md -- ERC20, proxy upgrades, gas optimization examples
External References
Weekly Installs
9
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Feb 20, 2026
Security Audits
Installed on
opencode9
gemini-cli9
github-copilot9
codex9
amp9
kimi-cli9