Web3 & Blockchain/Ethereum/Solidity Patterns

Solidity Patterns

Patterns I keep coming back to. All examples use Solidity 0.8.20+ with OpenZeppelin.


ERC-20 Token

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor(uint256 initialSupply)
        ERC20("MyToken", "MTK")
        Ownable(msg.sender)
    {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}

Always use OpenZeppelin. Do not reimplement ERC-20 from scratch.


Reentrancy Guard

The most common DeFi vulnerability. Always update state before making external calls.

// WRONG — reentrancy attack possible
function withdraw(uint256 amount) external {
    (bool ok,) = msg.sender.call{value: amount}(""); // attacker re-enters here
    balances[msg.sender] -= amount; // never reached on attack
}

// CORRECT — checks-effects-interactions pattern
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;  // 1. effects first
    (bool ok,) = msg.sender.call{value: amount}("");  // 2. then interact
    require(ok, "Transfer failed");
}

Or use OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract Vault is ReentrancyGuard {
    function withdraw(uint256 amount) external nonReentrant {
        // safe from reentrancy
    }
}

Access Control

import "@openzeppelin/contracts/access/AccessControl.sol";

contract Protocol is AccessControl {
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(OPERATOR_ROLE, msg.sender);
    }

    function adminAction() external onlyRole(DEFAULT_ADMIN_ROLE) {
        // only admin
    }

    function operatorAction() external onlyRole(OPERATOR_ROLE) {
        // admin or operator
    }
}

Events

Events are cheap to emit, indexed by the blockchain, and readable from the frontend. Emit an event for every state-changing action.

contract TokenSwap {
    event Swap(
        address indexed user,
        address indexed tokenIn,
        address indexed tokenOut,
        uint256 amountIn,
        uint256 amountOut
    );

    function swap(address tokenIn, address tokenOut, uint256 amountIn)
        external
        returns (uint256 amountOut)
    {
        // swap logic...
        emit Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut);
    }
}

Bulldex Finance Swap Contract (simplified)

contract BulldexSwap is ReentrancyGuard, Ownable {
    mapping(address => mapping(address => uint256)) public reserves;

    function swap(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 minAmountOut
    ) external nonReentrant returns (uint256 amountOut) {
        require(amountIn > 0, "Amount must be positive");

        // Transfer tokens in
        IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);

        // Calculate output (simplified constant product formula)
        uint256 reserveIn = reserves[tokenIn][tokenOut];
        uint256 reserveOut = reserves[tokenOut][tokenIn];
        amountOut = (amountIn * reserveOut) / (reserveIn + amountIn);

        require(amountOut >= minAmountOut, "Slippage too high");

        // Update reserves
        reserves[tokenIn][tokenOut] += amountIn;
        reserves[tokenOut][tokenIn] -= amountOut;

        // Transfer tokens out
        IERC20(tokenOut).transfer(msg.sender, amountOut);

        emit Swap(msg.sender, tokenIn, tokenOut, amountIn, amountOut);
    }
}

Testing with Hardhat

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function () {
  let token, owner, addr1;

  beforeEach(async function () {
    [owner, addr1] = await ethers.getSigners();
    const Token = await ethers.getContractFactory("MyToken");
    token = await Token.deploy(1000);
  });

  it("should assign total supply to owner", async function () {
    const balance = await token.balanceOf(owner.address);
    expect(balance).to.equal(ethers.parseUnits("1000", 18));
  });

  it("should transfer tokens", async function () {
    await token.transfer(addr1.address, ethers.parseUnits("100", 18));
    expect(await token.balanceOf(addr1.address))
      .to.equal(ethers.parseUnits("100", 18));
  });
});

Last updated: September 2026.

Last updated · September 2026