Skip to content

✍️ 필사 모드: Smart Contract Development Complete Guide 2025: Solidity, Foundry, Security Patterns, DeFi Implementation

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Why Smart Contracts? The Programmable Money Revolution

The 2016 DAO hack etched the importance of smart contract security into collective memory. A few lines of flawed code led to the theft of 3.6 million ETH (about 50M USD at the time, roughly 12B USD at today's prices). That event forced the Ethereum hard fork that created Ethereum Classic and fundamentally changed how we build on-chain systems.

By 2026, smart contracts are no longer experimental. DeFi TVL (Total Value Locked) has crossed 200 billion USD. Uniswap, Aave, Compound, and MakerDAO serve millions of users. NFTs have expanded into art, gaming, and identity. Real-world asset tokenization is accelerating.

Smart contracts offer three core properties.

First, programmable money. Value moves according to rules encoded in code. "If A pays 100, send token to B" executes without escrow agents or trusted intermediaries.

Second, immutability. Deployed code cannot be changed (proxies exist but are a deliberate design choice). Users know the code will not silently rewrite itself.

Third, transparency. Every line of code and every transaction is recorded on a public chain. Etherscan lets anyone read source and trace history.

These properties cut both ways. Immutability means bug fixes are hard. Transparency means attackers can study your attack surface. Programmable money means a single mistake can vaporize millions in minutes.

Smart contract engineering therefore requires a different mindset. Not "it works" but "it defends against every adversary I can imagine." This guide walks through that mindset end-to-end.

EVM Fundamentals

What the EVM Actually Is

The Ethereum Virtual Machine (EVM) is the state machine where smart contracts run. Every Ethereum node runs the same EVM and must arrive at the same result for every transaction. That consensus is the foundation of decentralization.

Key EVM traits:

  • Stack-based: up to 1024 256-bit stack slots. No registers.
  • 256-bit word: 32 bytes is the natural unit, matching keccak256 output and explaining why uint256 is everywhere.
  • Deterministic: same input always yields the same output. No randomness, no clocks, no network calls.

Data Locations: Storage, Memory, Calldata, Stack

LocationLifetimeGasPurpose
StoragePermanentVery expensive (SSTORE 20k)Contract state
MemoryTransactionCheap (linear)Function-local data
CalldataTransactionCheapest (read-only)External function args
StackExecutionFree (1024 cap)Locals, intermediates

Storage is permanent and expensive. First write costs 20,000 gas, subsequent writes 5,000. Memory is cheap but function-scoped. Calldata is the raw transaction input, cheaper than memory because no copy happens.

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

contract DataLocationDemo {
    uint256[] private _storedArray;

    function processMemory(uint256[] memory data) public pure returns (uint256) {
        data[0] = 999;
        return data[0];
    }

    function processCalldata(uint256[] calldata data) external pure returns (uint256) {
        return data[0];
    }

    function storeArray(uint256[] calldata data) external {
        _storedArray = data;
    }
}

Rule: prefer calldata for external function array/struct arguments. Copy to memory only when mutation is required.

The Gas Model

Every EVM opcode has a gas cost. Two reasons:

  • DoS prevention. Bounded execution protects the network from runaway loops.
  • Resource pricing. Operators are compensated for compute and storage.

Selected costs (London-era):

  • ADD, SUB: 3 gas
  • MUL, DIV: 5 gas
  • SLOAD (cold): 2,100; (warm): 100
  • SSTORE: 20,000 / 5,000 / refunds on zeroing
  • CALL (cold): 2,600 plus extras
  • CREATE: 32,000

You do not need to memorize the table, but internalize this: storage work is hundreds of times more expensive than arithmetic. Most optimization is about reducing storage touches.

Solidity Language Essentials

Types

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

contract TypeShowcase {
    uint256 public unsignedInt;
    int256 public signedInt;
    uint8 public smallInt;

    bool public flag;

    address public owner;
    address payable public recipient;

    bytes32 public hash;
    bytes4 public selector;

    bytes public dynamicBytes;
    string public name;

    uint256[] public dynamicArray;
    uint256[10] public fixedArray;

    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    struct User {
        uint256 id;
        string name;
        uint256 balance;
        bool active;
    }

    mapping(address => User) public users;

    enum Status { Pending, Active, Completed, Cancelled }
    Status public currentStatus;
}

Points to remember:

  • uint256 is the default. Smaller integers only save gas when packed in the same storage slot.
  • address is 20 bytes; use bytes32 for hashes.
  • string and bytes are dynamic and expensive. Prefer bytes32 when size permits.
  • Every mapping slot is conceptually zero-initialized.

Functions, Visibility, Mutability

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

contract FunctionDemo {
    uint256 private _counter;
    uint256 public publicValue;

    function incrementExternal() external {
        _counter += 1;
    }

    function incrementPublic() public {
        _counter += 1;
    }

    function _internalHelper() internal view returns (uint256) {
        return _counter * 2;
    }

    function _privateHelper() private pure returns (uint256) {
        return 42;
    }

    function getCounter() external view returns (uint256) {
        return _counter;
    }

    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    function deposit() external payable {}
}

Events

Events are log entries on chain. They are far cheaper than storage and can be streamed by off-chain indexers.

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

contract EventDemo {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => uint256) private _balances;

    function transfer(address to, uint256 amount) external {
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        _balances[msg.sender] -= amount;
        _balances[to] += amount;

        emit Transfer(msg.sender, to, amount);
    }
}

Frontends and tools like The Graph rely on events to reconstruct history without paying storage costs.

Modifiers

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

contract ModifierDemo {
    address public owner;
    bool public paused;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier whenNotPaused() {
        require(!paused, "Contract paused");
        _;
    }

    function pause() external onlyOwner whenNotPaused {
        paused = true;
    }
}

Interfaces and Inheritance

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

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

abstract contract Ownable {
    address private _owner;

    modifier onlyOwner() {
        require(msg.sender == _owner, "Not owner");
        _;
    }
}

Foundry Deep Dive

Why Foundry

Foundry is a Rust-based Solidity toolchain from Paradigm. It is 10-100x faster than Hardhat, lets you write tests in Solidity, and ships a fuzzer that finds edge cases for free. In 2026, more than 70% of new projects pick Foundry.

Setup

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

forge init my-project
cd my-project

Directory layout:

  • src/ - contracts
  • test/ - Solidity tests
  • script/ - deployment scripts
  • lib/ - dependencies
  • foundry.toml - config

foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
optimizer = true
optimizer_runs = 200

[profile.ci]
fuzz = { runs = 10000 }
invariant = { runs = 1000 }

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"

Writing Tests

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

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
    Counter public counter;
    address alice = address(0x1);

    function setUp() public {
        counter = new Counter();
        vm.deal(alice, 10 ether);
    }

    function test_Increment() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    function test_OnlyOwner() public {
        vm.prank(alice);
        vm.expectRevert("Not owner");
        counter.setNumber(100);
    }
}

Run them:

forge test
forge test --match-test test_Increment
forge test -vvv
forge test --gas-report

Fuzz Testing

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

import {Test} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterFuzzTest is Test {
    Counter public counter;

    function setUp() public {
        counter = new Counter();
    }

    function testFuzz_SetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
    }

    function testFuzz_IncrementBounded(uint8 times) public {
        vm.assume(times > 0 && times < 100);

        for (uint256 i = 0; i < times; i++) {
            counter.increment();
        }
        assertEq(counter.number(), times);
    }
}

Foundry runs 256 iterations by default, minimizes failing inputs automatically, and can scale to tens of thousands of runs in CI.

Invariant Testing

Invariant tests assert properties that must hold after any sequence of calls.

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

import {Test} from "forge-std/Test.sol";
import {MyToken} from "../src/MyToken.sol";

contract TokenInvariantTest is Test {
    MyToken public token;

    function setUp() public {
        token = new MyToken("Test", "TST", 1_000_000 ether);
    }

    function invariant_TotalSupplyConstant() public view {
        assertEq(token.totalSupply(), 1_000_000 ether);
    }
}

cast and anvil

cast call 0xContract "balanceOf(address)(uint256)" 0xUser
cast send 0xContract "transfer(address,uint256)" 0xTo 100 --private-key $PK --rpc-url $RPC
cast to-wei 1 ether

anvil
anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY

Mainnet forking lets you test against live Uniswap, Aave, and Chainlink feeds without paying real gas.

Foundry vs Hardhat

FeatureFoundryHardhat
Test speedVery fast (Rust)Slower (Node.js)
Test languageSolidityTypeScript/JS
Fuzz testingBuilt-inPlugin
Deployment scriptsSolidityTypeScript
Debugging-vvv tracesStack traces
Plugin ecosystemMediumLarge
Frontend integrationcast CLIethers.js/viem

Recommendation: new projects should pick Foundry. Use Hardhat if tight TypeScript frontend coupling matters. Large teams often use both.

OpenZeppelin Building Blocks

ERC-20 Token

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

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

contract MyToken is ERC20, Ownable {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) Ownable(msg.sender) {
        _mint(msg.sender, initialSupply);
    }

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

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}

ERC-721 NFT

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

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor() ERC721("MyNFT", "MNFT") Ownable(msg.sender) {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage) returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC721URIStorage) returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

AccessControl

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

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract RoleBasedToken is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    constructor() ERC20("RBT", "RBT") {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
    }

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

ReentrancyGuard

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

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

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");
        balances[msg.sender] -= amount;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

Security Patterns and Vulnerabilities

1. Reentrancy

// Vulnerable
contract VulnerableVault {
    mapping(address => uint256) public balances;

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

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);

        balances[msg.sender] = 0;
    }
}

// Safe - Checks-Effects-Interactions
contract SafeVault {
    mapping(address => uint256) public balances;

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

        balances[msg.sender] = 0;

        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

The DAO (2016), Fei Rari (2022, 80M USD), and Euler Finance (2023, 197M USD) all failed to follow this pattern.

2. Integer Overflow

Since Solidity 0.8.0 overflow reverts by default. Inside unchecked blocks it still wraps - use only when mathematically safe.

pragma solidity ^0.8.24;

contract OverflowDemo {
    function safeAdd(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    function uncheckedAdd(uint256 a, uint256 b) external pure returns (uint256) {
        unchecked {
            return a + b;
        }
    }
}

3. Front-Running / MEV

Public mempools let adversaries reorder or sandwich trades. Defenses: commit-reveal, Flashbots Protect, slippage limits, protocol-level hooks (Uniswap V4).

4. Oracle Manipulation

Use Chainlink, TWAPs, or multiple sources. Flash loans can move single-DEX spot prices enough to drain protocols that trust raw quotes.

5. Unchecked External Calls

function unsafeTransfer(address to, uint256 amount) external {
    payable(to).call{value: amount}("");
}

function safeTransfer(address to, uint256 amount) external {
    (bool success, ) = payable(to).call{value: amount}("");
    require(success, "Transfer failed");
}

Gas Optimization Techniques

1. Storage Packing

contract Unpacked {
    uint256 a;
    uint256 b;
    uint256 c;
}

contract Packed {
    uint128 a;
    uint64 b;
    uint64 c;
}

2. immutable and constant

contract ConstantsDemo {
    uint256 public constant MAX_SUPPLY = 1_000_000;
    address public immutable owner;

    constructor() {
        owner = msg.sender;
    }
}

3. Custom Errors

contract ErrorDemo {
    error ValueMustBePositive();
    error InsufficientBalance(uint256 available, uint256 required);

    function newWay(uint256 x) external pure {
        if (x == 0) revert ValueMustBePositive();
    }

    function withdraw(uint256 amount, uint256 balance) external pure {
        if (balance < amount) {
            revert InsufficientBalance(balance, amount);
        }
    }
}

4. calldata over memory and events over storage

Prefer calldata for external function parameters, and lean on events when data only needs to be reconstructable off-chain.

Upgradeable Contracts

UUPS Pattern

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

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

contract MyTokenV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public value;

    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) external {
        value = _value;
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}
}

Upgradeable contracts cannot use constructors; they use initialize() once. Storage layout changes corrupt data. OpenZeppelin Upgrades verifies layouts automatically.

Simple AMM Implementation

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleAMM {
    IERC20 public immutable tokenA;
    IERC20 public immutable tokenB;

    uint256 public reserveA;
    uint256 public reserveB;

    constructor(address _tokenA, address _tokenB) {
        tokenA = IERC20(_tokenA);
        tokenB = IERC20(_tokenB);
    }

    function addLiquidity(uint256 amountA, uint256 amountB) external {
        tokenA.transferFrom(msg.sender, address(this), amountA);
        tokenB.transferFrom(msg.sender, address(this), amountB);

        reserveA += amountA;
        reserveB += amountB;
    }

    function swap(uint256 amountAIn) external returns (uint256 amountBOut) {
        require(amountAIn > 0, "Invalid input");

        uint256 amountAInWithFee = (amountAIn * 997) / 1000;
        amountBOut = (reserveB * amountAInWithFee) / (reserveA + amountAInWithFee);

        tokenA.transferFrom(msg.sender, address(this), amountAIn);
        tokenB.transfer(msg.sender, amountBOut);

        reserveA += amountAIn;
        reserveB -= amountBOut;
    }
}

Real Uniswap is more complex (LP tokens, fee accrual, flash swap protection), but the invariant x * y = k is the heart of it.

Audit Tools

  • Slither: static analysis, dozens of known patterns. slither src/MyContract.sol.
  • Mythril: symbolic execution. myth analyze src/MyContract.sol.
  • Echidna: fuzz tester beyond Foundry's built-in.

Deployment Workflow

  1. Local development with anvil and forge test
  2. Testnet deployment (Sepolia, Holesky)
  3. External audit (Trail of Bits, OpenZeppelin, Consensys Diligence)
  4. Bug bounty program (Immunefi)
  5. Mainnet rollout with TVL caps
  6. Monitoring (Forta, Tenderly)
forge script script/Deploy.s.sol \
  --rpc-url sepolia \
  --broadcast \
  --verify

forge create src/MyToken.sol:MyToken \
  --rpc-url $RPC \
  --private-key $PK \
  --constructor-args "Name" "SYMBOL" 1000000

Etherscan verification is non-negotiable.

Production Checklist

  • Solidity 0.8.x or newer
  • Checks-Effects-Interactions everywhere
  • nonReentrant modifier on value-moving functions
  • unchecked only with proven safety
  • Check return value of call
  • Index searchable event fields
  • Custom errors instead of require strings
  • Chainlink TWAP oracles
  • Fuzz tests on every function
  • Invariant tests on global properties
  • Slither clean
  • External audit
  • Bug bounty
  • Multisig admin (Gnosis Safe)
  • Timelock on upgrades

Quiz

Q1. Why is Checks-Effects-Interactions important?

If state is updated after an external call, the callee can re-enter the function with stale state and drain funds. Updating state first eliminates the window of vulnerability.

Q2. What is the difference between calldata and memory?

calldata points to immutable transaction input, is the cheapest, and is read-only. memory is a mutable per-call region but copying into it costs gas. Prefer calldata for external function array/struct parameters unless mutation is required.

Q3. Why use unchecked in Solidity 0.8+?

Purely for gas savings. 0.8 inserts overflow checks on every arithmetic op. When overflow is mathematically impossible (e.g. loop counters bounded by length), unchecked removes those checks. Only use it when safety is provable.

Q4. What are the benefits of UUPS proxies?

UUPS keeps upgrade logic in the implementation, so the proxy is lighter and cheaper per call. The trade-off: if a new implementation forgets to expose an upgrade function, the contract becomes permanently frozen.

Q5. Fuzz testing vs invariant testing?

Fuzz tests drive a single function with randomized inputs. Invariant tests drive a random sequence of calls across the whole contract and assert global properties. Invariants are dramatically more powerful for DeFi where state interacts across many actions.

References

  1. Solidity Documentation
  2. Foundry Book
  3. OpenZeppelin Contracts
  4. Ethereum.org - Smart Contracts
  5. Consensys Smart Contract Best Practices
  6. SWC Registry
  7. Trail of Bits - Building Secure Contracts
  8. Slither
  9. Echidna
  10. Immunefi
  11. The DAO Hack Post-Mortem
  12. Uniswap V2 Core
  13. Paradigm Research
  14. Rekt.news

현재 단락 (1/497)

The 2016 DAO hack etched the importance of smart contract security into collective memory. A few lin...

작성 글자: 0원문 글자: 18,357작성 단락: 0/497