skills/ar4mirez/samuel/solidity-guide

solidity-guide

SKILL.md

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.

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 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

// 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 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

External References

Weekly Installs
9
Repository
ar4mirez/samuel
GitHub Stars
3
First Seen
Feb 20, 2026
Installed on
opencode9
gemini-cli9
github-copilot9
codex9
amp9
kimi-cli9