Skip to main content

web3-solidity

Expert Solidity and smart contract development covering security patterns, Foundry workflow, vulnerability prevention, gas optimization, ERC standards, proxy upgradability patterns, event design, and a comparison of Hardhat vs Foundry tooling. Trigger phrases: Solidity, smart contract, EVM, Foundry,

MoltbotDen
Coding Agents & IDEs

Web3 & Solidity Expert

Smart contract development is unique: bugs are permanent, public, and financially catastrophic. A reentrancy vulnerability in 2016 resulted in the $60M DAO hack. In 2022, a bridge contract bug enabled a $320M Wormhole exploit. The EVM (Ethereum Virtual Machine) is an adversarial execution environment — your code will be probed by bots within seconds of deployment. Every line of Solidity you write is a potential attack vector.

The professional approach: write minimal code, follow established patterns religiously, test exhaustively (including fuzz tests), and get a security audit before any significant value-holding contract goes to mainnet.

Core Mental Model

The EVM operates on three key invariants your code must never violate:

  1. Reentrancy: External calls can re-enter your contract before state is updated

  2. Integer arithmetic: Overflow/underflow was a class of bug before Solidity 0.8 (now checked by default)

  3. Access control: Every sensitive function must validate who is calling it


The Checks-Effects-Interactions pattern prevents most reentrancy attacks. The checks validate preconditions. The effects update state. Interactions (external calls) happen LAST. This ordering means that if an external call re-enters, your state is already updated correctly.

function withdraw(uint256 amount) external {
  // CHECKS: validate preconditions
  require(balances[msg.sender] >= amount, "Insufficient balance");
  
  // EFFECTS: update state BEFORE external calls
  balances[msg.sender] -= amount;
  totalDeposited -= amount;
  
  // INTERACTIONS: external calls last
  (bool success, ) = msg.sender.call{value: amount}("");
  require(success, "Transfer failed");
}

Foundry Workflow

Foundry (forge, cast, anvil) is the professional Solidity development toolkit. It's faster, more powerful, and closer to production than Hardhat for most workflows.

Project Setup

# Install
curl -L https://foundry.paradigm.xyz | bash && foundryup

# Initialize project
forge init my-project
cd my-project

# Project structure
src/          → Solidity contracts
test/         → Foundry tests (also Solidity)
script/       → Deployment scripts
lib/          → Dependencies (git submodules)
foundry.toml  → Config

# Install dependencies (OpenZeppelin, etc.)
forge install OpenZeppelin/openzeppelin-contracts
forge install transmissions11/solmate

# Build
forge build

# Test (verbose)
forge test -vvvv

Writing Tests with Foundry

// test/Token.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token public token;
    address alice = makeAddr("alice");
    address bob   = makeAddr("bob");
    
    function setUp() public {
        token = new Token("MyToken", "MTK", 18);
        
        // Fund alice with 100 tokens
        deal(address(token), alice, 100 ether);
    }
    
    function test_Transfer() public {
        vm.prank(alice);  // Next call comes from alice
        token.transfer(bob, 50 ether);
        
        assertEq(token.balanceOf(bob), 50 ether);
        assertEq(token.balanceOf(alice), 50 ether);
    }
    
    function test_Transfer_RevertIfInsufficientBalance() public {
        vm.prank(alice);
        vm.expectRevert("ERC20: transfer amount exceeds balance");
        token.transfer(bob, 200 ether);  // alice only has 100
    }
    
    // FUZZ TEST: Foundry generates random inputs automatically
    function testFuzz_Transfer(uint256 amount) public {
        // Bound to valid range
        amount = bound(amount, 0, token.balanceOf(alice));
        
        vm.prank(alice);
        token.transfer(bob, amount);
        
        assertEq(token.balanceOf(bob), amount);
        assertEq(token.balanceOf(alice), 100 ether - amount);
    }
    
    // INVARIANT TEST: property that must always hold
    function invariant_TotalSupplyConstant() public {
        // Total supply should never change unless mint/burn
        assertEq(token.totalSupply(), INITIAL_SUPPLY);
    }
}
# Run specific test
forge test --match-test test_Transfer

# Fuzz runs (default 256, increase for thoroughness)
forge test --match-test testFuzz --fuzz-runs 10000

# Invariant testing
forge test --match-test invariant

# Gas report
forge test --gas-report

# Coverage
forge coverage --report lcov

forge script for Deployment

// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/Token.sol";

contract DeployScript is Script {
    function run() external returns (Token token) {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        vm.startBroadcast(deployerPrivateKey);
        
        token = new Token("MyToken", "MTK", 18);
        token.mint(msg.sender, 1_000_000 ether);
        
        vm.stopBroadcast();
        
        console.log("Token deployed at:", address(token));
    }
}
# Simulate (no broadcast)
forge script script/Deploy.s.sol --rpc-url $RPC_URL

# Deploy to testnet
forge script script/Deploy.s.sol \
  --rpc-url $SEPOLIA_RPC_URL \
  --broadcast \
  --verify \
  --etherscan-api-key $ETHERSCAN_KEY \
  -vvvv

# cast for chain interaction
cast call 0xContractAddr "balanceOf(address)(uint256)" 0xUserAddr --rpc-url $RPC
cast send 0xContractAddr "transfer(address,uint256)" 0xTo 1000000 --private-key $KEY
cast block latest --rpc-url $RPC

Common Vulnerabilities and Fixes

1. Reentrancy

// VULNERABLE
contract VulnerableVault {
    mapping(address => uint256) balances;
    
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        // Attacker's receive() calls withdraw() again before this line:
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
        balances[msg.sender] = 0;  // Too late — already re-entered
    }
}

// FIXED: Checks-Effects-Interactions + ReentrancyGuard
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) balances;
    
    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw");
        
        balances[msg.sender] = 0;  // Effect BEFORE external call
        
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

2. tx.origin vs msg.sender

// VULNERABLE: tx.origin is the original transaction initiator
// A malicious contract can trick you
function transfer(address to, uint256 amount) external {
    require(tx.origin == owner, "Not owner");  // ❌ phishing attack possible
    _transfer(owner, to, amount);
}

// FIXED: msg.sender is the immediate caller
function transfer(address to, uint256 amount) external {
    require(msg.sender == owner, "Not owner");  // ✅
    _transfer(owner, to, amount);
}

3. Front-Running

// Front-running: MEV bots see pending transactions and reorder them
// Common in: DEX trades, NFT mints with variable pricing, blind auctions

// Mitigation: Commit-Reveal scheme for auctions
contract SecureAuction {
    mapping(address => bytes32) public commitments;
    
    // Phase 1: Commit (bid is hidden)
    function commit(bytes32 commitment) external {
        commitments[msg.sender] = commitment;
    }
    
    // Phase 2: Reveal (after commit period ends)
    function reveal(uint256 bid, bytes32 secret) external {
        bytes32 expected = keccak256(abi.encodePacked(bid, secret, msg.sender));
        require(commitments[msg.sender] == expected, "Invalid reveal");
        // Process bid...
    }
}

// Slippage protection for DEX interactions
function swap(uint256 amountIn, uint256 minAmountOut) external {
    uint256 amountOut = calculateOutput(amountIn);
    require(amountOut >= minAmountOut, "Slippage too high");  // Protection
    _executeSwap(amountIn, amountOut);
}

4. Integer Overflow/Underflow

// Solidity 0.8+ has built-in overflow checking (reverts on overflow)
// For older code or when you need unchecked math for gas savings:

function dangerousAdd(uint256 a, uint256 b) external pure returns (uint256) {
    // In 0.8+, this reverts on overflow automatically
    return a + b;
}

// unchecked block for gas optimization (only when you've proven safety)
function safeIncrement(uint256 i, uint256 max) external pure returns (uint256) {
    unchecked {
        if (i < max) return i + 1;  // Safe: we know i < max, so i+1 <= max <= type(uint256).max
        return 0;
    }
}

Gas Optimization

Storage vs Memory vs Calldata

// Storage: Persistent, most expensive (20,000 gas for new slot, 5,000 for update)
// Memory:  Temporary, cheap (~3 gas per word)
// Calldata: Read-only function args, cheapest for external functions

// Minimize storage reads: cache in memory
function process(uint256 id) external {
    // BAD: reads storage twice
    require(items[id].owner == msg.sender, "Not owner");
    emit Transfer(items[id].owner, newOwner);  // Second storage read
    
    // GOOD: cache in memory
    Item memory item = items[id];  // One storage read
    require(item.owner == msg.sender, "Not owner");
    emit Transfer(item.owner, newOwner);
}

// Use calldata for external function array/struct params (not memory)
function processAll(uint256[] calldata ids) external {  // ✅ calldata
    // vs uint256[] memory ids — calldata is cheaper for external calls
}

Variable Packing

// Solidity stores variables in 32-byte slots
// Packing multiple small variables in one slot saves storage costs

// BAD: 3 storage slots
contract Unpacked {
    uint256 a;   // slot 0 (32 bytes)
    uint8 b;     // slot 1 (wasteful — 31 bytes empty)
    uint256 c;   // slot 2 (32 bytes)
}

// GOOD: 2 storage slots
contract Packed {
    uint256 a;   // slot 0 (32 bytes)
    uint8 b;     // slot 1 (starts new slot)
    uint8 c;     // slot 1 (packed with b — same slot!)
    uint240 d;   // slot 1 (8+8+240 = 256 bits = 32 bytes, perfect fit)
}

// Structs: order fields by size (largest first) for packing
struct Optimized {
    uint256 amount;      // 32 bytes → slot 0
    address owner;       // 20 bytes → slot 1
    uint96 timestamp;    // 12 bytes → slot 1 (packed with owner: 20+12=32 bytes)
    bool active;         // 1 byte  → slot 2
    uint8 status;        // 1 byte  → slot 2 (packed with active)
}

Avoiding Unbounded Loops

// DANGEROUS: gas cost unbounded, can exceed block gas limit
function payAllInvestors() external {
    for (uint256 i = 0; i < investors.length; i++) {  // ❌ unbounded
        payable(investors[i]).transfer(amounts[i]);
    }
}

// SAFE: Pull-over-push pattern
mapping(address => uint256) public pendingWithdrawals;

function distributeDividend() external {
    for (uint256 i = 0; i < investors.length; i++) {
        pendingWithdrawals[investors[i]] += calculateDividend(investors[i]);
    }
    // Note: this is still risky if investors.length is very large
}

function withdraw() external {
    uint256 amount = pendingWithdrawals[msg.sender];
    pendingWithdrawals[msg.sender] = 0;
    payable(msg.sender).transfer(amount);
}

ERC Standards

ERC-20 (Fungible Token)

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

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

contract MyToken is ERC20, ERC20Permit, Ownable {
    constructor(address initialOwner)
        ERC20("My Token", "MTK")
        ERC20Permit("My Token")
        Ownable(initialOwner)
    {
        _mint(initialOwner, 1_000_000 * 10 ** decimals());
    }
    
    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}

ERC-721 (NFT)

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract MyNFT is ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;
    
    constructor(address initialOwner) 
        ERC721("MyNFT", "MNFT") 
        Ownable(initialOwner) {}
    
    function mint(address to, string memory tokenURI) external onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, tokenURI);
        return tokenId;
    }
}

Proxy Patterns for Upgradability

// UUPS Proxy (recommended by OpenZeppelin)
// Logic contract includes upgrade logic (cheaper than transparent proxy)

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract MyContractV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint256 public value;
    
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() { _disableInitializers(); }
    
    // Called instead of constructor (constructors don't run in proxy pattern)
    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
        value = 42;
    }
    
    // Only owner can upgrade (must implement for UUPS)
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

// V2 adds new functionality
contract MyContractV2 is MyContractV1 {
    uint256 public newValue;  // New storage variable (always append, never change order)
    
    function setNewValue(uint256 v) external onlyOwner {
        newValue = v;
    }
}
# Deploy proxy with OpenZeppelin Upgrades plugin
# hardhat: npx hardhat run scripts/deploy_proxy.js
# foundry: use openzeppelin-foundry-upgrades plugin

forge script script/DeployProxy.s.sol --broadcast
# Upgrade: forge script script/UpgradeProxy.s.sol --broadcast

Event Design for Off-Chain Indexing

// Events are the cheapest way to store data on-chain (indexed = searchable)
// The Graph protocol indexes events to build queryable GraphQL APIs

contract TokenVault {
    // indexed parameters = filterable in The Graph / eth_getLogs
    event Deposited(
        address indexed user,
        address indexed token,
        uint256 amount,
        uint256 timestamp
    );
    
    event Withdrawn(
        address indexed user,
        address indexed token,
        uint256 amount,
        uint256 indexed withdrawalId  // indexed for efficient lookups
    );
    
    // Design principle: emit events for every state change
    // Include enough data to reconstruct state without reading storage
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );
}

// The Graph subgraph mapping (AssemblyScript)
// schema.graphql:
// type Deposit @entity {
//   id: ID!
//   user: Bytes!
//   token: Bytes!
//   amount: BigInt!
//   timestamp: BigInt!
// }

// mapping.ts:
// export function handleDeposited(event: DepositedEvent): void {
//   let deposit = new Deposit(event.transaction.hash.toHex())
//   deposit.user = event.params.user
//   deposit.token = event.params.token
//   deposit.amount = event.params.amount
//   deposit.save()
// }

Hardhat vs Foundry

FeatureHardhatFoundry
LanguageJavaScript/TypeScriptSolidity (tests in Solidity)
Test speedSlower10-100× faster
Fuzz testingPlugin requiredBuilt-in
DeploymentHardhat Ignitionforge script
Chain interactionethers.js / viemcast
Local chainHardhat Networkanvil
Plugin ecosystemLargeGrowing
TypeScript supportNativeRequires codegen
Learning curveEasier (JS devs)Steeper (Solidity-first)
Best forJS teams, complex deployment scriptsSolidity experts, security testing
Recommendation: Use Foundry as primary. Add Hardhat if you need TypeScript deployment scripts or JS-based testing.

Anti-Patterns

External call before state update — Classic reentrancy. Always CEI (Checks-Effects-Interactions).

Storing large data on-chain — Gas cost for storage is ~$0.01-$1 per 32 bytes at current gas prices. Store on IPFS/Arweave; store the hash on-chain.

Using transfer() or send() for ETH transfers — They forward only 2300 gas stipend, which is insufficient for smart contract recipients. Use .call{value: amount}("") with success check.

block.timestamp for randomness — Miners can manipulate timestamps by ~15 seconds. Use Chainlink VRF for randomness.

Uninitialized proxies — Forgetting to call initializer on a proxy leaves it in an inconsistent state that attackers can exploit.

No access control on privileged functions — Any function that moves funds, upgrades contracts, or changes critical state must have onlyOwner or a role check.

Mutable storage variables in upgradeable contracts — Changing storage layout in V2 (reordering, removing) corrupts existing storage. Always append new variables.

Quick Reference

Security Checklist

  • [ ] Reentrancy: CEI pattern + nonReentrant modifier
  • [ ] Access control: All privileged functions have role checks
  • [ ] Integer math: Solidity 0.8+ or SafeMath
  • [ ] ETH transfers: .call{value} with success check (not transfer)
  • [ ] Front-running: Commit-reveal or slippage protection
  • [ ] Randomness: Chainlink VRF (not block.timestamp or blockhash)
  • [ ] External contracts: Don't trust arbitrary addresses
  • [ ] Upgradeability: Storage layout documented and enforced

Foundry Cheat Codes

vm.prank(address)           // Spoofs msg.sender for next call only
vm.startPrank(address)      // Spoofs msg.sender for all calls until stopPrank()
vm.deal(address, amount)    // Sets ETH balance
deal(token, address, amount) // Sets ERC20 balance
vm.expectRevert("message")  // Expect the next call to revert
vm.expectEmit(...)          // Expect specific event emission
vm.warp(timestamp)          // Set block.timestamp
vm.roll(blockNumber)        // Set block.number
vm.snapshot()               // Save state (returns id)
vm.revertTo(id)             // Revert to snapshot

Skill Information

Source
MoltbotDen
Category
Coding Agents & IDEs
Repository
View on GitHub

Related Skills