Files
gh-tenequm-claude-plugins-f…/skills/skill/references/solidity-modern.md
2025-11-30 09:01:14 +08:00

642 lines
15 KiB
Markdown

# Modern Solidity (0.8.30)
Complete guide to Solidity 0.8.20-0.8.30 features, gas optimization, and security patterns.
## Version Feature Summary
| Version | Key Features |
|---------|-------------|
| 0.8.30 | Prague EVM default, NatSpec for enums |
| 0.8.29 | Custom storage layout (`layout at`) |
| 0.8.28 | Transient storage for value types |
| 0.8.27 | Transient storage parser support |
| 0.8.26 | `require(bool, Error)` custom errors |
| 0.8.25 | Cancun EVM default, MCOPY opcode |
| 0.8.24 | `blobbasefee`, `blobhash()`, tload/tstore in Yul |
| 0.8.22 | File-level events |
| 0.8.21 | Relaxed immutable initialization |
| 0.8.20 | Shanghai EVM default, PUSH0 |
| 0.8.19 | Custom operators for user-defined types |
| 0.8.18 | Named mapping parameters |
## Custom Errors (0.8.4+)
Gas-efficient error handling.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
error Unauthorized(address caller, address required);
error InsufficientBalance(uint256 available, uint256 required);
error InvalidAmount();
error Expired(uint256 deadline, uint256 current);
contract Token {
mapping(address => uint256) public balanceOf;
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address to, uint256 amount) external {
if (amount == 0) revert InvalidAmount();
if (amount > balanceOf[msg.sender]) {
revert InsufficientBalance(balanceOf[msg.sender], amount);
}
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
function adminFunction() external {
if (msg.sender != owner) {
revert Unauthorized(msg.sender, owner);
}
// ...
}
}
```
### require with Custom Errors (0.8.26+)
```solidity
function transfer(address to, uint256 amount) external {
require(amount > 0, InvalidAmount());
require(
amount <= balanceOf[msg.sender],
InsufficientBalance(balanceOf[msg.sender], amount)
);
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
```
### Gas Comparison
```solidity
// String error: ~4 bytes selector + string data
require(x > 0, "Amount must be greater than zero");
// Custom error: ~4 bytes selector only
if (x == 0) revert InvalidAmount();
// Custom error with params: ~4 bytes + encoded params
if (x > balance) revert InsufficientBalance(balance, x);
// Savings: 50-200+ gas per error
```
## Transient Storage (0.8.28+)
Cheap temporary storage cleared after transaction.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract ReentrancyGuard {
bool transient locked;
modifier nonReentrant() {
require(!locked, "Reentrancy");
locked = true;
_;
locked = false; // CRITICAL: Reset for composability
}
function withdraw() external nonReentrant {
// Safe from reentrancy
}
}
```
### Flash Loan Example
```solidity
contract FlashLender {
IERC20 public token;
bool transient flashLoanActive;
function flashLoan(uint256 amount, address receiver) external {
require(!flashLoanActive, "Flash loan active");
flashLoanActive = true;
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(receiver, amount);
IFlashBorrower(receiver).onFlashLoan(amount);
require(
token.balanceOf(address(this)) >= balanceBefore,
"Flash loan not repaid"
);
flashLoanActive = false;
}
}
```
### Gas Comparison
| Operation | Persistent Storage | Transient Storage |
|-----------|-------------------|-------------------|
| First write | 20,000+ gas | 100 gas |
| Subsequent write | 2,900 gas | 100 gas |
| Read | 100 gas | 100 gas |
**Important**: Always reset transient storage at function exit for composability.
## Immutable Variables (Relaxed in 0.8.21+)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
contract Config {
address public immutable owner;
uint256 public immutable deployTime;
bytes32 public immutable configHash;
uint256 public immutable maxSupply;
constructor(uint256 _maxSupply, bytes32 _configHash) {
owner = msg.sender;
deployTime = block.timestamp;
// Relaxed: can read/write immutables anywhere in constructor
if (_maxSupply == 0) {
maxSupply = 1_000_000e18;
} else {
maxSupply = _maxSupply;
}
// Can use other immutables
configHash = _configHash != bytes32(0)
? _configHash
: keccak256(abi.encode(owner, maxSupply));
}
}
```
### Gas Benefits
- Immutable: **3 gas** (value inlined)
- Storage: **100 gas** (SLOAD)
- Constant: **0 gas** (compile-time constant)
## User-Defined Types with Operators (0.8.19+)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
type Amount is uint256;
type Price is uint256;
using {add as +, sub as -, mul, eq as ==, lt as <} for Amount global;
function add(Amount a, Amount b) pure returns (Amount) {
return Amount.wrap(Amount.unwrap(a) + Amount.unwrap(b));
}
function sub(Amount a, Amount b) pure returns (Amount) {
return Amount.wrap(Amount.unwrap(a) - Amount.unwrap(b));
}
function mul(Amount a, Price p) pure returns (uint256) {
return Amount.unwrap(a) * Price.unwrap(p);
}
function eq(Amount a, Amount b) pure returns (bool) {
return Amount.unwrap(a) == Amount.unwrap(b);
}
function lt(Amount a, Amount b) pure returns (bool) {
return Amount.unwrap(a) < Amount.unwrap(b);
}
contract TypeSafe {
function calculate(Amount a, Amount b, Price p) external pure returns (uint256) {
Amount total = a + b;
if (total < Amount.wrap(100)) {
return 0;
}
return total.mul(p);
}
}
```
## Named Mapping Parameters (0.8.18+)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Token {
// Highly readable
mapping(address account => uint256 balance) public balanceOf;
mapping(address owner => mapping(address spender => uint256 amount)) public allowance;
// Complex mappings
mapping(uint256 tokenId => mapping(address operator => bool approved)) public operatorApproval;
}
```
## File-Level Events (0.8.22+)
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
// File-level event
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
contract Token is IERC20 {
mapping(address => uint256) public balanceOf;
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
```
## Custom Storage Layout (0.8.29+)
For EIP-7702 delegate implementations:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
// Delegate 1: Storage starts at 0x1000
contract DelegateA layout at 0x1000 {
uint256 public valueA; // Slot 0x1000
function setA(uint256 v) external {
valueA = v;
}
}
// Delegate 2: Storage starts at 0x2000
contract DelegateB layout at 0x2000 {
uint256 public valueB; // Slot 0x2000
function setB(uint256 v) external {
valueB = v;
}
}
// Both can be delegated to from same account without storage collision
```
## Checked vs Unchecked Arithmetic
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract Arithmetic {
// Default: checked (reverts on overflow)
function checkedAdd(uint256 a, uint256 b) public pure returns (uint256) {
return a + b; // Reverts if overflow
}
// Explicit: unchecked (wraps on overflow)
function uncheckedIncrement(uint256 i) public pure returns (uint256) {
unchecked {
return i + 1; // Does not revert
}
}
// Common pattern: loop counter
function sum(uint256[] calldata arr) public pure returns (uint256 total) {
for (uint256 i = 0; i < arr.length;) {
total += arr[i];
unchecked { ++i; } // Safe: i < arr.length guarantees no overflow
}
}
}
```
### When to Use Unchecked
Safe scenarios:
- Loop counters bounded by array length
- Incrementing from known safe values
- Subtracting after explicit bounds check
- Timestamp/block number arithmetic
Never use unchecked:
- User-provided inputs without validation
- Token amounts in DeFi
- Any value that could overflow
## Gas Optimization Techniques
### 1. Storage Packing
```solidity
// Bad: 3 slots
contract Unpacked {
uint256 a; // Slot 0
uint128 b; // Slot 1
uint128 c; // Slot 2
}
// Good: 2 slots
contract Packed {
uint256 a; // Slot 0
uint128 b; // Slot 1 (first half)
uint128 c; // Slot 1 (second half)
}
```
### 2. Caching Storage in Memory
```solidity
// Bad: Multiple SLOADs
function bad(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
emit Transfer(msg.sender, amount, balances[msg.sender]);
}
// Good: Cache storage
function good(uint256 amount) external {
uint256 balance = balances[msg.sender];
require(balance >= amount);
uint256 newBalance = balance - amount;
balances[msg.sender] = newBalance;
emit Transfer(msg.sender, amount, newBalance);
}
```
### 3. Use calldata for Read-Only Arrays
```solidity
// Bad: Copies to memory
function bad(uint256[] memory data) external { }
// Good: Direct calldata access
function good(uint256[] calldata data) external { }
```
### 4. Short-Circuit Conditionals
```solidity
// Cheaper checks first
function check(uint256 amount, address user) external view {
// Cheap: comparison
// Expensive: storage read
if (amount > 0 && balances[user] >= amount) {
// ...
}
}
```
### 5. Use Constants and Immutables
```solidity
// Constant: 0 gas (inlined at compile time)
uint256 constant MAX_SUPPLY = 1_000_000e18;
// Immutable: 3 gas (inlined in bytecode)
address immutable owner;
// Storage: 100 gas (SLOAD)
address _owner;
```
### 6. Increment Patterns
```solidity
// Most efficient
unchecked { ++i; }
// Less efficient
unchecked { i++; }
// Least efficient (checked)
i++;
```
### Gas Costs Reference
| Operation | Gas Cost |
|-----------|----------|
| SLOAD (warm) | 100 |
| SLOAD (cold) | 2,100 |
| SSTORE (zero to non-zero) | 20,000 |
| SSTORE (non-zero to non-zero) | 2,900 |
| SSTORE (non-zero to zero) | Refund 4,800 |
| TLOAD | 100 |
| TSTORE | 100 |
| Memory read/write | 3 |
| Calldata read | 3 |
## Security Best Practices
### 1. Checks-Effects-Interactions
```solidity
function withdraw(uint256 amount) external {
// CHECKS
require(amount <= balances[msg.sender], "Insufficient");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
```
### 2. Reentrancy Protection
```solidity
// Using transient storage (0.8.28+)
bool transient locked;
modifier nonReentrant() {
require(!locked);
locked = true;
_;
locked = false;
}
// Or OpenZeppelin ReentrancyGuard
```
### 3. Access Control
```solidity
error Unauthorized();
modifier onlyOwner() {
if (msg.sender != owner) revert Unauthorized();
_;
}
// Use OpenZeppelin AccessControl for complex permissions
```
### 4. Safe External Calls
```solidity
// Check return value
(bool success, bytes memory data) = target.call(payload);
require(success, "Call failed");
// Or use SafeCall pattern
function safeTransferETH(address to, uint256 amount) internal {
(bool success,) = to.call{value: amount}("");
if (!success) revert TransferFailed();
}
```
### 5. Input Validation
```solidity
function deposit(uint256 amount) external {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_DEPOSIT) revert ExceedsMax(amount, MAX_DEPOSIT);
// Proceed
}
```
### 6. Integer Overflow Protection
```solidity
// Built-in since 0.8.0, but be careful with unchecked
function safeAdd(uint256 a, uint256 b) internal pure returns (uint256) {
// This reverts on overflow (default behavior)
return a + b;
}
```
### 7. Timestamp Dependence
```solidity
// Don't use for randomness
// Small manipulations possible by miners
// OK for time-based logic with tolerance
require(block.timestamp >= deadline, "Not yet");
// Bad: exact timing
require(block.timestamp == exactTime, "Wrong time");
```
## EVM Version Features
### Prague (0.8.30 default)
- All Cancun features
- EIP-7702 preparation
### Cancun (0.8.25 default)
- Transient storage (TLOAD, TSTORE)
- MCOPY for memory copying
- BLOBHASH, BLOBBASEFEE
### Shanghai (0.8.20 default)
- PUSH0 opcode
### Block Properties by Version
```solidity
// Always available
block.number
block.timestamp
block.chainid
block.coinbase
block.gaslimit
// Paris+
block.prevrandao // Replaces block.difficulty
// Cancun+
block.blobbasefee
blobhash(index)
```
## Common Patterns
### Factory Pattern
```solidity
contract Factory {
event Created(address indexed instance);
function create(bytes32 salt, bytes calldata initData) external returns (address) {
address instance = address(new Contract{salt: salt}(initData));
emit Created(instance);
return instance;
}
function predictAddress(bytes32 salt, bytes calldata initData) external view returns (address) {
bytes32 hash = keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(Contract).creationCode,
abi.encode(initData)
))
));
return address(uint160(uint256(hash)));
}
}
```
### Minimal Proxy (EIP-1167)
```solidity
function clone(address implementation) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
mstore(add(ptr, 0x14), shl(0x60, implementation))
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
instance := create(0, ptr, 0x37)
}
require(instance != address(0), "Clone failed");
}
```
### Merkle Proof Verification
```solidity
function verify(
bytes32[] calldata proof,
bytes32 root,
bytes32 leaf
) public pure returns (bool) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length;) {
bytes32 proofElement = proof[i];
if (computedHash <= proofElement) {
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
unchecked { ++i; }
}
return computedHash == root;
}
```