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
@devonly, 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
- Never reorder existing variables
- Never change variable types
- Only add new variables at the end
- 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