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

582 lines
13 KiB
Markdown

# Solidity Patterns and Idioms
Common patterns for modern Solidity 0.8.30 smart contract development.
## Access Control Patterns
### Ownable Pattern
Single owner - simplest but centralized:
```solidity
contract Ownable {
address private _owner;
error Unauthorized();
modifier onlyOwner() {
if (msg.sender != _owner) revert Unauthorized();
_;
}
constructor() {
_owner = msg.sender;
}
function owner() public view returns (address) {
return _owner;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid owner");
_owner = newOwner;
}
}
```
**Use:** Simple contracts, single admin
**Pitfall:** Single point of failure
### Role-Based Access Control
Fine-grained permissions:
```solidity
contract AccessControl {
mapping(bytes32 => mapping(address => bool)) private _roles;
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
error AccessDenied(address account, bytes32 role);
modifier onlyRole(bytes32 role) {
if (!_roles[role][msg.sender])
revert AccessDenied(msg.sender, role);
_;
}
function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
_roles[role][account] = true;
}
function hasRole(bytes32 role, address account) public view returns (bool) {
return _roles[role][account];
}
}
```
**Use:** DeFi protocols, complex permissions
**Recommendation:** Use OpenZeppelin's AccessControl
### Multi-Signature Pattern
Require multiple approvals for critical operations:
```solidity
contract MultiSig {
address[] public owners;
uint256 public requiredSignatures;
mapping(bytes32 => mapping(address => bool)) public confirmations;
mapping(bytes32 => bool) public executed;
function submitTransaction(
address target,
uint256 value,
bytes calldata data
) external returns (bytes32) {
bytes32 txHash = keccak256(abi.encode(target, value, data, block.timestamp));
confirmations[txHash][msg.sender] = true;
return txHash;
}
function executeTransaction(
address target,
uint256 value,
bytes calldata data,
bytes32 txHash
) external {
require(!executed[txHash], "Already executed");
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txHash][owners[i]]) count++;
}
require(count >= requiredSignatures, "Not enough signatures");
executed[txHash] = true;
(bool success,) = target.call{value: value}(data);
require(success, "Execution failed");
}
}
```
**Use:** Treasury, critical upgrades, governance
## Reentrancy Protection
### Checks-Effects-Interactions (CEI)
Most important pattern - prevent state inconsistencies:
```solidity
// BAD: State update AFTER external call
function withdrawBad() external {
uint256 amount = balances[msg.sender];
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // Vulnerable!
}
// GOOD: CEI pattern
function withdrawGood() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // Effects first
(bool success,) = msg.sender.call{value: amount}("");
require(success); // Interactions last
}
```
### Mutex Lock (ReentrancyGuard)
Traditional approach using storage:
```solidity
contract ReentrancyGuard {
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status = NOT_ENTERED;
modifier nonReentrant() {
require(_status != ENTERED, "ReentrancyGuard: reentrant call");
_status = ENTERED;
_;
_status = NOT_ENTERED;
}
}
```
**Cost:** ~5,000 gas (2 SSTOREs)
### Transient Storage Lock (0.8.28+)
Gas-efficient using EIP-1153:
```solidity
contract TransientReentrancyGuard {
modifier nonReentrant() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly {
tstore(0, 0)
}
}
}
```
**Cost:** ~200 gas (TSTORE/TLOAD) - 25x cheaper
## Factory Patterns
### Basic Factory (CREATE)
```solidity
contract TokenFactory {
address[] public deployedTokens;
event TokenCreated(address indexed token, address indexed creator);
function createToken(string memory name, string memory symbol)
external
returns (address)
{
Token token = new Token(name, symbol, msg.sender);
deployedTokens.push(address(token));
emit TokenCreated(address(token), msg.sender);
return address(token);
}
}
```
**Use:** Simple deployments where address doesn't matter
### Deterministic Factory (CREATE2)
Deploy to predictable addresses across chains:
```solidity
contract DeterministicFactory {
event Deployed(address indexed addr, bytes32 salt);
function deploy(bytes32 salt, bytes memory bytecode)
external
returns (address addr)
{
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
emit Deployed(addr, salt);
}
function computeAddress(bytes32 salt, bytes32 bytecodeHash)
external
view
returns (address)
{
return address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
bytecodeHash
)))));
}
}
```
**Use:** Counterfactual deployments, cross-chain consistency
### Minimal Proxy (ERC-1167 Clones)
Deploy cheap proxies (~45 bytes):
```solidity
contract CloneFactory {
function clone(address implementation) internal returns (address instance) {
assembly {
mstore(0x00, or(
shr(0xe8, shl(0x60, implementation)),
0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000
))
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
}
}
```
**Cost:** ~10K gas vs ~170K for full contract
**Use:** Token factories, vault templates
## Payment Patterns
### Pull Over Push
**Push (Anti-pattern):**
```solidity
// BAD: Can fail if recipient reverts
function sendRewards() external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amounts[i]);
}
}
```
**Pull (Recommended):**
```solidity
contract PullPayments {
mapping(address => uint256) public pendingWithdrawals;
function claimReward() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to claim");
pendingWithdrawals[msg.sender] = 0;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
```
**Why pull is safer:**
- User controls when to withdraw
- Failing transfers don't block protocol
- Prevents DoS via malicious fallbacks
## Emergency Controls
### Pausable Pattern
```solidity
contract Pausable {
bool public paused;
address public owner;
error ContractPaused();
modifier whenNotPaused() {
if (paused) revert ContractPaused();
_;
}
function pause() external onlyOwner {
paused = true;
}
function unpause() external onlyOwner {
paused = false;
}
function transfer(address to, uint256 amount) external whenNotPaused {
// Transfer logic
}
}
```
**Use:** Security response, gradual rollouts
## Proxy Patterns
### UUPS (Recommended)
Implementation controls upgrades:
```solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyContractV1 is UUPSUpgradeable {
uint256 public value;
function initialize(uint256 _value) public initializer {
value = _value;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
```
### Transparent Proxy
Admin calls go to proxy, user calls go to implementation:
```solidity
// Use OpenZeppelin's TransparentUpgradeableProxy
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
```
**Trade-offs:**
- UUPS: Smaller proxy, upgrade logic in implementation
- Transparent: Larger proxy, clearer admin separation
## State Machine Pattern
```solidity
contract Auction {
enum Phase { Bidding, Reveal, Finished }
Phase public currentPhase;
uint256 public phaseDeadline;
error WrongPhase(Phase expected, Phase actual);
modifier atPhase(Phase expected) {
_checkPhaseTransition();
if (currentPhase != expected)
revert WrongPhase(expected, currentPhase);
_;
}
function _checkPhaseTransition() internal {
if (block.timestamp >= phaseDeadline) {
currentPhase = Phase(uint256(currentPhase) + 1);
phaseDeadline = block.timestamp + 1 days;
}
}
function placeBid(bytes32 hashedBid) external atPhase(Phase.Bidding) {
// Bidding logic
}
function revealBid(uint256 value, bytes32 secret) external atPhase(Phase.Reveal) {
// Reveal logic
}
}
```
## Commit-Reveal Scheme
Prevent front-running:
```solidity
contract CommitReveal {
mapping(address => bytes32) public commits;
mapping(address => uint256) public commitTimes;
uint256 public constant REVEAL_DELAY = 1 hours;
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
commitTimes[msg.sender] = block.timestamp;
}
function reveal(uint256 value, bytes32 salt) external {
require(block.timestamp >= commitTimes[msg.sender] + REVEAL_DELAY, "Too early");
require(commits[msg.sender] == keccak256(abi.encode(value, salt, msg.sender)), "Invalid reveal");
delete commits[msg.sender];
// Process revealed value
}
}
```
**Use:** Auctions, voting, sealed-bid protocols
## Timelock Pattern
```solidity
contract Timelock {
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queuedTransactions;
function queue(address target, bytes calldata data) external returns (bytes32) {
bytes32 txHash = keccak256(abi.encode(target, data, block.timestamp));
queuedTransactions[txHash] = block.timestamp + DELAY;
return txHash;
}
function execute(address target, bytes calldata data, bytes32 txHash) external {
uint256 executeTime = queuedTransactions[txHash];
require(executeTime != 0, "Not queued");
require(block.timestamp >= executeTime, "Too early");
delete queuedTransactions[txHash];
(bool success,) = target.call(data);
require(success, "Execution failed");
}
}
```
**Use:** Governance, critical upgrades, parameter changes
## Error Handling
### Custom Errors (Preferred)
```solidity
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
error ZeroAddress();
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount)
revert InsufficientBalance(balances[msg.sender], amount);
// ...
}
```
**Advantages:**
- ~90% gas savings vs require strings
- Structured error data for debugging
- Type-safe parameters
## Initialization Patterns
### Constructor (Non-upgradeable)
```solidity
contract Permanent {
address public immutable owner;
uint256 public immutable maxSupply;
constructor(address _owner, uint256 _maxSupply) {
owner = _owner;
maxSupply = _maxSupply;
}
}
```
### Initializer (Upgradeable)
```solidity
contract Upgradeable {
bool private _initialized;
address public owner;
modifier initializer() {
require(!_initialized, "Already initialized");
_initialized = true;
_;
}
function initialize(address _owner) external initializer {
owner = _owner;
}
}
```
**Critical:** Constructors don't run on proxies - use initializers
## Common Pitfalls
### tx.origin vs msg.sender
```solidity
// WRONG: Vulnerable to phishing
if (tx.origin == owner) { /* ... */ }
// CORRECT
if (msg.sender == owner) { /* ... */ }
```
### Storage Layout with Upgrades
```solidity
// V1
contract V1 {
uint256 public value; // slot 0
}
// V2 - CORRECT: Append only
contract V2 is V1 {
uint256 public newValue; // slot 1
}
// V2 - WRONG: Inserting breaks layout
contract V2Bad {
uint256 public newValue; // slot 0 - collision!
uint256 public value; // slot 1
}
```
### Unbounded Loops
```solidity
// DANGEROUS: Can run out of gas
function distributeAll() external {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amounts[i]);
}
}
// SAFE: Batch processing
function distributeBatch(uint256 start, uint256 end) external {
require(end <= recipients.length && end - start <= 100);
for (uint256 i = start; i < end; i++) {
payable(recipients[i]).transfer(amounts[i]);
}
}
```
### Unchecked External Calls
```solidity
// WRONG: Return value ignored
token.transfer(user, amount);
// CORRECT: Check return
bool success = token.transfer(user, amount);
require(success, "Transfer failed");
// BEST: Use SafeERC20
IERC20(token).safeTransfer(user, amount);
```