Smart Contracts

Programmable money and autonomous code — how self-executing agreements written in Solidity run on the world's decentralised computer, what makes them vulnerable, and how engineers secure and optimise them.

Prereq: basic blockchain concepts, some programming experience Time to read: ~35 min Interactive figures: 1

1. What is a smart contract?

In 1994, cryptographer Nick Szabo described smart contracts as "a set of promises, specified in digital form, including protocols within which the parties perform on these promises." The canonical example he gave was a vending machine: insert correct payment, receive item — no clerk needed, no possibility of the machine deciding to keep your money. The rules are enforced mechanically.

On Ethereum, a smart contract is a program deployed to a deterministic state machine. It executes identically on every full node in the network. Once deployed, the code is immutable (absent upgrade patterns). Trust does not come from trusting the counterparty — it comes from the network verifying execution.

THE CORE GUARANTEE

If the code says "transfer 1 ETH when condition X is met," the network will do exactly that, without asking anyone's permission, without the possibility of the deployer reversing it. The code is the agreement.

Five key properties

The oracle problem

Smart contracts execute in a closed, deterministic environment. They cannot natively fetch the BTC/USD price, the weather in London, or the outcome of an election. They are isolated from the external world by design.

The solution is an oracle — a trusted (or cryptoeconomically incentivised) external service that writes real-world data on-chain. Chainlink is the dominant oracle network: a decentralised set of nodes aggregates data feeds and writes the result to a smart contract, which other protocols query. The security assumption shifts: your contract is only as trustworthy as its oracle.

IMPORTANT DISTINCTION

The blockchain guarantees that code executes as written. It makes no guarantee that the inputs to that code are accurate. Garbage in, garbage out — just immutably.

2. The Ethereum Virtual Machine

The EVM is a 256-bit stack machine. Every integer operand is a 256-bit (32-byte) word. The architecture has five data regions:

Ethereum has two types of accounts. Externally Owned Accounts (EOAs) are controlled by private keys — wallets like MetaMask. They have a nonce (transaction counter) and an ETH balance, but no code and no storage. Contract accounts are controlled by their deployed bytecode. They have a nonce, a balance, a codeHash pointing to their bytecode, and a storageRoot — the root of a Merkle Patricia trie holding all their persistent state.

EVM Account Model — EOA vs Contract Account
EOA (Externally Owned) nonce tx count (replay protect) balance ETH in wei codeHash ∅ (empty) storageRoot ∅ (empty) controlled by private key Contract Account nonce contract create count balance ETH in wei codeHash → EVM bytecode storageRoot → Patricia trie controlled by bytecode logic vs
Both account types share the same four fields in the Ethereum state trie. EOAs have empty code and storage; contracts have both. A contract's storageRoot is the root of a Merkle Patricia trie mapping 256-bit slot keys to 256-bit values.
EVM Memory Model
Stack
A LIFO structure with a maximum depth of 1,024 items. Each item is one 256-bit word. Almost all EVM arithmetic operates on the stack. Why 256 bits? To natively handle Ethereum addresses (160 bits) and keccak256 hashes (256 bits) without truncation.
Memory
Byte-addressable scratch space that exists only for the duration of a call. Expands in 32-byte chunks. Gas cost is quadratic in the number of words used: $\text{gas} = 3w + \lfloor w^2/512 \rfloor$ where $w$ is the number of 32-byte words. Accessing high memory addresses is deliberately expensive to prevent unbounded memory allocation.
Storage
A persistent key–value store mapping 256-bit keys to 256-bit values. Per-contract, survives between transactions. Very expensive: 20,000 gas to write a non-zero value to a previously zero slot (SSTORE), 2,100 gas for a cold read (SLOAD). The EVM's most significant cost centre.
Calldata
Read-only data passed from the transaction initiator. Cheap: 16 gas per non-zero byte, 4 gas per zero byte. Function selectors and arguments arrive here. Use calldata keyword in Solidity for read-only external function parameters to avoid copying to memory.
Code
The contract's deployed bytecode. Immutable. Accessed by the CODECOPY opcode and read by EXTCODECOPY for other contracts' code.

EVM instruction set highlights

CategoryOpcodesNotes
ArithmeticADD, SUB, MUL, DIV, SDIV, MOD, ADDMOD, MULMOD, EXPAll modular $2^{256}$; no built-in overflow check pre-0.8
StackPUSH1–PUSH32, POP, DUP1–DUP16, SWAP1–SWAP16PUSH pushes N-byte literal; DUP/SWAP reference stack depth
MemoryMLOAD, MSTORE, MSTORE8MLOAD reads 32 bytes; MSTORE8 writes 1 byte
StorageSLOAD, SSTOREMost expensive opcodes; warm vs. cold access after EIP-2929
Flow controlJUMP, JUMPI, PC, JUMPDEST, STOPJUMPI = conditional jump; destination must be JUMPDEST
EnvironmentADDRESS, CALLER, CALLVALUE, CALLDATALOAD, CALLDATASIZE, BLOCKHASH, TIMESTAMP, NUMBERTransaction and block context
CallsCALL, STATICCALL, DELEGATECALL, CREATE, CREATE2DELEGATECALL runs target code in caller's storage context
LoggingLOG0–LOG4Emit events; indexed by up to 4 topics; cheap but not queryable on-chain
SystemRETURN, REVERT, SELFDESTRUCT, INVALIDREVERT refunds remaining gas; SELFDESTRUCT deprecated (EIP-6780)

Gas: the anti-halting-problem tax

The EVM is Turing-complete. Without some mechanism to bound execution, a malicious contract could run an infinite loop and halt all nodes. The solution is gas: every opcode costs a fixed amount of gas, deducted from the transaction's gas limit. If gas runs out, execution reverts and the fee is forfeit. This makes infinite loops economically impossible — they would exhaust the gas budget first.

$$\text{transaction fee} = \text{gas used} \times \text{gas price (gwei)}$$

Representative costs: SSTORE (new slot) = 20,000 gas, CALL to new address = 25,000 gas, ADD = 3 gas, MUL = 5 gas, simple ETH transfer = 21,000 gas base.

ABI encoding: how calldata works

When you call a contract function, the EVM doesn't know function names — it only sees bytes. The Application Binary Interface (ABI) is the convention for encoding function calls.

The first 4 bytes are the function selector: the first 4 bytes of the keccak256 hash of the canonical function signature string.

EXAMPLE: ERC-20 transfer

Signature string: transfer(address,uint256)
keccak256 → 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
Selector (first 4 bytes): 0xa9059cbb

Full calldata for transfer(0xABCD...1234, 1000000000000000000):
0xa9059cbb  ← selector
000000000000000000000000ABCD...1234  ← address, left-padded to 32 bytes
0000000000000000000000000000000000000000000000000de0b6b3a7640000  ← 1e18 in hex

Dynamic types (strings, bytes, dynamic arrays) are encoded with an offset to their data and a length prefix. Static types (uint256, address, bool, bytes32) are encoded inline, each padded to 32 bytes.

3. Solidity fundamentals

Solidity is a statically-typed, curly-brace language inspired by JavaScript and C++, compiled to EVM bytecode. The current stable version is 0.8.x, which introduced built-in checked arithmetic (overflow reverts instead of wrapping).

Types

Data locations

Every reference type has a data location modifier. Getting this wrong is a common source of bugs and gas waste.

Visibility and mutability

Errors and events

require(condition, "message") reverts the entire transaction if the condition is false, refunding remaining gas and returning the message. revert CustomError(args) (Solidity 0.8.4+) is cheaper and allows typed error arguments.

Events are logged to a Bloom filter stored in the transaction receipt. They cost roughly 375 gas base + 8 gas/byte of data + 375 gas per topic. They cannot be read by smart contracts on-chain, but they are queryable by clients (ethers.js, web3.py) via eth_getLogs. Use events for historical records, not for data your contract needs to query itself.

A minimal contract: counter

Counter.sol — all basic Solidity concepts in one contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title Counter — minimal Solidity example
contract Counter {
    // ── State variables (live in storage) ────────────────────────
    uint256 private _count;
    address public  owner;

    // ── Events ────────────────────────────────────────────────────
    event Incremented(address indexed by, uint256 newCount);
    event Decremented(address indexed by, uint256 newCount);
    event Reset(address indexed by);

    // ── Custom errors (cheaper than string revert) ────────────────
    error NotOwner();
    error Underflow();

    // ── Modifier ─────────────────────────────────────────────────
    modifier onlyOwner() {
        if (msg.sender != owner) revert NotOwner();
        _;  // placeholder for function body
    }

    // ── Constructor ───────────────────────────────────────────────
    constructor() {
        owner = msg.sender;
        _count = 0;
    }

    // ── External / public functions ───────────────────────────────

    /// @notice Increment the counter by 1
    function increment() external {
        _count += 1;                    // 0.8+ checked; reverts on overflow
        emit Incremented(msg.sender, _count);
    }

    /// @notice Decrement the counter by 1 (reverts at zero)
    function decrement() external {
        if (_count == 0) revert Underflow();
        _count -= 1;
        emit Decremented(msg.sender, _count);
    }

    /// @notice Reset counter to zero (owner only)
    function reset() external onlyOwner {
        _count = 0;
        emit Reset(msg.sender);
    }

    /// @notice Read the current count — free (view function)
    function get() external view returns (uint256) {
        return _count;
    }

    // ── Pure helper example ───────────────────────────────────────

    /// @notice Return x squared — no state access at all
    function square(uint256 x) external pure returns (uint256) {
        return x * x;
    }
}
contract Counter:
    state:
        _count: uint256  = 0
        owner: address   = deployer

    modifier onlyOwner:
        if caller != owner: revert
        run function body

    function increment():
        _count = _count + 1          // reverts on overflow (0.8+)
        emit Incremented(caller, _count)

    function decrement():
        if _count == 0: revert Underflow
        _count = _count - 1
        emit Decremented(caller, _count)

    function reset() [onlyOwner]:
        _count = 0
        emit Reset(caller)

    function get() [view] -> uint256:
        return _count

    function square(x) [pure] -> uint256:
        return x * x

4. A complete ERC-20 token

ERC-20 is Ethereum's token standard, defined in EIP-20. Every fungible token — USDC, DAI, UNI, LINK — implements this interface. The standard ensures any wallet or exchange can interact with any token without token-specific logic.

ERC-20 Transfer Flow
Alice 0xA1… transfer(Bob, 100) ERC-20 Contract balances[Alice] -= 100 balances[Bob] += 100 Transfer event Bob 0xB2… approve / transferFrom path: Alice owner approve(Spender,amt) ERC-20 Contract allowances[Alice][Spender]=amt transferFrom(…) Spender DEX / protocol Top path: direct transfer. Bottom path: approve then transferFrom (used by DEXes, lending protocols).
The transfer() path lets Alice send tokens directly. The approve() + transferFrom() path lets Alice authorise a smart contract (Spender) to pull tokens on her behalf — the mechanism behind every DEX trade and DeFi deposit.

The IERC20 interface

IERC20.sol — the full standard interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC20 {
    // ── Events ────────────────────────────────────────────────────

    /// @dev Emitted when `value` tokens are moved from `from` to `to`.
    ///      Also emitted when tokens are minted (from = address(0))
    ///      or burned (to = address(0)).
    event Transfer(address indexed from, address indexed to, uint256 value);

    /// @dev Emitted when the allowance of `spender` over `owner`'s tokens
    ///      is set by a call to approve(). `value` is the new allowance.
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // ── Read functions ────────────────────────────────────────────

    /// @return Total token supply
    function totalSupply() external view returns (uint256);

    /// @return Token balance of `account`
    function balanceOf(address account) external view returns (uint256);

    /// @return Remaining tokens that `spender` is allowed to spend
    ///         on behalf of `owner` via transferFrom().
    function allowance(address owner, address spender)
        external view returns (uint256);

    // ── Write functions ───────────────────────────────────────────

    /// @notice Move `amount` tokens from the caller to `to`.
    /// @return True on success (always; failure reverts)
    function transfer(address to, uint256 amount) external returns (bool);

    /// @notice Approve `spender` to spend `amount` of caller's tokens.
    /// @return True on success
    function approve(address spender, uint256 amount) external returns (bool);

    /// @notice Move `amount` tokens from `from` to `to` using the
    ///         allowance mechanism. Decrements allowance.
    /// @return True on success
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool);
}

Complete implementation

ERC20.sol — 85-line self-contained implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./IERC20.sol";

contract ERC20 is IERC20 {
    // ── Storage ───────────────────────────────────────────────────
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;
    uint256 private _totalSupply;

    string public name;
    string public symbol;
    uint8  public constant decimals = 18;

    // ── Constructor ───────────────────────────────────────────────
    constructor(string memory _name, string memory _symbol, uint256 initialSupply) {
        name   = _name;
        symbol = _symbol;
        _mint(msg.sender, initialSupply * 10 ** decimals);
    }

    // ── IERC20 view functions ─────────────────────────────────────
    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) external view returns (uint256) {
        return _balances[account];
    }

    function allowance(address owner, address spender)
        external view returns (uint256)
    {
        return _allowances[owner][spender];
    }

    // ── IERC20 write functions ────────────────────────────────────
    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        require(spender != address(0), "ERC20: approve to zero address");
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount)
        external returns (bool)
    {
        uint256 currentAllowance = _allowances[from][msg.sender];
        require(currentAllowance >= amount, "ERC20: insufficient allowance");
        // Checks-effects: update allowance BEFORE transfer
        unchecked {
            _allowances[from][msg.sender] = currentAllowance - amount;
        }
        emit Approval(from, msg.sender, _allowances[from][msg.sender]);
        _transfer(from, to, amount);
        return true;
    }

    // ── Internal helpers ──────────────────────────────────────────

    /// @dev Core transfer logic. Validates, updates state, emits event.
    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "ERC20: transfer from zero address");
        require(to   != address(0), "ERC20: transfer to zero address");

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");

        // Effects before any external interaction (not that we have any here)
        unchecked {
            _balances[from] = fromBalance - amount;  // can't underflow: checked above
        }
        _balances[to] += amount;                     // can't overflow: total supply bounded

        emit Transfer(from, to, amount);
    }

    /// @dev Mint `amount` new tokens to `account`. Increases total supply.
    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "ERC20: mint to zero address");
        _totalSupply      += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);
    }

    /// @dev Burn `amount` tokens from `account`. Decreases total supply.
    function _burn(address account, uint256 amount) internal {
        require(account != address(0), "ERC20: burn from zero address");
        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn exceeds balance");
        unchecked { _balances[account] = accountBalance - amount; }
        _totalSupply -= amount;
        emit Transfer(account, address(0), amount);
    }
}
contract ERC20:
    state:
        _balances:    mapping(address -> uint256)
        _allowances:  mapping(address -> mapping(address -> uint256))
        _totalSupply: uint256
        name, symbol: string
        decimals:     uint8 = 18

    constructor(name, symbol, initialSupply):
        self.name   = name
        self.symbol = symbol
        _mint(deployer, initialSupply * 10^18)

    // ── View ─────────────────────────────────────────────────────
    totalSupply()  → _totalSupply
    balanceOf(a)   → _balances[a]
    allowance(o,s) → _allowances[o][s]

    // ── Transfer ─────────────────────────────────────────────────
    transfer(to, amount):
        _transfer(caller, to, amount)
        return true

    approve(spender, amount):
        _allowances[caller][spender] = amount
        emit Approval(caller, spender, amount)
        return true

    transferFrom(from, to, amount):
        check _allowances[from][caller] >= amount
        _allowances[from][caller] -= amount      // effect before interaction
        _transfer(from, to, amount)
        return true

    // ── Internal ─────────────────────────────────────────────────
    _transfer(from, to, amount):
        check from != 0, to != 0
        check _balances[from] >= amount
        _balances[from] -= amount
        _balances[to]   += amount
        emit Transfer(from, to, amount)

    _mint(account, amount):
        _totalSupply      += amount
        _balances[account] += amount
        emit Transfer(0, account, amount)

    _burn(account, amount):
        check _balances[account] >= amount
        _balances[account] -= amount
        _totalSupply       -= amount
        emit Transfer(account, 0, amount)

5. Common patterns

Ownable

Almost every production contract needs administrative functions callable only by the deployer or a governance address. The Ownable pattern standardises this:

Ownable.sol — owner access control
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract Ownable {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    error OwnableUnauthorized(address account);

    constructor() {
        _owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    function owner() public view returns (address) { return _owner; }

    modifier onlyOwner() {
        if (msg.sender != _owner) revert OwnableUnauthorized(msg.sender);
        _;
    }

    /// @notice Transfer ownership to a new address. Use renounceOwnership()
    ///         to leave the contract without an owner (disables onlyOwner forever).
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is zero address");
        address old = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(old, newOwner);
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }
}

Reentrancy guard

The checks-effects-interactions pattern requires that you update all state before making external calls. A reentrancy guard enforces this as a mutex:

ReentrancyGuard.sol — mutex for external calls
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

abstract contract ReentrancyGuard {
    // Using 1 and 2 rather than 0 and 1 saves gas on resets
    // (avoids writing a zero value, which triggers the 20,000 → 2,900 SSTORE discount)
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED     = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status == _NOT_ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

// Usage example: a simple ETH withdrawal
contract Vault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Effects BEFORE interaction — critical!
        balances[msg.sender] -= amount;

        // Interaction last
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Transfer failed");
    }
}

Upgradeable proxy pattern

Deployed contract code is immutable. But the Ethereum community developed proxy patterns that preserve upgradeability while keeping this guarantee for a given implementation:

HOW PROXIES WORK

A proxy contract holds all the user's storage and receives all calls. When a function is called that the proxy doesn't define, it DELEGATECALLs to an implementation contract — which executes in the proxy's storage context. To upgrade, you point the proxy at a new implementation. Storage layout must be preserved between upgrades.

Proxy Storage Layout & EIP-1967 Fix
Implementation slot 0 owner address slot 1 balance uint256 slot 2 data bytes32 Proxy (naive — CLASH) slot 0 _implementation ← overwrites impl's slot 0! slot 1 _admin CLASH EIP-1967 Fix — pseudo-random storage slot Implementation slot = keccak256("eip1967.proxy.implementation") - 1 = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc Chosen specifically to avoid any Solidity sequential slot. Admin slot uses a different keccak256 seed.
A naive proxy stores _implementation in slot 0 — clashing with the implementation's slot 0 variable. EIP-1967 solves this by storing the implementation address at a pseudo-random slot derived from a keccak256 hash, which will never be assigned by the Solidity sequential slot allocator.

6. Security vulnerabilities

HIGH STAKES

Smart contracts hold real value: billions of dollars locked in DeFi protocols. A single bug, once deployed, is exploitable by anyone in the world. There is no patch Tuesday. There is no reversing the transaction (usually). Security is not optional.

Reentrancy — The DAO hack (2016, $60M)

In June 2016, an attacker drained approximately $60M from The DAO (a decentralised venture fund) by exploiting reentrancy. The vulnerable code looked like this:

Reentrancy — vulnerable vs. fixed
// VULNERABLE — do NOT deploy
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() external payable { balances[msg.sender] += msg.value; }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");

        // INTERACTION before EFFECT — the fatal mistake
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);

        // By the time we reach here the attacker has already re-entered
        // and called withdraw() again with the same (stale) balance
        balances[msg.sender] = 0;  // too late
    }
}
// FIXED — checks-effects-interactions pattern
contract SafeVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function deposit() external payable { balances[msg.sender] += msg.value; }

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");

        // EFFECT first — zero out balance before external call
        balances[msg.sender] = 0;

        // INTERACTION last
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "transfer failed");
    }
}
// The attacker's contract
contract Attacker {
    VulnerableVault public victim;
    uint256 public count;

    constructor(address _victim) { victim = VulnerableVault(_victim); }

    // Seed the attack: deposit a small amount so we have a balance
    function attack() external payable {
        require(msg.value >= 1 ether);
        victim.deposit{value: msg.value}();
        victim.withdraw();  // trigger the chain
    }

    // Fallback fires when victim sends ETH
    receive() external payable {
        count++;
        if (count < 10 && address(victim).balance >= 1 ether) {
            victim.withdraw();  // re-enter before balance is zeroed
        }
    }
}

The attacker's receive() fallback fires whenever the victim sends ETH. Because the victim hadn't zeroed the balance yet, the check amount > 0 still passes on re-entry. The attack runs until the vault is empty or gas runs out.

Reentrancy Attack — Call Stack Animation
Press Play to animate the reentrancy call stack.

Integer overflow / underflow

Before Solidity 0.8.0, arithmetic silently wrapped. A classic exploit: if a contract decremented a uint256 past zero, you'd get $2^{256} - 1$ — an astronomically large number that could give you unlimited token balance.

$$\text{uint256}: \quad 0 - 1 \equiv 2^{256} - 1 \pmod{2^{256}}$$

Fix: use Solidity ≥0.8.0 (checked by default) or OpenZeppelin's SafeMath library for older code. In 0.8+ code, use unchecked {} blocks only when you have mathematically proven the operation cannot overflow.

Access control failures

Missing or incorrect access control is consistently the second-largest exploit category by value. A real-world pattern:

Access control — missing modifier
contract BadToken {
    address public owner;
    mapping(address => uint256) public balances;

    constructor() { owner = msg.sender; }

    // MISSING onlyOwner — ANYONE can call this
    function mint(address to, uint256 amount) external {
        balances[to] += amount;
    }

    // MISSING onlyOwner — ANYONE can destroy the contract
    function destroy() external {
        selfdestruct(payable(msg.sender));  // sends all ETH to attacker
    }
}
contract GoodToken is Ownable {
    mapping(address => uint256) public balances;

    // onlyOwner modifier prevents unauthorised minting
    function mint(address to, uint256 amount) external onlyOwner {
        balances[to] += amount;
    }

    // renounceOwnership() or transferOwnership() via Ownable
    // selfdestruct removed — prefer pausing mechanisms
}

Front-running (MEV)

Ethereum transactions are broadcast to a public mempool before inclusion in a block. Validators (and specialised bots) can observe pending transactions and insert their own transactions before them by paying a higher gas price — a practice called Maximal Extractable Value (MEV).

Classic attacks:

Mitigations: commit-reveal schemes (submit a hash first, reveal the value later); slippage tolerance limits in DEX trades; private transaction pools (Flashbots Protect).

Oracle manipulation — Mango Markets (2022, $116M)

Mango Markets used a TWAP (time-weighted average price) derived from thin on-chain order books to price its MNGO collateral. An attacker purchased massive amounts of MNGO perp futures on two accounts (one long, one short), temporarily driving the on-chain oracle price from ~$0.03 to ~$0.91. With the artificially inflated collateral value, they borrowed $116M in other tokens, then price collapsed. The "borrowed" funds were unrecoverable.

Fix: use robust, manipulation-resistant oracles (Chainlink, Uniswap v3 TWAP with long windows), circuit breakers on price deviations, borrow caps per asset.

Signature replay and EIP-712

If a contract accepts off-chain signatures for authorisations (e.g., permit(), meta-transactions), a signature valid on Ethereum Mainnet is also valid on every chain that forks it — unless the signed message includes the chain ID.

EIP-712 standardises typed structured data hashing. The domain separator includes chainId and the contract's address, making signatures chain- and contract-specific. Always use EIP-712 for any off-chain signed authorisation.

7. Gas optimization

Gas is money. On a busy chain, a single contract call can cost $5–$50. Optimisation is not premature — it is the difference between a usable protocol and an unusable one.

TechniqueApprox. savingNotes
Use calldata instead of memory for read-only params ~200 gas / 32-byte word Avoids CALLDATACOPY into memory. Valid for external functions only.
Pack storage slots ~15,000 gas per avoided slot EVM writes 32-byte slots. Put uint128 a; uint128 b; together — one SSTORE instead of two.
Use uint256 not uint8 in local variables ~20–100 gas EVM pads all values to 256 bits anyway. Smaller types cost a masking opcode inside functions. Exception: storage packing (above).
unchecked {} for safe arithmetic ~40 gas per operation Only when you can mathematically prove no overflow. E.g., for (uint i; i < n; ++i) — increment can't overflow a 256-bit counter.
Cache storage reads in memory ~2,000 gas per avoided SLOAD A cold SLOAD = 2,100 gas; a warm one = 100 gas. A MLOAD costs 3 gas. uint256 cached = storageVar; and use cached in a loop.
Use events instead of storage for history ~19,000 gas per data point Emitting an event with 2 topics and 32 bytes data ≈ 1,500 gas. Storing the same data in a slot = 20,000 gas. Events are unreadable on-chain but perfectly fine for off-chain indexers.
Minimise contract size (EIP-170 limit: 24 KB) Deployment cost Deployment costs 200 gas/byte. Use libraries, split logic, remove dead code. --via-ir with the Solidity optimizer can significantly reduce output size.
++i not i++ ~5 gas Post-increment creates a temporary value. In unchecked loops it adds up over thousands of iterations.
OPTIMIZER TIP

Enable the Solidity optimizer with --optimize --optimize-runs 200. The "runs" value is a hint about how many times each function will be called: higher values optimise for runtime gas at the cost of larger bytecode; lower values optimise for deployment cost. DeFi protocols that expect heavy usage set runs to 1,000,000.

8. Beyond Ethereum

Ethereum pioneered smart contracts, but the ecosystem has diversified. Each platform makes different trade-offs between throughput, safety, and programmability.

PlatformVM / RuntimeLanguageTPS (approx.)Key property
Ethereum EVM (256-bit stack) Solidity, Vyper, Yul ~15–30 (L1), ~2,000+ (rollups) Largest ecosystem, most tooling, most audited patterns
Solana Sealevel (parallel BPF/eBPF) Rust (Anchor framework) ~50,000 theoretical Parallel execution; account model (state passed in by caller); no EVM compatibility
Cosmos / CosmWasm CosmWasm (WASM VM) Rust ~10,000 per chain IBC cross-chain messaging; sovereign app-chains; actor model with explicit message passing
Polkadot Substrate / ink! Rust (ink! DSL) ~1,000+ per parachain Shared security from relay chain; parachain customisation; WASM-based
Sui / Aptos Move VM Move ~100,000+ claimed Object-centric model; linear types prevent reentrancy and double-spend by construction

The Move language insight

Move was designed at Meta (for the Diem blockchain) specifically to model financial assets safely. Its central innovation is linear types (called resources): a value of resource type can be moved from one owner to another, but it cannot be copied or dropped (silently destroyed). The type system enforces this at compile time.

MOVE'S GUARANTEE

In Solidity, a "token balance" is just a uint256 in a mapping — you could accidentally create tokens by writing to the wrong slot. In Move, a Coin<T> is a resource. You can't create one without calling the module that mints them. You can't destroy one without calling the burn function. The compiler enforces conservation of value as a type-level property, eliminating entire classes of exploits — including reentrancy — by construction.

9. Interactive: EVM stack machine visualizer

The program below runs on a simplified EVM. Press Step to execute the next opcode and watch the stack update in real time. Gas is tracked per instruction.

EVM Stack Machine — PUSH1 3, PUSH1 5, ADD, PUSH1 2, MUL, STOP
Press Step to begin execution.

10. Source code

Source — Smart Contracts & EVM
// ── ERC-20 transfer logic ────────────────────────────────────────
function transfer(from, to, amount):
    require from != ZERO_ADDRESS
    require to   != ZERO_ADDRESS
    require balances[from] >= amount
    balances[from] -= amount           // effect before any interaction
    balances[to]   += amount
    emit Transfer(from, to, amount)
    return true

// ── Reentrancy guard ─────────────────────────────────────────────
status = NOT_ENTERED  (= 1)

modifier nonReentrant:
    require status == NOT_ENTERED
    status = ENTERED  (= 2)
    run protected function body
    status = NOT_ENTERED

// ── Minimal EVM interpreter ──────────────────────────────────────
state:
    stack:   []
    memory:  bytes
    storage: map(uint256 -> uint256)
    pc:      int = 0
    gas:     int = initial_gas

function step(bytecode):
    opcode = bytecode[pc]; pc += 1
    match opcode:
        PUSH1:
            val = bytecode[pc]; pc += 1
            stack.push(val); gas -= 3
        ADD:
            b = stack.pop(); a = stack.pop()
            stack.push((a + b) mod 2^256); gas -= 3
        MUL:
            b = stack.pop(); a = stack.pop()
            stack.push((a * b) mod 2^256); gas -= 5
        SSTORE:
            key = stack.pop(); val = stack.pop()
            storage[key] = val
            gas -= 20000 if slot was zero else 5000
        STOP:
            execution halts
        REVERT:
            execution halts, all state changes rolled back
"""
Smart contract interaction with Python web3.py
Requires: pip install web3
"""
from web3 import Web3
from web3.middleware import geth_poa_middleware
import json

# ── Connect ──────────────────────────────────────────────────────
w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_KEY"))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
assert w3.is_connected()

# ── ERC-20 ABI (minimal) ─────────────────────────────────────────
ERC20_ABI = json.loads('''[
  {"name":"balanceOf","type":"function","stateMutability":"view",
   "inputs":[{"name":"account","type":"address"}],
   "outputs":[{"name":"","type":"uint256"}]},
  {"name":"transfer","type":"function","stateMutability":"nonpayable",
   "inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],
   "outputs":[{"name":"","type":"bool"}]},
  {"name":"Transfer","type":"event",
   "inputs":[{"indexed":true,"name":"from","type":"address"},
             {"indexed":true,"name":"to","type":"address"},
             {"indexed":false,"name":"value","type":"uint256"}]}
]''')

USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
contract = w3.eth.contract(address=USDC_ADDRESS, abi=ERC20_ABI)

# ── Read (free — no gas) ─────────────────────────────────────────
MY_WALLET = "0xYOUR_WALLET_ADDRESS"
balance_raw = contract.functions.balanceOf(MY_WALLET).call()
# USDC has 6 decimals
balance_usdc = balance_raw / 10**6
print(f"USDC balance: {balance_usdc:.2f}")

# ── Write (requires signing and broadcasting) ────────────────────
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"
account     = w3.eth.account.from_key(PRIVATE_KEY)
recipient   = "0xRECIPIENT_ADDRESS"
amount_usdc = int(1.5 * 10**6)   # 1.5 USDC

tx = contract.functions.transfer(recipient, amount_usdc).build_transaction({
    "from":     account.address,
    "nonce":    w3.eth.get_transaction_count(account.address),
    "gas":      100_000,
    "maxFeePerGas":         w3.to_wei("20", "gwei"),
    "maxPriorityFeePerGas": w3.to_wei("1",  "gwei"),
    "chainId":  1,
})

signed = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f"tx: {tx_hash.hex()}  status: {receipt['status']}")

# ── Decode past Transfer events ──────────────────────────────────
from_block = w3.eth.block_number - 500
logs = contract.events.Transfer().get_logs(fromBlock=from_block)
for log in logs[:5]:
    args = log["args"]
    print(f"  {args['from'][:8]}… → {args['to'][:8]}… : {args['value'] / 1e6:.2f} USDC")
/**
 * Smart contract interaction with ethers.js v6
 * npm install ethers hardhat
 */
import { ethers } from "ethers";

// ── Connect ───────────────────────────────────────────────────────
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/KEY");
const signer   = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider);

// ── ERC-20 minimal ABI ────────────────────────────────────────────
const ERC20_ABI = [
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address to, uint256 amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
];

const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const token = new ethers.Contract(USDC, ERC20_ABI, signer);

// ── Read ──────────────────────────────────────────────────────────
const raw = await token.balanceOf(signer.address);
console.log("Balance:", ethers.formatUnits(raw, 6), "USDC");

// ── Write (send a transaction) ────────────────────────────────────
const tx = await token.transfer("0xRECIPIENT", ethers.parseUnits("1.5", 6));
console.log("tx hash:", tx.hash);
const receipt = await tx.wait();
console.log("confirmed in block", receipt.blockNumber);

// ── Event listener ────────────────────────────────────────────────
token.on("Transfer", (from, to, value, event) => {
  console.log(`Transfer: ${from} -> ${to} : ${ethers.formatUnits(value, 6)} USDC`);
  console.log("block:", event.log.blockNumber);
});

// ── Hardhat test example ─────────────────────────────────────────
// test/ERC20.test.js
import { expect } from "chai";
import hre from "hardhat";

describe("ERC20", () => {
  let token, owner, alice;

  beforeEach(async () => {
    [owner, alice] = await hre.ethers.getSigners();
    const Factory = await hre.ethers.getContractFactory("ERC20");
    token = await Factory.deploy("MyToken", "MTK", 1_000_000n);
    await token.waitForDeployment();
  });

  it("mints initial supply to owner", async () => {
    const bal = await token.balanceOf(owner.address);
    expect(bal).to.equal(ethers.parseUnits("1000000", 18));
  });

  it("transfers tokens", async () => {
    const amount = ethers.parseUnits("100", 18);
    await token.transfer(alice.address, amount);
    expect(await token.balanceOf(alice.address)).to.equal(amount);
  });

  it("reverts on insufficient balance", async () => {
    const tooMuch = ethers.parseUnits("2000000", 18);
    await expect(token.transfer(alice.address, tooMuch))
      .to.be.revertedWith("ERC20: transfer amount exceeds balance");
  });
});
/*
 * ABI-encode an ERC-20 transfer call in C
 * (keccak256 via libssl / OpenSSL EVP)
 * gcc -o abi_encode abi_encode.c -lssl -lcrypto
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <openssl/evp.h>

/* keccak256 wrapper using EVP_MD_CTX */
static int keccak256(const uint8_t *in, size_t inlen, uint8_t out[32]) {
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    const EVP_MD *md = EVP_MD_fetch(NULL, "KECCAK-256", NULL);
    if (!ctx || !md) return -1;
    EVP_DigestInit_ex(ctx, md, NULL);
    EVP_DigestUpdate(ctx, in, inlen);
    unsigned int outlen = 32;
    EVP_DigestFinal_ex(ctx, out, &outlen);
    EVP_MD_CTX_free(ctx);
    EVP_MD_free((EVP_MD*)md);
    return 0;
}

/* ABI-encode transfer(address,uint256)
 * Layout: [4-byte selector][32-byte address][32-byte uint256]
 * Total:  68 bytes
 */
void abi_encode_transfer(
    const uint8_t addr[20],          /* 20-byte Ethereum address */
    const uint8_t amount[32],        /* 32-byte big-endian uint256 */
    uint8_t calldata_out[68])
{
    /* 1. Compute selector: keccak256("transfer(address,uint256)")[0:4] */
    const char *sig = "transfer(address,uint256)";
    uint8_t hash[32];
    keccak256((const uint8_t*)sig, strlen(sig), hash);

    memcpy(calldata_out, hash, 4);   /* selector */

    /* 2. Encode address: left-pad 20 bytes to 32 bytes */
    memset(calldata_out + 4, 0, 12);
    memcpy(calldata_out + 4 + 12, addr, 20);

    /* 3. Encode uint256: already 32 bytes (caller responsible for big-endian) */
    memcpy(calldata_out + 36, amount, 32);
}

int main(void) {
    uint8_t addr[20] = {
        0xAB,0xCD,0xEF,0x00,0x11,0x22,0x33,0x44,0x55,0x66,
        0x77,0x88,0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x12
    };
    /* 1 ether = 10^18 = 0x0de0b6b3a7640000 */
    uint8_t amount[32] = {0};
    amount[24] = 0x0d; amount[25] = 0xe0; amount[26] = 0xb6;
    amount[27] = 0xb3; amount[28] = 0xa7; amount[29] = 0x64;
    amount[30] = 0x00; amount[31] = 0x00;

    uint8_t calldata[68];
    abi_encode_transfer(addr, amount, calldata);

    printf("Calldata (%d bytes):\n  ", 68);
    for (int i = 0; i < 68; i++) {
        printf("%02x", calldata[i]);
        if ((i+1) % 32 == 0) printf("\n  ");
    }
    printf("\n");
    return 0;
}
/*
 * Minimal EVM stack machine in C++
 * Demonstrates PUSH1, ADD, MUL, STOP
 */
#include <cstdint>
#include <vector>
#include <stdexcept>
#include <iostream>
#include <format>

using Word = uint64_t;  // simplified: real EVM uses 256-bit integers

enum Opcode : uint8_t {
    STOP  = 0x00,
    ADD   = 0x01,
    MUL   = 0x02,
    PUSH1 = 0x60,
};

struct EVMResult {
    std::vector<Word> stack;
    uint64_t gasUsed;
    bool     stopped;
};

EVMResult runEVM(const std::vector<uint8_t>& bytecode, uint64_t gasLimit) {
    std::vector<Word> stack;
    uint64_t gasUsed = 0;
    size_t pc = 0;

    auto consumeGas = [&](uint64_t cost) {
        if (gasUsed + cost > gasLimit)
            throw std::runtime_error("Out of gas");
        gasUsed += cost;
    };

    while (pc < bytecode.size()) {
        uint8_t op = bytecode[pc++];
        switch (op) {
            case PUSH1: {
                consumeGas(3);
                if (pc >= bytecode.size()) throw std::runtime_error("PUSH1: missing operand");
                stack.push_back(bytecode[pc++]);
                break;
            }
            case ADD: {
                consumeGas(3);
                if (stack.size() < 2) throw std::runtime_error("ADD: stack underflow");
                Word b = stack.back(); stack.pop_back();
                Word a = stack.back(); stack.pop_back();
                stack.push_back(a + b);
                break;
            }
            case MUL: {
                consumeGas(5);
                if (stack.size() < 2) throw std::runtime_error("MUL: stack underflow");
                Word b = stack.back(); stack.pop_back();
                Word a = stack.back(); stack.pop_back();
                stack.push_back(a * b);
                break;
            }
            case STOP:
                consumeGas(0);
                return {stack, gasUsed, true};
            default:
                throw std::runtime_error(std::format("Unknown opcode: 0x{:02x}", op));
        }
    }
    return {stack, gasUsed, false};
}

int main() {
    // PUSH1 3, PUSH1 5, ADD, PUSH1 2, MUL, STOP
    // Expected: (3 + 5) * 2 = 16
    std::vector<uint8_t> program = {
        0x60, 0x03,   // PUSH1 3
        0x60, 0x05,   // PUSH1 5
        0x01,         // ADD   → stack: [8]
        0x60, 0x02,   // PUSH1 2
        0x02,         // MUL   → stack: [16]
        0x00          // STOP
    };

    auto result = runEVM(program, 100'000);
    std::cout << "Stack top: " << result.stack.back()
              << "  gas used: " << result.gasUsed << "\n";
    return 0;
}
/**
 * Smart contract interaction with web3j
 * Maven: org.web3j:core:4.10.3
 */
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.http.HttpService;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.*;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.model.ERC20;   // generated wrapper

import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;

public class SmartContractDemo {

    static final String NODE_URL    = "https://mainnet.infura.io/v3/KEY";
    static final String PRIVATE_KEY = "0xYOUR_KEY";
    static final String USDC_ADDR   = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

    public static void main(String[] args) throws Exception {
        Web3j web3j   = Web3j.build(new HttpService(NODE_URL));
        Credentials creds = Credentials.create(PRIVATE_KEY);

        // ── Using a generated wrapper (web3j generate ...) ──────────
        // ERC20 token = ERC20.load(USDC_ADDR, web3j, creds, new DefaultGasProvider());
        // BigInteger balance = token.balanceOf(creds.getAddress()).send();
        // System.out.println("Balance: " + balance);

        // ── Manual ABI call (no generated wrapper needed) ────────────
        Function balanceOf = new Function(
            "balanceOf",
            Arrays.asList(new Address(creds.getAddress())),
            Arrays.asList(new TypeReference<Uint256>() {})
        );

        String encoded = FunctionEncoder.encode(balanceOf);
        EthCall response = web3j.ethCall(
            org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(
                creds.getAddress(), USDC_ADDR, encoded
            ),
            org.web3j.protocol.core.DefaultBlockParameterName.LATEST
        ).send();

        List<Type> result = FunctionReturnDecoder.decode(
            response.getValue(), balanceOf.getOutputParameters()
        );
        BigInteger rawBalance = ((Uint256) result.get(0)).getValue();
        System.out.printf("USDC balance: %.2f%n", rawBalance.doubleValue() / 1e6);

        // ── Send a transfer transaction ───────────────────────────────
        Function transfer = new Function(
            "transfer",
            Arrays.asList(
                new Address("0xRECIPIENT"),
                new Uint256(BigInteger.valueOf(1_500_000L))  // 1.5 USDC (6 decimals)
            ),
            Arrays.asList(new TypeReference<Bool>() {})
        );

        String txData = FunctionEncoder.encode(transfer);
        BigInteger nonce = web3j.ethGetTransactionCount(
            creds.getAddress(),
            org.web3j.protocol.core.DefaultBlockParameterName.LATEST
        ).send().getTransactionCount();

        org.web3j.protocol.core.methods.request.Transaction tx =
            org.web3j.protocol.core.methods.request.Transaction.createFunctionCallTransaction(
                creds.getAddress(), nonce,
                org.web3j.tx.gas.DefaultGasProvider.GAS_PRICE,
                org.web3j.tx.gas.DefaultGasProvider.GAS_LIMIT,
                USDC_ADDR, txData
            );

        EthSendTransaction ethSendTx = web3j.ethSendTransaction(tx).send();
        System.out.println("tx hash: " + ethSendTx.getTransactionHash());

        web3j.shutdown();
    }
}
// Smart contract interaction with go-ethereum (geth)
// go get github.com/ethereum/go-ethereum
package main

import (
    "context"
    "fmt"
    "log"
    "math/big"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/ethclient"
    "strings"
)

const erc20ABI = `[
  {"name":"balanceOf","type":"function","stateMutability":"view",
   "inputs":[{"name":"account","type":"address"}],
   "outputs":[{"name":"","type":"uint256"}]},
  {"name":"transfer","type":"function","stateMutability":"nonpayable",
   "inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],
   "outputs":[{"name":"","type":"bool"}]}
]`

func main() {
    client, err := ethclient.Dial("https://mainnet.infura.io/v3/KEY")
    if err != nil { log.Fatal(err) }
    defer client.Close()

    parsedABI, err := abi.JSON(strings.NewReader(erc20ABI))
    if err != nil { log.Fatal(err) }

    usdcAddr    := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
    myAddr      := common.HexToAddress("0xYOUR_ADDRESS")

    // ── Pack a balanceOf call ────────────────────────────────────
    callData, err := parsedABI.Pack("balanceOf", myAddr)
    if err != nil { log.Fatal(err) }

    msg := ethereum.CallMsg{To: &usdcAddr, Data: callData}
    rawResult, err := client.CallContract(context.Background(), msg, nil)
    if err != nil { log.Fatal(err) }

    // ── Unpack the result ────────────────────────────────────────
    results, err := parsedABI.Unpack("balanceOf", rawResult)
    if err != nil { log.Fatal(err) }

    balance := results[0].(*big.Int)
    fmt.Printf("USDC balance: %.2f\n", float64(balance.Int64())/1e6)

    // ── Send a transfer transaction ──────────────────────────────
    privateKey, err := crypto.HexToECDSA("YOUR_PRIVATE_KEY_NO_0x")
    if err != nil { log.Fatal(err) }

    recipient := common.HexToAddress("0xRECIPIENT")
    amount    := big.NewInt(1_500_000)  // 1.5 USDC

    txData, err := parsedABI.Pack("transfer", recipient, amount)
    if err != nil { log.Fatal(err) }

    nonce, _    := client.PendingNonceAt(context.Background(), myAddr)
    gasPrice, _ := client.SuggestGasPrice(context.Background())
    chainID, _  := client.ChainID(context.Background())

    tx := types.NewTransaction(nonce, usdcAddr, big.NewInt(0), 100_000, gasPrice, txData)
    signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    if err != nil { log.Fatal(err) }

    if err := client.SendTransaction(context.Background(), signedTx); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("tx sent: %s\n", signedTx.Hash().Hex())
}

11. Summary

ONE-PARAGRAPH SUMMARY

A smart contract is a deterministic program stored on a blockchain that executes exactly as written without a trusted intermediary. Ethereum's EVM is a 256-bit stack machine where every opcode costs gas, making infinite loops economically impossible. Solidity compiles to EVM bytecode and provides a high-level type system including mappings, structs, events, and modifiers. The ERC-20 standard defines the interface all fungible tokens implement. Common security patterns — the checks-effects-interactions ordering, reentrancy guards, Ownable access control, EIP-712 signed messages — guard against the most costly exploit classes, from the $60M DAO reentrancy hack to oracle manipulation and front-running. Gas optimisation (packing storage, caching reads, using calldata, unchecked arithmetic) reduces fees from prohibitive to acceptable. Beyond Ethereum, Solana, Cosmos, and Move-based chains (Sui, Aptos) offer alternative execution environments, with Move's linear type system providing the most fundamental safety guarantee: assets cannot be copied or silently destroyed.

Where to go next