iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Contracts

A smart contract is bytecode + storage living at an Ethereum address. Solidity is the dominant language; deployments are immutable; user-facing safety + gas efficiency are the design pressure.

Solidity contract anatomy + tooling

EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

// 1) State variables (storage), events, modifiers, functions
contract Vault is Ownable, ReentrancyGuard, Pausable {
    mapping(address => uint256) public balanceOf;
    uint256 public totalLocked;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event Paused();   // (Pausable also has its own events)

    error InsufficientBalance(uint256 requested, uint256 available);
    error ZeroAmount();

    constructor() Ownable(msg.sender) {}

    // 2) Receive ETH — deposit
    function deposit() external payable whenNotPaused {
        if (msg.value == 0) revert ZeroAmount();
        balanceOf[msg.sender] += msg.value;
        totalLocked           += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    // 3) Withdraw — Checks → Effects → Interactions
    function withdraw(uint256 amount) external nonReentrant whenNotPaused {
        // CHECKS
        uint256 bal = balanceOf[msg.sender];
        if (amount > bal) revert InsufficientBalance(amount, bal);

        // EFFECTS (state changes BEFORE external call)
        balanceOf[msg.sender] = bal - amount;
        totalLocked          -= amount;

        // INTERACTIONS
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
        emit Withdrawn(msg.sender, amount);
    }

    // 4) Owner-only admin
    function pause()   external onlyOwner { _pause(); }
    function unpause() external onlyOwner { _unpause(); }

    // 5) View / pure — no gas if called externally as a constant
    function isEmpty(address user) external view returns (bool) {
        return balanceOf[user] == 0;
    }

    // 6) Receive() / fallback() — handle plain ETH transfers
    receive() external payable {
        deposit();
    }
}

/* ============================================================
   TOOLING
   ============================================================ */

// Foundry — the modern Solidity toolchain
//   forge init my-vault              # scaffold
//   forge build                       # compile
//   forge test                        # run tests (Solidity-native, FAST)
//   forge fmt                         # format
//   forge create --rpc-url … --private-key … src/Vault.sol:Vault
//   forge verify-contract … --etherscan-api-key …

// Hardhat — established JS toolchain
//   npx hardhat init
//   npx hardhat compile
//   npx hardhat test
//   npx hardhat ignition deploy ./ignition/modules/Vault.ts --network sepolia
//   npx hardhat verify --network sepolia <ADDR>

// 7) Tests in Solidity (Foundry)
// test/Vault.t.sol
import "forge-std/Test.sol";
import "../src/Vault.sol";

contract VaultTest is Test {
    Vault v;
    address alice = address(0xA11CE);
    address bob   = address(0xB0B);

    function setUp() public {
        v = new Vault();
        vm.deal(alice, 10 ether);
        vm.deal(bob,   10 ether);
    }

    function testDepositAndWithdraw() public {
        vm.prank(alice);
        v.deposit{value: 1 ether}();
        assertEq(v.balanceOf(alice), 1 ether);

        vm.prank(alice);
        v.withdraw(0.5 ether);
        assertEq(v.balanceOf(alice), 0.5 ether);
    }

    function testCannotWithdrawMoreThanDeposited() public {
        vm.prank(alice);
        v.deposit{value: 1 ether}();
        vm.expectRevert(abi.encodeWithSelector(Vault.InsufficientBalance.selector, 2 ether, 1 ether));
        vm.prank(alice);
        v.withdraw(2 ether);
    }

    function testFuzz_DepositWithdraw(uint96 amount) public {
        vm.assume(amount > 0 && amount <= 5 ether);
        vm.prank(alice);
        v.deposit{value: amount}();
        vm.prank(alice);
        v.withdraw(amount);
        assertEq(v.balanceOf(alice), 0);
    }
}

// 8) Interact off-chain (ethers v6)
import { ethers } from 'ethers';
import VaultABI from './out/Vault.sol/Vault.json' assert { type: 'json' };

const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet   = new ethers.Wallet(PRIVATE_KEY, provider);
const vault    = new ethers.Contract(VAULT_ADDR, VaultABI.abi, wallet);

await (await vault.deposit({ value: ethers.parseEther('1.0') })).wait();
const bal = await vault.balanceOf(wallet.address);
console.log(ethers.formatEther(bal));

await (await vault.withdraw(ethers.parseEther('0.5'))).wait();

// === Best practices ===

// 1) Checks-Effects-Interactions — defeat reentrancy
// 2) Pull-over-push — let users withdraw, don't push payments
// 3) Use Solidity 0.8+ — built-in overflow checks
// 4) Custom errors > require strings — cheaper, more expressive
// 5) Build on OpenZeppelin — battle-tested implementations
// 6) Pin compiler version (^0.8.26 or =0.8.26)
// 7) ReentrancyGuard on every function with an external call
// 8) Pausable + Ownable for emergency stops
// 9) Multisig (Safe) for ownership in production — never an EOA
// 10) Audit before mainnet for non-trivial contracts
// 11) Set up monitoring + alerts (Forta, OpenZeppelin Defender, Tenderly)
// 12) Verify contracts on Etherscan after deploy

// === Common bugs ===
//   • Reentrancy (e.g. The DAO hack)
//   • Integer overflow / underflow (pre 0.8)
//   • Floating-point with /  (Solidity has no floats — use fixed-point libraries)
//   • Default visibility (functions default to internal in 0.5+, but be explicit)
//   • Storage layout collisions in upgradeable contracts
//   • Missing access control on admin functions
//   • Front-running / MEV — use commit-reveal or atomic flash-loans for sensitive ops
//   • Phishing via signed permits — show the user what they're signing

Why it matters

Foundry + OpenZeppelin + CEI pattern + ReentrancyGuard + multisig ownership is the modern Solidity baseline. Pin the compiler, verify on Etherscan, audit before mainnet — immutability is a feature you can’t take back.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint256 public n;
    function bump() external { n += 1; }
}
Try it Yourself »

Exercise

Solidity keyword that starts a contract.

Counter { uint256 public n; }

Discussion

Loading…