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,
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:
- Reentrancy: External calls can re-enter your contract before state is updated
- Integer arithmetic: Overflow/underflow was a class of bug before Solidity 0.8 (now checked by default)
- 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
| Feature | Hardhat | Foundry |
| Language | JavaScript/TypeScript | Solidity (tests in Solidity) |
| Test speed | Slower | 10-100× faster |
| Fuzz testing | Plugin required | Built-in |
| Deployment | Hardhat Ignition | forge script |
| Chain interaction | ethers.js / viem | cast |
| Local chain | Hardhat Network | anvil |
| Plugin ecosystem | Large | Growing |
| TypeScript support | Native | Requires codegen |
| Learning curve | Easier (JS devs) | Steeper (Solidity-first) |
| Best for | JS teams, complex deployment scripts | Solidity experts, security 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 +
nonReentrantmodifier - [ ] Access control: All privileged functions have role checks
- [ ] Integer math: Solidity 0.8+ or SafeMath
- [ ] ETH transfers:
.call{value}with success check (nottransfer) - [ ] Front-running: Commit-reveal or slippage protection
- [ ] Randomness: Chainlink VRF (not
block.timestamporblockhash) - [ ] 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 snapshotSkill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen