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

13 KiB

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:

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:

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:

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:

// 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:

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:

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)

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:

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):

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):

// 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):

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

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

Implementation controls upgrades:

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:

// 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

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:

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

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)

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)

contract Permanent {
    address public immutable owner;
    uint256 public immutable maxSupply;

    constructor(address _owner, uint256 _maxSupply) {
        owner = _owner;
        maxSupply = _maxSupply;
    }
}

Initializer (Upgradeable)

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

// WRONG: Vulnerable to phishing
if (tx.origin == owner) { /* ... */ }

// CORRECT
if (msg.sender == owner) { /* ... */ }

Storage Layout with Upgrades

// 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

// 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

// 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);