Initial commit
This commit is contained in:
581
skills/skill/references/patterns.md
Normal file
581
skills/skill/references/patterns.md
Normal file
@@ -0,0 +1,581 @@
|
||||
# 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);
|
||||
```
|
||||
Reference in New Issue
Block a user