Engineering Notes

孟斌的小站

技术博客与学习记录

共 608 篇文章 标签与分类索引已启用

《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)

  • 时间:2017 年 7 月(第一次) & 2017 年 11 月(第二次)
  • 事件:Parity 多签钱包合约存在严重漏洞,被攻击者利用,最终导致 约 51 万 ETH(当时价值 3 亿美元) 被盗/冻结。
  • 影响:继 The DAO Hack 之后又一次震惊整个以太坊社区的安全事故。

1. 背景

Parity Wallet 是由 Parity Technologies(Gavin Wood 创立的公司,以太坊联合创始人)开发的钱包,支持 多签机制(Multisig Wallet),广泛被 ICO 项目和机构投资人使用。

继续阅读

《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)

The DAO Hack 简介

  • 时间:2016 年 6 月
  • 事件:一个基于以太坊的“去中心化投资基金”——The DAO,被黑客利用智能合约漏洞攻击,导致 360 万 ETH(当时约 5000 万美元)被盗。
  • 影响:直接导致以太坊社区分裂,产生了 ETH(Ethereum)与 ETC(Ethereum Classic) 两条链。

1. 背景

The DAO 是由 Slock.it 团队发起的一个智能合约,目标是让全球投资人通过 ETH 投资 DAO,然后社区投票决定投资哪些项目。

继续阅读

《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)

1、学习目标

  1. 理解 DAO 的核心理念:由代币持有人共同治理
  2. 学习实现 提案(Proposal)+ 投票(Voting)+ 执行(Execution) 流程
  3. 引入 治理代币(Governance Token),绑定投票权
  4. 学习 时间锁 Timelock,防止恶意提案被立即执行

2、DAO 合约设计要点

  • 治理代币:每个代币 = 1 票
  • 提案 Proposal:由用户提交,包含目标地址 + 执行数据
  • 投票 Voting:代币持有人按比例投票,投票期内可投
  • 执行 Execution:提案通过后,由合约调用目标合约
  • 时间锁 Timelock:执行需等待一段时间(例如 2 天)

3、示例合约 SimpleDAO.sol

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

/// @title SimpleDAO - 简化版 DAO 治理合约
/// @notice 教学演示用,不可用于生产

interface IERC20 {
    function balanceOf(address account) external view returns (uint);
}

contract SimpleDAO {
    IERC20 public governanceToken;
    uint public proposalCount;
    uint public constant VOTING_PERIOD = 3 days;   // 投票期
    uint public constant TIMELOCK_DELAY = 2 days;  // 执行延迟
    uint public constant QUORUM = 100e18;          // 最低投票总数(100 票)

    enum ProposalState { Active, Defeated, Succeeded, Queued, Executed }

    struct Proposal {
        address proposer;
        address target;
        bytes data;
        string description;
        uint voteFor;
        uint voteAgainst;
        uint startTime;
        uint endTime;
        uint eta; // Estimated Time for execution
        ProposalState state;
    }

    mapping(uint => Proposal) public proposals;
    mapping(uint => mapping(address => bool)) public hasVoted;

    event ProposalCreated(uint id, address proposer, string description);
    event Voted(uint id, address voter, bool support, uint weight);
    event ProposalQueued(uint id, uint eta);
    event ProposalExecuted(uint id);

    constructor(address _token) {
        governanceToken = IERC20(_token);
    }

    /// @notice 创建提案
    function propose(address target, bytes calldata data, string calldata description) external {
        proposalCount++;
        proposals[proposalCount] = Proposal({
            proposer: msg.sender,
            target: target,
            data: data,
            description: description,
            voteFor: 0,
            voteAgainst: 0,
            startTime: block.timestamp,
            endTime: block.timestamp + VOTING_PERIOD,
            eta: 0,
            state: ProposalState.Active
        });

        emit ProposalCreated(proposalCount, msg.sender, description);
    }

    /// @notice 投票
    function vote(uint proposalId, bool support) external {
        Proposal storage proposal = proposals[proposalId];
        require(block.timestamp >= proposal.startTime, "voting not started");
        require(block.timestamp <= proposal.endTime, "voting ended");
        require(!hasVoted[proposalId][msg.sender], "already voted");

        uint weight = governanceToken.balanceOf(msg.sender);
        require(weight > 0, "no voting power");

        if (support) {
            proposal.voteFor += weight;
        } else {
            proposal.voteAgainst += weight;
        }

        hasVoted[proposalId][msg.sender] = true;
        emit Voted(proposalId, msg.sender, support, weight);
    }

    /// @notice 投票结果检查,并进入 Timelock 队列
    function queue(uint proposalId) external {
        Proposal storage proposal = proposals[proposalId];
        require(block.timestamp > proposal.endTime, "voting not ended");
        require(proposal.state == ProposalState.Active, "not active");

        if (proposal.voteFor <= proposal.voteAgainst || proposal.voteFor < QUORUM) {
            proposal.state = ProposalState.Defeated;
        } else {
            proposal.state = ProposalState.Queued;
            proposal.eta = block.timestamp + TIMELOCK_DELAY;
            emit ProposalQueued(proposalId, proposal.eta);
        }
    }

    /// @notice 执行提案
    function execute(uint proposalId) external {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.state == ProposalState.Queued, "not queued");
        require(block.timestamp >= proposal.eta, "timelock not expired");

        (bool success, ) = proposal.target.call(proposal.data);
        require(success, "execution failed");

        proposal.state = ProposalState.Executed;
        emit ProposalExecuted(proposalId);
    }
}

4、测试文件 test/SimpleDAO.t.sol

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

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

/// @notice 简单的治理代币 (ERC20-like)
contract GovernanceToken is IERC20 {
    string public name = "GovToken";
    string public symbol = "GOV";
    uint8 public decimals = 18;
    uint public totalSupply;
    mapping(address => uint) public balanceOf;

    function mint(address to, uint amount) external {
        balanceOf[to] += amount;
        totalSupply += amount;
    }
}

/// @notice 被治理的目标合约(DAO 将控制它)
contract TargetContract {
    uint public value;

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

contract SimpleDAOTest is Test {
    GovernanceToken public gov;
    SimpleDAO public dao;
    TargetContract public target;

    address alice = address(0x123);
    address bob = address(0x234);

    function setUp() public {
        gov = new GovernanceToken();
        dao = new SimpleDAO(address(gov));
        target = new TargetContract();

        // 给 Alice 和 Bob 铸造治理代币
        gov.mint(alice, 100e18);
        gov.mint(bob, 50e18);
    }

    /// @notice 测试完整的提案生命周期
    function testProposalLifecycle() public {
        vm.startPrank(alice);

        // Alice 提出一个提案:调用 target.setValue(42)
        bytes memory data = abi.encodeWithSignature("setValue(uint256)", 42);
        dao.propose(address(target), data, "Set value to 42");

        vm.stopPrank();

        // Alice 投支持票
        vm.startPrank(alice);
        dao.vote(1, true);
        vm.stopPrank();

        // Bob 投反对票
        vm.startPrank(bob);
        dao.vote(1, false);
        vm.stopPrank();

        // 快进 3 天,投票结束
        vm.warp(block.timestamp + 3 days + 1);

        // 进入 Timelock 队列
        dao.queue(1);

        // 立即执行应失败(需要 timelock)
        vm.expectRevert();
        dao.execute(1);

        // 再快进 2 天
        vm.warp(block.timestamp + 2 days);

        // 执行提案
        dao.execute(1);

        // 验证目标合约的值已被修改
        assertEq(target.value(), 42);
    }
}

执行测试:

继续阅读

《纸上谈兵·solidity》第 26 课:借贷合约简化实现

1、学习目标

  1. 理解 借贷协议核心机制:存款、借款、还款、清算
  2. 掌握 抵押率(Collateral Factor) 的风险控制方法
  3. 学会实现一个最小版 Compound/Aave 借贷池

2、合约设计要点

  • 用户存入 ETH 作为抵押
  • 用户可以借出 ERC20 稳定币(如 DAI)
  • 设置 抵押率(Collateral Factor),保证抵押物 > 借款
  • 借款人债务随时间增长(利息按年化利率计算)
  • 当抵押不足时,可以被 清算(Liquidation)

3、合约实现 SimpleLending.sol

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

/// @title SimpleLending - 带利息的简化版借贷池
/// @notice 仅用于教学演示,不能用于生产环境
/// @dev 该合约实现了基本的借贷功能,包括抵押、借款、还款和清算
interface IERC20 {
    /// @notice 转账函数
    /// @param to 接收地址
    /// @param amount 转账金额
    /// @return 是否成功
    function transfer(address to, uint amount) external returns (bool);

    /// @notice 从指定地址转账
    /// @param from 转出地址
    /// @param to 接收地址
    /// @param amount 转账金额
    /// @return 是否成功
    function transferFrom(address from, address to, uint amount) external returns (bool);

    /// @notice 查询余额
    /// @param account 查询地址
    /// @return 余额
    function balanceOf(address account) external view returns (uint);

    /// @notice 铸造代币
    /// @param to 接收地址
    /// @param amount 铸造金额
    function mint(address to, uint amount) external;
}

contract SimpleLending {
    /// @notice 稳定币合约地址
    IERC20 public stablecoin; 

    /// @notice 抵押率,75%
    uint public constant COLLATERAL_FACTOR = 75;

    /// @notice 清算阈值,80%
    uint public constant LIQUIDATION_THRESHOLD = 80;

    /// @notice 年化利率,5% (0.05 * 1e18)
    uint public constant INTEREST_RATE_PER_YEAR = 5e16;

    /// @notice 一年的秒数
    uint public constant SECONDS_PER_YEAR = 365 days;

    /// @notice 账户信息结构体
    struct Account {
        uint collateralETH; // 抵押 ETH
        uint debt;          // 借款本金 + 利息
        uint lastAccrued;   // 上次计息时间
    }

    /// @notice 用户账户映射
    mapping(address => Account) public accounts;

    // 事件
    /// @notice 抵押事件
    event Deposit(address indexed user, uint amount);

    /// @notice 借款事件
    event Borrow(address indexed user, uint amount);

    /// @notice 还款事件
    event Repay(address indexed user, uint amount);

    /// @notice 清算事件
    event Liquidate(address indexed liquidator, address indexed user, uint repayAmount);

    /// @notice 计息事件
    event AccrueInterest(address indexed user, uint newDebt);

    /// @notice 构造函数
    /// @param stablecoinAddr 稳定币合约地址
    constructor(address stablecoinAddr) {
        stablecoin = IERC20(stablecoinAddr);
    }

    /// @notice 内部函数:计息
    /// @dev 根据时间计算利息并更新债务
    /// @param user 用户地址
    function _accrueInterest(address user) internal {
        Account storage account = accounts[user];
        if (account.debt == 0) {
            account.lastAccrued = block.timestamp;
            return;
        }

        uint elapsed = block.timestamp - account.lastAccrued;
        if (elapsed == 0) return;

        uint interest = (account.debt * INTEREST_RATE_PER_YEAR * elapsed) / (SECONDS_PER_YEAR * 1e18);
        account.debt += interest;
        account.lastAccrued = block.timestamp;

        emit AccrueInterest(user, account.debt);
    }

    /// @notice 存入 ETH 作为抵押
    /// @dev 用户可以通过此函数存入 ETH 作为抵押
    function depositCollateral() external payable {
        accounts[msg.sender].collateralETH += msg.value;
        if (accounts[msg.sender].lastAccrued == 0) {
            accounts[msg.sender].lastAccrued = block.timestamp;
        }
        emit Deposit(msg.sender, msg.value);
    }

    /// @notice 借款
    /// @dev 用户可以通过此函数借款
    /// @param amount 借款金额
    function borrow(uint amount) external {
        _accrueInterest(msg.sender);

        Account storage account = accounts[msg.sender];
        require(account.collateralETH > 0, "no collateral");

        uint maxBorrow = (account.collateralETH * COLLATERAL_FACTOR) / 100;
        require(account.debt + amount <= maxBorrow, "exceeds borrow limit");

        account.debt += amount;
        stablecoin.mint(msg.sender, amount);

        emit Borrow(msg.sender, amount);
    }

    /// @notice 还款
    /// @dev 用户可以通过此函数还款
    /// @param amount 还款金额
    function repay(uint amount) external {
        _accrueInterest(msg.sender);

        Account storage account = accounts[msg.sender];
        require(account.debt >= amount, "repay too much");

        require(stablecoin.transferFrom(msg.sender, address(this), amount), "transfer failed");
        account.debt -= amount;

        emit Repay(msg.sender, amount);
    }

    /// @notice 清算
    /// @dev 清算人可以通过此函数清算用户的抵押
    /// @param user 被清算的用户地址
    function liquidate(address user) external {
        _accrueInterest(user);

        Account storage account = accounts[user];
        require(account.debt > 0, "no debt");

        uint collateralValue = account.collateralETH;
        uint threshold = (collateralValue * LIQUIDATION_THRESHOLD) / 100;
        require(account.debt > threshold, "healthy position");

        uint repayAmount = account.debt;
        require(stablecoin.transferFrom(msg.sender, address(this), repayAmount), "transfer failed");

        account.debt = 0;
        uint seizedETH = account.collateralETH;
        account.collateralETH = 0;

        payable(msg.sender).transfer(seizedETH);

        emit Liquidate(msg.sender, user, repayAmount);
    }

    /// @notice 查询抵押率
    /// @dev 返回用户的抵押率
    /// @param user 用户地址
    /// @return 抵押率
    function getCollateralRatio(address user) external view returns (uint) {
        Account memory account = accounts[user];
        if (account.debt == 0) return type(uint).max;
        return (account.collateralETH * 100) / account.debt;
    }
}

4、测试文件 test/SimpleLending.t.sol

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

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

/// @title MockStablecoin - 模拟稳定币合约
/// @notice 用于测试 SimpleLending 合约的模拟稳定币
contract MockStablecoin {
    string public name = "Mock DAI";
    string public symbol = "mDAI";
    uint8 public decimals = 18;

    /// @notice 账户余额映射
    mapping(address => uint) public balanceOf;

    /// @notice 授权额度映射
    mapping(address => mapping(address => uint)) public allowance;

    /// @notice 转账事件
    event Transfer(address indexed from, address indexed to, uint value);

    /// @notice 授权事件
    event Approval(address indexed owner, address indexed spender, uint value);

    /// @notice 铸造代币
    /// @param to 接收地址
    /// @param amount 铸造金额
    function mint(address to, uint amount) external {
        balanceOf[to] += amount;
        emit Transfer(address(0), to, amount);
    }

    /// @notice 授权额度
    /// @param spender 授权地址
    /// @param amount 授权金额
    /// @return 是否成功
    function approve(address spender, uint amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    /// @notice 转账
    /// @param to 接收地址
    /// @param amount 转账金额
    /// @return 是否成功
    function transfer(address to, uint amount) external returns (bool) {
        require(balanceOf[msg.sender] >= amount, "balance too low");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    /// @notice 从授权地址转账
    /// @param from 转出地址
    /// @param to 接收地址
    /// @param amount 转账金额
    /// @return 是否成功
    function transferFrom(address from, address to, uint amount) external returns (bool) {
        require(balanceOf[from] >= amount, "balance too low");
        require(allowance[from][msg.sender] >= amount, "allowance too low");
        balanceOf[from] -= amount;
        allowance[from][msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}

/// @title SimpleLendingTest - SimpleLending 合约的测试
/// @notice 测试 SimpleLending 合约的功能
contract SimpleLendingTest is Test {
    /// @notice 模拟稳定币合约
    MockStablecoin dai;

    /// @notice SimpleLending 合约
    SimpleLending lending;

    /// @notice 测试用户 Alice
    address alice = address(0x123);

    /// @notice 测试用户 Bob
    address bob   = address(0x234);

    /// @notice 初始化测试环境
    function setUp() public {
        dai = new MockStablecoin();
        lending = new SimpleLending(address(dai));

        // 给 Bob 一些 mDAI 用于清算 + 授权
        dai.mint(bob, 1000 ether);
        vm.prank(bob);
        dai.approve(address(lending), type(uint).max);

        // 给 Alice/Bob 充值 ETH(Alice 要抵押 1 ETH)
        vm.deal(alice, 100 ether);
        vm.deal(bob, 100 ether);
    }

    /// @notice 测试抵押和借款功能
    function testDepositAndBorrow() public {
        vm.startPrank(alice);
        lending.depositCollateral{value: 1 ether}();
        lending.borrow(0.5 ether);
        uint ratio = lending.getCollateralRatio(alice);
        assertGt(ratio, 75);
        vm.stopPrank();
    }

    /// @notice 测试还款功能
    function testRepayDebt() public {
        vm.startPrank(alice);
        lending.depositCollateral{value: 1 ether}();
        lending.borrow(0.5 ether);
        dai.approve(address(lending), type(uint).max);
        lending.repay(0.5 ether);
        vm.stopPrank();

        ( , uint debtAfter, ) = lending.accounts(alice);
        assertEq(debtAfter, 0);
    }

    /// @notice 测试清算功能
    function testLiquidation() public {
        // 抵押 1 ETH,借到上限 0.75 ETH
        vm.startPrank(alice);
        lending.depositCollateral{value: 1 ether}();
        lending.borrow(0.75 ether);
        vm.stopPrank();

        // 利息 5%/年,2 年后:0.75 * (1 + 0.05*2) = 0.825 > 清算阈值 0.8
        vm.warp(block.timestamp + 2 * 365 days);

        // Bob 清算
        vm.prank(bob);
        lending.liquidate(alice);

        (uint collAfter, uint debtAfter, ) = lending.accounts(alice);
        assertEq(debtAfter, 0);
        assertEq(collAfter, 0);
    }

    /// @notice 测试利息计算功能
    function testInterestAccrual() public {
        vm.startPrank(alice);
        lending.depositCollateral{value: 1 ether}();
        lending.borrow(0.5 ether);
        vm.warp(block.timestamp + 365 days);
        // 触发一次计息(0 转账也会进 _accrueInterest)
        lending.repay(0);
        vm.stopPrank();

        (, uint debtWithInterest, ) = lending.accounts(alice);
        assertGt(debtWithInterest, 0.5 ether);
    }
}

执行测试:

继续阅读

《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)

1、学习目标

  1. 理解 恒定乘积公式 x * y = k 的原理
  2. 实现一个最小化的 DEX,支持 流动性提供 / 兑换 / 提取
  3. 引入 LP Token,模拟流动性凭证
  4. 探讨 AMM 的优缺点 & Gas 优化点

2、恒定乘积公式 (AMM)

  • 资金池:假设有 TokenATokenB,储备量分别为 xy
  • 公式x * y = k
  • 含义:只要有人兑换,必须保持乘积 k 不变
  • 结果:兑换时会自动形成滑点,越大的单笔兑换,价格偏移越大

3、简化版 DEX 合约

我们实现一个简化的 ETH ↔ ERC20 Token 交易对

继续阅读

《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战

1、本课学习目标

  • 理解去中心化众筹的业务模型与关键边界条件(目标、截止时间、退款/领取)
  • 能从零实现一个支持多 Campaign 的众筹合约(以太币版本)
  • 设计安全的资金流(pull-over-push、checks-effects-interactions、重入保护)
  • 用 Foundry 写完整测试(创建 Campaign、认购、退回、提取)

2、关键设计点

  1. Campaign 状态机
    • Active(正在进行,可 pledging)
    • Successful(达到目标,所有人可领取)
    • Failed(截止且未达到目标,支持退款)
    • Withdrawn(创建者已领取资金)
  2. 谁可以做什么
    • 任意地址可以创建 Campaign(或限定为合约 Owner)
    • 任意地址在活动期间可 pledge(支付 ETH)
    • 创建者在活动结束且目标达成后可 claim(提取所有资金)
    • 投资者在活动结束且目标未达成后可 refund(取回自己投入)
  3. 资金流安全原则
    • Pull over Push:优先把退款/领取弧度做成可提款模式(用户调用提取),不要把外部合约回调放在自动转账中
    • Checks-Effects-Interactions:先改变合约状态再进行外部调用
    • 重入防护:使用互斥锁或 OpenZeppelin 的 ReentrancyGuard
    • 检查零地址、金额、截止时间合理性
  4. 时间处理
    • 使用 block.timestamp;注意矿工可微调时间(可被操纵 ~900s),对大额攻击场景需谨慎
  5. Gas / DoS 风险
    • 不要在单笔函数里遍历大量数组(避免被 gas 限制 DOS)
    • 使用 mapping 存储出资详情,避免遍历退款列表

3、简单实现

src/SimpleCrowdfunding.sol

继续阅读

《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战

1、学习目标

  • 理解 ERC721 与 ERC1155 的标准接口
  • 从零实现一个 最小化 ERC721(NFT)合约
  • 扩展功能:元数据管理(BaseURI)、批量铸造 / 批量转账
  • 对比 OpenZeppelin 实现
  • 使用 Foundry 测试

2、知识点梳理

  1. ERC721 核心接口
    • balanceOf(address)
    • ownerOf(uint256)
    • safeTransferFrom(address,address,uint256)
    • transferFrom(address,address,uint256)
    • approve(address,uint256) / setApprovalForAll(address,bool)
    • 事件:Transfer, Approval, ApprovalForAll
  2. ERC1155 核心接口
    • 支持 多代币标准(FT / NFT / SFT)
    • balanceOf(address,uint256)
    • safeTransferFrom(address,address,uint256,uint256,bytes)
    • safeBatchTransferFrom(...)
    • 事件:TransferSingle, TransferBatch, ApprovalForAll
  3. 应用场景差异
    • ERC721 → 独一无二的资产(头像、土地、艺术品)
    • ERC1155 → 大规模批量资产(游戏道具、门票、盲盒)

3、最小 ERC721 实现

MyERC721.sol

继续阅读

Go 并发编程实战:从数据竞争到 Mutex 与读写锁

在日常开发中,我们经常会遇到高并发的业务场景,比如钱包系统的转账。如何保证并发情况下的数据一致性,是 Go 工程师必须掌握的技能之一。今天我用一个简单的钱包转账例子,带大家看看 Go 中数据竞争是怎么发生的,以及如何用 sync.Mutexsync.RWMutex 来解决。

继续阅读