🤝 第10课:合约交互

智能合约 ✅ 验证通过

🎯 学习目标:掌握合约间调用(直接/接口/call/delegatecall),理解代理模式,防御重入攻击。

📖 一、四种合约间调用方式

直接调用: Target(addr).func(x) ✅类型安全 ❌编译绑定 接口调用: IERC20(token).transfer() ✅标准化 低级call: (ok,data)=addr.call(d) ✅灵活 ⚠️无类型检查 delegatecall: addr.delegatecall(d) ✅代理基础 ⚠️存储碰撞!

💻 二、合约间调用实战

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

contract Calculator {
    uint256 public lastResult;
    event Calc(string op, uint256 result);
    function add(uint256 a, uint256 b) external returns (uint256) {
        lastResult = a + b; emit Calc("add", lastResult); return lastResult;
    }
    function mul(uint256 a, uint256 b) external returns (uint256) {
        lastResult = a * b; emit Calc("mul", lastResult); return lastResult;
    }
    receive() external payable {}
}

interface ICalc {
    function add(uint256, uint256) external returns (uint256);
}

contract Caller {
    function callDirect(address c, uint256 a, uint256 b) public returns (uint256) {
        return Calculator(c).add(a, b);
    }
    function callInterface(address c, uint256 a, uint256 b) public returns (uint256) {
        return ICalc(c).add(a, b);
    }
    function callLowLevel(address c, uint256 a, uint256 b) public returns (bool, uint256) {
        bytes memory d = abi.encodeWithSignature("add(uint256,uint256)", a, b);
        (bool ok, bytes memory r) = c.call(d);
        uint256 res = ok && r.length >= 32 ? abi.decode(r, (uint256)) : 0;
        return (ok, res);
    }
    function callWithETH(address t) public payable returns (bool) {
        (bool s, ) = t.call{value: msg.value}("");
        require(s); return s;
    }
}
// ✅ 验证通过

📖 三、Delegatecall与代理模式

contract SimpleProxy {
    address public impl;
    address public admin;
    uint256 public ver;

    constructor(address i) { impl = i; admin = msg.sender; ver = 1; }

    fallback() external payable {
        address i = impl;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let r := delegatecall(gas(), i, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch r case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
    receive() external payable {}

    function upgrade(address n) public {
        require(msg.sender == admin); impl = n; ver++;
    }
}
contract LogicV1 { uint256 public count; function inc() public { count += 1; } }
contract LogicV2 { uint256 public count; uint256 public total; function inc() public { count += 2; total++; } }
// ✅ 验证通过 ⚠️存储布局必须对齐!
⚠️ delegatecall存储碰撞是最大风险!逻辑合约存储布局必须与代理完全对齐。生产环境用OpenZeppelin透明代理/UUPS。

📖 四、重入攻击与CEI防御

// ❌ 存在重入漏洞!
contract Vulnerable {
    mapping(address => uint256) public balances;
    function withdraw() public {
        uint256 b = balances[msg.sender];
        (bool s, ) = msg.sender.call{value: b}("");  // 先转账
        require(s);
        balances[msg.sender] = 0;  // 后更新(太晚!)
    }
}

// ✅ CEI模式 (Checks-Effects-Interactions)
contract Secure {
    mapping(address => uint256) public balances;
    bool private _locked;
    modifier nonReentrant() {
        require(!_locked, "Reentrancy"); _locked = true; _; _locked = false;
    }
    function withdraw() public nonReentrant {
        uint256 b = balances[msg.sender];
        require(b > 0);
        balances[msg.sender] = 0;  // 先更新(Effects)
        (bool s, ) = msg.sender.call{value: b}("");  // 后交互(Interactions)
        require(s);
    }
}
// ✅ 验证通过
delegatecall vs call? CEI正确顺序? 重入根因? 代理升级最重要? 低级call风险?

🔧 动手实验

  1. 部署Calculator+Caller测试三种调用
  2. 部署Proxy+LogicV1升级到LogicV2
  3. 验证升级后存储连续性
  4. 演示重入攻击和CEI防御

📖 五、receive和fallback

contract ReceiveFallback {
    event Received(address from, uint256 amount, bytes data);

    // ✅ 接收纯ETH转账(无calldata)
    receive() external payable {
        emit Received(msg.sender, msg.value, "");
    }

    // ✅ 处理不匹配的函数调用(有calldata)
    fallback() external payable {
        emit Received(msg.sender, msg.value, msg.data);
    }

    // 调用优先级:
    // 有data → fallback | 无data → receive(如果存在)
    // 只有fallback → 所有情况都走fallback
}
// ✅ 验证通过

📖 六、CREATE2预计算地址

contract Factory {
    // CREATE: 地址 = keccak256(rlp([sender, nonce]))
    function create(bytes memory bytecode) public returns (address) {
        address addr;
        assembly { addr := create(0, add(bytecode, 0x20), mload(bytecode)) }
        require(addr != address(0)); return addr;
    }

    // CREATE2: 地址 = keccak256(0xff, sender, salt, keccak256(bytecode))
    // ✅ 可预先计算部署地址! 反事实部署基础
    function create2(bytes memory bytecode, uint256 salt) public returns (address) {
        address addr;
        assembly { addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) }
        require(addr != address(0)); return addr;
    }

    // 预计算地址(不实际部署)
    function getAddress(bytes memory bytecode, uint256 salt) public view returns (address) {
        return address(uint160(uint256(keccak256(abi.encodePacked(
            bytes1(0xff), address(this), salt, keccak256(bytecode)
        )))));
    }
}
// ✅ 验证通过

📖 七、常见陷阱与最佳实践

  1. ❌ delegatecall存储布局不对齐导致数据错乱
  2. ❌ 外部调用失败不传播(需检查返回值)
  3. ❌ 循环中的外部调用可能导致Gas溢出
  4. ❌ CEI模式未遵守导致重入攻击
  5. ❌ 代理和逻辑合约的storage layout不匹配
  6. ✅ 使用OpenZeppelin代理而非手写
  7. ✅ 所有外部调用都检查返回值

📖 八、多签钱包实现

contract MultiSigWallet {
    event Deposit(address indexed sender, uint256 amount);
    event SubmitTransaction(address indexed owner, uint256 txIndex, address to, uint256 value, bytes data);
    event ConfirmTransaction(address indexed owner, uint256 txIndex);
    event ExecuteTransaction(address indexed owner, uint256 txIndex);

    address[] public owners;
    mapping(address => bool) public isOwner;
    uint256 public required;  // 需要多少确认

    struct Transaction { address to; uint256 value; bytes data; bool executed; }
    Transaction[] public transactions;
    mapping(uint256 => mapping(address => bool)) public isConfirmed;

    modifier onlyOwner() { require(isOwner[msg.sender]); _; }
    modifier txExists(uint256 i) { require(i < transactions.length); _; }
    modifier notExecuted(uint256 i) { require(!transactions[i].executed); _; }
    modifier notConfirmed(uint256 i) { require(!isConfirmed[i][msg.sender]); _; }

    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length >= _required && _required > 0);
        for (uint i = 0; i < _owners.length; i++) {
            require(!isOwner[_owners[i]] && _owners[i] != address(0));
            isOwner[_owners[i]] = true; owners.push(_owners[i]);
        }
        required = _required;
    }

    function submitTransaction(address to, uint256 value, bytes memory data) public onlyOwner returns (uint256) {
        transactions.push(Transaction(to, value, data, false));
        emit SubmitTransaction(msg.sender, transactions.length - 1, to, value, data);
        return transactions.length - 1;
    }

    function confirmTransaction(uint256 i) public onlyOwner txExists(i) notExecuted(i) notConfirmed(i) {
        isConfirmed[i][msg.sender] = true;
        emit ConfirmTransaction(msg.sender, i);
    }

    function executeTransaction(uint256 i) public onlyOwner txExists(i) notExecuted(i) {
        uint256 count = 0;
        for (uint j = 0; j < owners.length; j++) if (isConfirmed[i][owners[j]]) count++;
        require(count >= required, "Not enough confirmations");
        Transaction storage t = transactions[i]; t.executed = true;
        (bool s, ) = t.to.call{value: t.value}(t.data);
        require(s, "tx failed"); emit ExecuteTransaction(msg.sender, i);
    }

    receive() external payable { emit Deposit(msg.sender, msg.value); }
}
// ✅ 验证通过

📖 九、合约组合模式

// ✅ 合约组合: 通过继承和接口组合功能
contract ReentrancyGuard {
    bool private _locked;
    modifier nonReentrant() {
        require(!_locked, "Reentrancy"); _locked = true; _;
        _locked = false;
    }
}

contract Ownable {
    address public owner;
    modifier onlyOwner() { require(msg.sender == owner); _; }
    constructor() { owner = msg.sender; }
}

contract Pausable {
    bool public paused;
    modifier whenNotPaused() { require(!paused); _; }
    function togglePause() public virtual { paused = !paused; }
}

// ✅ 组合继承
contract DeFiVault is Ownable, ReentrancyGuard, Pausable {
    mapping(address => uint256) public deposits;

    function deposit() public payable whenNotPaused nonReentrant {
        deposits[msg.sender] += msg.value;
    }

    function withdraw(uint256 amt) public whenNotPaused nonReentrant {
        require(deposits[msg.sender] >= amt);
        deposits[msg.sender] -= amt;
        (bool s, ) = msg.sender.call{value: amt}("");
        require(s);
    }

    function togglePause() public override onlyOwner { super.togglePause(); }
}
// ✅ 验证通过

📖 十、合约安全审计清单

交互安全检查项:
1. ✅ 所有外部调用检查返回值
2. ✅ 遵循CEI模式防重入
3. ✅ 设置合理的Gas限制
4. ✅ 验证合约地址非零
5. ✅ 使用SafeERC20处理代币
6. ✅ 防止整数溢出(0.8+自动)
7. ✅ 访问控制到位
8. ✅ 紧急暂停机制
9. ✅ 时间锁保护关键操作
10. ✅ 多签管理管理员权限
🤝

合约交互者

你已掌握合约间通信与安全——调用/代理/重入防护!

✅ 调用方式 ✅ 代理模式 ✅ 重入防护 ✅ CEI模式

📋 课程目录