solidity-style

Installation
SKILL.md

Solidity Style Guide

Follow these conventions when writing Solidity code. Consistency is paramount.

Import Conventions

Always use named imports:

// ✅ Good
import {Contract} from "./contract.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

// ❌ Bad
import "./contract.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Naming Conventions

Element Convention Example
Private/internal state variables Prefix with _ uint256 private _totalSupply;
Private/internal functions Prefix with _ function _mint() internal
Function parameters Start with _ function transfer(address _to, uint256 _amount)
Return values Only use named returns when there's more than one return value. End with _ returns (uint256 balance_, address user)
Interfaces Prefix with I interface IERC20
Events Past tense event TokensTransferred(...)
Mappings Use named parameters mapping(address owner => uint256 balance)

Code Layout

  • Indentation: 4 spaces (no tabs)
  • Max line length: 120 characters
  • Blank lines: 2 between top-level declarations, 1 between functions within a contract

Function Ordering

Order functions by visibility, with view/pure functions last in each group:

contract Example {
    // 1. Constructor
    constructor() { }
    
    // 2. Receive function (if exists)
    receive() external payable { }
    
    // 3. Fallback function (if exists)
    fallback() external { }
    
    // 4. External functions
    function externalFunc() external { }
    function externalView() external view returns (uint256) { }
    
    // 5. Public functions
    function publicFunc() public { }
    function publicView() public view returns (uint256) { }
    
    // 6. Internal functions
    function _internalFunc() internal { }
    function _internalView() internal view returns (uint256) { }
    
    // 7. Private functions
    function _privateFunc() private { }
}

Function Modifier Order

function functionName()
    visibility      // external, public, internal, private
    mutability      // view, pure, payable
    virtual         // if overridable
    override        // if overriding
    customModifiers // onlyOwner, whenNotPaused, etc.
    returns (Type)
{

NatSpec Documentation

/// @notice Transfers tokens to a recipient (for external/public)
/// @dev Implementation details here (for internal/private, use @dev only)
/// @param _to The recipient address
/// @param _amount The amount to transfer
/// @return success_ Whether the transfer succeeded
function transfer(address _to, uint256 _amount) external returns (bool success_) {
  • External/public functions: Include @notice
  • Internal/private functions: Use @dev only, no @notice
  • All files: Include /// @custom:security-contact security@yourproject.xyz

Code Organization

Use section comments for large contracts:

contract LargeContract {
    // ---------------------------------------------------------------
    // State Variables
    // ---------------------------------------------------------------
    
    // ---------------------------------------------------------------
    // Events
    // ---------------------------------------------------------------
    
    // ---------------------------------------------------------------
    // External & Public Functions
    // ---------------------------------------------------------------
    
    // ---------------------------------------------------------------
    // Internal Functions
    // ---------------------------------------------------------------
    
    // ---------------------------------------------------------------
    // Private Functions
    // ---------------------------------------------------------------
    
    // ---------------------------------------------------------------
    // Custom Errors
    // ---------------------------------------------------------------
}

Error Handling

  • Prefer custom errors over require strings (saves gas, more expressive)
  • Place errors at the end of the implementation file, not in interfaces
  • Include context parameters for debugging (see Error Handling Patterns below)

Control Structures

  • Opening brace on same line as declaration
  • Single space between keyword and parenthesis
  • Single space before opening brace
// ✅ Good
if (condition) {
    doSomething();
} else {
    doSomethingElse();
}

for (uint256 i = 0; i < length; i++) {
    process(i);
}

// ❌ Bad
if(condition){
    doSomething();
}
else {
    doSomethingElse();
}

License

All Solidity files should have SPDX license identifier:

// SPDX-License-Identifier: MIT

Solidity Design Patterns

Proven patterns for building maintainable, upgradeable smart contracts.

Upgrade Patterns

UUPS (Universal Upgradeable Proxy Standard) - Preferred

import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyContract is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public value;
    
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
    
    function initialize(address _owner) external initializer {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
    }
    
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
    
    // Storage gap for future upgrades
    uint256[50] private __gap;
}

Why UUPS over Transparent Proxy:

  • Upgrade logic in implementation (smaller proxy)
  • More gas efficient for users
  • Upgrade authorization is explicit

Storage Gaps

Always include storage gaps in upgradeable contracts:

contract V1 {
    uint256 public value;
    address public admin;
    
    // Reserve 50 slots for future variables
    uint256[50] private __gap;
}

// In V2, you can safely add:
contract V2 {
    uint256 public value;
    address public admin;
    uint256 public newValue; // Uses slot from __gap
    
    uint256[49] private __gap; // Reduce gap by 1
}

Storage Layout Rules

  1. Never reorder existing variables
  2. Never change variable types
  3. Only add new variables at the end
  4. Use gaps to reserve space

Error Handling Patterns

Custom Errors with Context

// Define errors with useful context
error InsufficientBalance(address account, uint256 available, uint256 required);
error Unauthorized(address caller, bytes32 requiredRole);
error InvalidParameter(string paramName, uint256 value, uint256 minValue, uint256 maxValue);
error DeadlineExpired(uint256 deadline, uint256 currentTime);

// Use with require
```solidity
function withdraw(uint256 _amount) external {
    uint256 balance = balances[msg.sender];
    require(balance >= _amount, InsufficientBalance(msg.sender, balance, _amount));
    // ...
}

Event Patterns

Event Naming (Past Tense)

// ✅ Good - past tense, describes what happened
event TokensTransferred(address indexed from, address indexed to, uint256 amount);
event ProposalCreated(uint256 indexed proposalId, address proposer);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

// ❌ Bad - present tense
event TransferTokens(...);
event CreateProposal(...);

Event Indexing Strategy

  • Index up to 3 parameters for filtering
  • Index addresses and IDs that will be queried
  • Don't index large data (wastes gas)
event Transfer(
    address indexed from,     // Indexed - filter by sender
    address indexed to,       // Indexed - filter by recipient  
    uint256 amount           // Not indexed - just data
);

event Swap(
    address indexed sender,
    uint256 amount0In,       // Not indexed - numerical data
    uint256 amount1Out,
    address indexed to
);

Emit Events for Important Changes

Events are important, but prefer emitting one event only per function call, especially on L1 where gas costs accumulate.

function setFee(uint256 _newFee) external onlyOwner {
    uint256 oldFee = fee;
    fee = _newFee;
    emit FeeUpdated(oldFee, _newFee); // Always emit!
}

Access Control Patterns

Role-Based Access

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract MyContract is AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    
    constructor(address _admin) {
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }
    
    function mint(address _to, uint256 _amount) external onlyRole(MINTER_ROLE) {
        _mint(_to, _amount);
    }
}

Two-Step Ownership Transfer

Prevents accidental ownership loss:

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

contract SafeOwnable is Ownable2Step {
    // transferOwnership() sets pendingOwner
    // New owner must call acceptOwnership()
}

Modular Contract Design

Interface-First Design

// Define interface first
interface IVault {
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
    function balanceOf(address account) external view returns (uint256);
    
    event Deposited(address indexed account, uint256 amount);
    event Withdrawn(address indexed account, uint256 amount);
}

// Implement interface
contract Vault is IVault {
    // Implementation
}

Pull Over Push Pattern

Let users withdraw rather than pushing funds when possible:

// ✅ Good - Pull pattern
mapping(address => uint256) public pendingWithdrawals;

function claimReward() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    pendingWithdrawals[msg.sender] = 0;
    
    (bool success,) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

// ❌ Bad - Push pattern (can fail, block contract)
function distributeRewards(address[] calldata _recipients) external {
    for (uint256 i; i < _recipients.length; i++) {
        // If one transfer fails, entire function reverts
        payable(_recipients[i]).transfer(rewards[_recipients[i]]);
    }
}

Factory Pattern

contract VaultFactory {
    event VaultCreated(address indexed vault, address indexed owner);
    
    mapping(address => address[]) public userVaults;
    
    function createVault() external returns (address vault_) {
        vault_ = address(new Vault(msg.sender));
        userVaults[msg.sender].push(vault_);
        emit VaultCreated(vault_, msg.sender);
    }
}

Pattern Checklist

  • Using UUPS for upgradeable contracts
  • Storage gaps included (50 slots recommended)
  • Custom errors with context
  • Events for all state changes
  • Past tense event names
  • Role-based access control for complex permissions
  • Two-step ownership transfer
  • Pull over push for fund distribution
  • Interface-first design
Installs
1
First Seen
Mar 27, 2026