# Solidity Guide

> Solidity/Ethereum guardrails, patterns, and best practices for AI-assisted development. Use when working with Solidity files (.sol), or when the user mentions Solidity/Ethereum/smart contracts. Provides gas optimization patterns, reentrancy prevention, upgradeable contract guidelines, and testing standards specific to this project's coding standards.

- Skill: `majiayu000/solidity-guide` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/solidity-guide`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/solidity-guide/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/solidity-guide

---


# Solidity Guide

> Applies to: Solidity 0.8.20+, Ethereum, EVM-compatible chains, DeFi, NFTs

## Core Principles

1. **Security First**: Every function is a potential attack surface. Assume adversarial callers.
2. **Gas Efficiency**: On-chain computation is expensive. Optimize storage access and minimize state changes.
3. **Explicit Over Implicit**: Use explicit visibility, explicit types, and named return values.
4. **Immutability by Default**: Prefer `immutable` and `constant` for values that do not change after deployment.
5. **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.origin` for authorization. Use `msg.sender`.
- NEVER use `selfdestruct` in new contracts (deprecated in Dencun).
- ALWAYS use `ReentrancyGuard` from OpenZeppelin for functions that make external calls.
- ALWAYS validate external inputs: check zero addresses, bounds, and array lengths.
- Use `SafeERC20` for 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`, `bool` into single 256-bit slots.
- Use `calldata` instead of `memory` for read-only external function parameters.
- Use custom errors instead of `require` strings (saves ~50 bytes per error site).
- Cache storage reads in local variables when accessed more than once.
- Use `unchecked` blocks for arithmetic that provably cannot overflow.
- Prefer `mapping` over `array` for large datasets (O(1) vs O(n) lookups).

### Access Control

- Use OpenZeppelin `AccessControl` or `Ownable2Step` (not plain `Ownable`).
- 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-upgradeable` with `initializer` instead 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.

```solidity
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.

```solidity
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.

```solidity
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

```solidity
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 `nonReentrant` to 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 `unchecked` only when provably safe.
- **Front-Running**: Use commit-reveal for auctions/voting. Add deadline params to swaps. Use `block.timestamp`, not `block.number`.
- **Access Control**: Never rely on `tx.origin`. Use `Pausable` for emergency stops.

## Testing

### Foundry (forge) -- Recommended

```solidity
// 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

```bash
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 test` passes, 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](references/patterns.md) -- ERC20, proxy upgrades, gas optimization examples

## External References

- [Solidity Docs](https://docs.soliditylang.org/) | [OpenZeppelin](https://docs.openzeppelin.com/contracts/) | [Foundry Book](https://book.getfoundry.sh/)
- [Solidity by Example](https://solidity-by-example.org/) | [EIP Standards](https://eips.ethereum.org/)
- [Smart Contract Security (Consensys)](https://consensys.github.io/smart-contract-best-practices/) | [Slither](https://github.com/crytic/slither)

