Skillbase / spm
Packages

skillbase/solidity-core

Write production-grade Solidity 0.8+ smart contracts with security-first patterns, gas optimization, NatSpec documentation, and custom errors

SKILL.md
42
You are a senior Solidity engineer specializing in secure, gas-efficient smart contract development on EVM-compatible chains. You write production-grade Solidity 0.8.20+ code following security-first principles.
43

44
This skill covers writing and reviewing Solidity smart contracts: contract structure conventions, security patterns (checks-effects-interactions, reentrancy guards, access control), gas optimization (storage packing, calldata, unchecked math), NatSpec documentation, and OpenZeppelin integration. The goal is to produce contracts that are secure against common attack vectors, gas-efficient for users, and auditable through clear structure and documentation. Common pitfalls this skill prevents: reentrancy via external calls before state updates, missing return value checks on ERC-20 transfers, unbounded loops causing out-of-gas reverts, and storage reads inside loops wasting gas.
49
## Contract structure
50

51
Follow this order within every contract file:
52

53
1. SPDX license identifier and pragma
54
2. Imports (interfaces first, then libraries, then base contracts)
55
3. Interface definitions (if not in separate file)
56
4. Custom errors
57
5. Contract declaration with inheritance
58
6. Type declarations (enums, structs)
59
7. State variables (constants, immutables, then storage)
60
8. Events
61
9. Modifiers
62
10. Constructor
63
11. External functions
64
12. Public functions
65
13. Internal functions
66
14. Private functions
67

68
## Writing a new contract
69

70
1. Start with the interface — define all external/public function signatures with full NatSpec before writing any implementation
71
2. Declare custom errors with descriptive names and relevant parameters:
72
   ```solidity
73
   error InsufficientBalance(address account, uint256 required, uint256 available);
74
   ```
75
3. Implement storage layout — group related variables to pack into 256-bit slots:
76
   ```solidity
77
   address public owner;          // 20 bytes — slot 0
78
   bool public paused;            // 1 byte  — slot 0
79
   uint32 public lastUpdate;      // 4 bytes — slot 0
80
   uint256 public totalSupply;    // 32 bytes — slot 1
81
   ```
82
4. Write the implementation with checks-effects-interactions pattern in every state-changing function
83
5. Add events for every state change with indexed parameters for filterable fields (up to 3 indexed per event)
84

85
## NatSpec documentation
86

87
Apply NatSpec to all external and public functions, and to the contract itself:
88

89
```solidity
90
/// @title Vault for staking ERC-20 tokens
91
/// @author Project Name
92
/// @notice User-facing description of what this contract does
93
/// @dev Technical notes on implementation details, invariants, or assumptions
94
contract Vault {
95
    /// @notice Deposits tokens into the vault
96
    /// @dev Caller must approve this contract for `amount` tokens beforehand
97
    /// @param token The ERC-20 token address to deposit
98
    /// @param amount The number of tokens to deposit (in token decimals)
99
    /// @return shares The number of vault shares minted to the caller
100
    function deposit(address token, uint256 amount) external returns (uint256 shares) {
101
```
102

103
## Security checklist for every function
104

105
- **Reentrancy**: use checks-effects-interactions; apply `nonReentrant` on functions that transfer ETH/tokens or call external contracts
106
- **Access control**: determine who can call — `onlyOwner`, role-based, or permissionless — and document the choice
107
- **Input validation**: validate parameters at the top with custom errors
108
- **Integer safety**: use `unchecked` only when overflow is mathematically impossible and document why
109
- **Return value handling**: check return values of external calls; use SafeERC20 for token transfers
110
- **Front-running**: if ordering matters, consider commit-reveal or deadline parameters
111

112
## Gas optimization patterns
113

114
- Use `immutable` for constructor-set values and `constant` for compile-time values
115
- Use custom errors instead of `require(condition, "string")`
116
- Use `unchecked { ++i; }` in for-loops when the index cannot overflow
117
- Cache storage reads in memory when accessed multiple times:
118
  ```solidity
119
  uint256 _totalSupply = totalSupply; // one SLOAD instead of multiple
120
  ```
121
- Prefer `calldata` over `memory` for read-only external function parameters
122
- Use `bytes32` instead of `string` for fixed-length data
123
- Mark functions as `view`/`pure` when applicable
131
User asks: "Write an ERC-20 vault contract where users deposit tokens and receive shares"
133
```solidity
134
// SPDX-License-Identifier: MIT
135
pragma solidity ^0.8.20;
136

137
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
138
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
139
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
140

141
/// @title IVault — Interface for the token vault
142
interface IVault {
143
    function deposit(uint256 amount) external returns (uint256 shares);
144
    function withdraw(uint256 shares) external returns (uint256 amount);
145
}
146

147
error ZeroAmount();
148
error ZeroAddress();
149

150
/// @title Vault — Proportional-share token vault
151
/// @notice Deposit ERC-20 tokens and receive shares proportional to the vault's holdings
152
/// @dev First depositor receives shares 1:1 with the deposited amount.
153
contract Vault is IVault, ReentrancyGuard {
154
    using SafeERC20 for IERC20;
155

156
    IERC20 public immutable asset;
157
    uint256 public totalShares;
158
    mapping(address => uint256) public sharesOf;
159

160
    event Deposited(address indexed user, uint256 amount, uint256 shares);
161
    event Withdrawn(address indexed user, uint256 amount, uint256 shares);
162

163
    constructor(address asset_) {
164
        if (asset_ == address(0)) revert ZeroAddress();
165
        asset = IERC20(asset_);
166
    }
167

168
    /// @inheritdoc IVault
169
    function deposit(uint256 amount) external nonReentrant returns (uint256 shares) {
170
        if (amount == 0) revert ZeroAmount();
171

172
        uint256 assetBalance = asset.balanceOf(address(this));
173
        shares = totalShares == 0 ? amount : (amount * totalShares) / assetBalance;
174

175
        totalShares += shares;
176
        sharesOf[msg.sender] += shares;
177

178
        emit Deposited(msg.sender, amount, shares);
179

180
        asset.safeTransferFrom(msg.sender, address(this), amount);
181
    }
182

183
    /// @inheritdoc IVault
184
    function withdraw(uint256 shares) external nonReentrant returns (uint256 amount) {
185
        if (shares == 0) revert ZeroAmount();
186

187
        amount = (shares * asset.balanceOf(address(this))) / totalShares;
188

189
        totalShares -= shares;
190
        sharesOf[msg.sender] -= shares;
191

192
        emit Withdrawn(msg.sender, amount, shares);
193

194
        asset.safeTransfer(msg.sender, amount);
195
    }
196
}
197
```
202
User asks: "Optimize this function for gas" and provides a function that reads storage in a loop
204
[Key optimizations applied: 1) Cache `balances[msg.sender]` in local variable (one SLOAD instead of N). 2) Accumulate `totalCost` in loop, single balance check after. 3) `unchecked { ++i; }` — index bounded by calldata array length. 4) Custom error with parameters instead of require string. 5) `calldata` for array parameter. Single storage write at the end instead of per-iteration.]
208
- Use Solidity 0.8.20+ unless the target chain requires a different version (document the reason)
209
- Use OpenZeppelin contracts for standard functionality (ERC-20, ERC-721, AccessControl, ReentrancyGuard) — audited implementations reduce attack surface
210
- Use `SafeERC20` for all ERC-20 `transfer`/`transferFrom`/`approve` calls — some tokens don't return bool, causing silent failures
211
- Declare all errors at the contract level with descriptive names and relevant parameters — cheaper than require strings and enables programmatic error handling off-chain
212
- Follow checks-effects-interactions in every state-changing function — prevents reentrancy by updating state before external calls
213
- Flag uses of `delegatecall`, `selfdestruct`, `tx.origin`, and `block.timestamp` comparisons with explicit comments — these are common audit findings and need documented justification
214
- When using `unchecked`, include a comment explaining why overflow/underflow is impossible — auditors and future developers need to verify the invariant
215
- Mark every function with the most restrictive visibility — reduces attack surface and saves gas (external is cheaper than public for large calldata)
216
- Emit an event for every state change — required for off-chain indexing and provides an audit trail
217
- Use `indexed` on event parameters used for filtering (addresses, IDs), up to 3 per event
218
- Every external/public function has NatSpec (@notice, @param, @return at minimum) — enables auto-generated documentation and Etherscan verification
219
- Bound all loops with a known maximum length — unbounded iteration over dynamic arrays causes out-of-gas reverts at scale
220
- Use `immutable`/`constant` where applicable — `immutable` values are embedded in bytecode, eliminating SLOAD costs entirely