首页
学习
活动
专区
圈层
工具
发布
49 篇文章
1
《纸上谈兵·solidity》第 0 课:搭建 Solidity 开发环境(三种方式)
2
《纸上谈兵·solidity》第 1 课:部署你的第一个 Solidity 合约
3
《纸上谈兵·solidity》第 2 课:调用、修改、读取,Solidity 合约不是 REST API
4
《纸上谈兵·solidity》第 3 课:事件(Event)机制与链上日志——不是 print,是广播!
5
《纸上谈兵·solidity》第 4 课:Solidity 合约中的错误处理机制(`require`、`revert`、`assert`)和自定义错误
6
《纸上谈兵·solidity》第 5 课:依赖与外部调用 —— 合约交互的风险与防护
7
《纸上谈兵·solidity》第 6 课:Solidity 数据存储布局 —— memory、storage、calldata 傻傻分不清?
8
《纸上谈兵·solidity》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签
9
《纸上谈兵·solidity》第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
10
《纸上谈兵·solidity》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具
11
《纸上谈兵·solidity》第 10 课:Solidity `fallback` / `receive` 函数 —— 合约如何收 ETH 和响应未知调用?
12
《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败
13
《纸上谈兵·solidity》第 12 课:Solidity 函数选择器与 ABI 编码原理
14
《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”
15
《纸上谈兵·solidity》第 14 课:Solidity 中的可升级合约模式 —— 从代理合约到透明代理、UUPS 与安全陷阱
16
《纸上谈兵·solidity》第 15 课:Solidity 库与可重用代码
17
《纸上谈兵·solidity》第 16 课:Pull over Push 支付模式与 Check-Effects-Interactions 原则
18
《纸上谈兵·solidity》第 17 课:合约设计模式实战(二)—— Access Control 与权限管理
19
《纸上谈兵·solidity》第 18 课:合约设计模式实战(三)—— 代理 + 插件化架构(Diamond Standard / EIP-2535)
20
《纸上谈兵·solidity》第 19 课:安全专题(一)—— 常见攻击手法与防御
21
《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞
22
《纸上谈兵·solidity》第 21 课:Gas 优化与成本分析 —— 写出便宜的智能合约
23
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
24
《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战
25
《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战
26
《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)简化版
27
《纸上谈兵·solidity》第 26 课:借贷合约简化实现
28
《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)
29
《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)
30
《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)
31
《纸上谈兵·solidity》第 30 课:智能合约安全审计案例复盘 -- Nomad Bridge(2022)
32
《纸上谈兵·solidity》第 31 课:多签钱包在跨链桥中的应用 —— Nomad 事件复盘
33
《纸上谈兵·solidity》第 32 课:DeFi 基础合约
34
《纸上谈兵·solidity》第 33 课:多签钱包(Multisig Wallet)-- 合约设计与实现
35
《纸上谈兵·solidity》第 34 课:多签钱包(Multisig Wallet)-- 上线
36
《纸上谈兵·solidity》第 35 课:去中心化交易所(DEX)实战 — 合约设计
37
《纸上谈兵·solidity》第 36 课:去中心化交易所(DEX)实战 — 上线
38
《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型
39
《纸上谈兵·solidity》第 38 课:DeFi 实战(2) -- 清算机制与价格预言机
40
《纸上谈兵·solidity》第 39 课:DeFi 实战(3) -- 利息累积与 aToken 设计
41
《纸上谈兵·solidity》第 40 课:DeFi 实战(4) -- 风险控制与防护
42
《纸上谈兵·solidity》第 41 课:DeFi 实战(5) -- 协议费与治理
43
《纸上谈兵·solidity》第 42 课:DeFi 实战(6) -- 跨资产借贷与多市场支持
44
《纸上谈兵·solidity》第 43 课:DeFi 实战(7) -- 清算机制进阶(多资产抵押清算路径、拍卖机制)
45
《纸上谈兵·solidity》第 44 课:DeFi 实战(8) -- 利率曲线与资金池优化(动态利用率模型)
46
《纸上谈兵·solidity》第 45 课:DeFi 实战(9) -- 利息累积与结算机制(可复利)
47
《纸上谈兵·solidity》第 46 课:DeFi 实战(10) -- 跨链借贷与流动性桥接
48
《纸上谈兵·solidity》第 47 课:DeFi 实战(11) -- 治理代币 & 激励机制(Tokenomics & Governance)
49
《纸上谈兵·solidity》第 48 课:DeFi 实战(12) -- 前端 DApp 集成与用户交互(React + ethers.js 实战)

《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”

1. 三种低级调用方式对比

调用方式

是否切换上下文(storage/msg.sender/msg.value)

是否能改状态

特点与用途

call

✅ 切换到被调用合约

最通用的外部调用,可带 ETH,可调用任意函数

delegatecall

❌ 保持当前合约上下文

代理模式核心,让当前合约执行别人的代码

staticcall

✅ 切换到被调用合约

安全读取外部数据,不改状态

记忆口诀:

call:切场景、能改状态。 delegatecall:不切场景、能改状态。 staticcall:切场景、不能改状态。


2. 原理解析

在 EVM 中,外部调用本质是一次 CALL 指令:

代码语言:txt
复制
CALL(gas, to, value, in_offset, in_size, out_offset, out_size)
  • gas:给被调用者的剩余 gas。
  • to:目标地址。
  • value:转账的 wei 数量。
  • in_offset / in_size:内存中输入数据的位置和长度(ABI 编码后)。
  • out_offset / out_size:输出数据存放位置和长度。

delegatecallcall 的主要区别是:

  • delegatecall 不会更改 msg.sendermsg.value
  • 存储上下文(storage slot)不切换,直接写到当前合约。

staticcall 的底层指令是 STATICCALL,它会禁止在调用期间修改状态。


3. call 示例

被调用合约:Callee.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Callee {
    uint256 public value;

    event ValueSet(uint256 newValue);

    function setValue(uint256 _v) external payable {
        value = _v;
        emit ValueSet(_v);
    }

    function getValue() external view returns (uint256) {
        return value;
    }
}

调用方合约:Caller.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Caller {
    // 通过 call 调用 setValue
    function callSetValue(address _callee, uint256 _v) external payable {
        (bool success, ) = _callee.call(
            abi.encodeWithSignature("setValue(uint256)", _v)
        );
        require(success, "call failed");
    }

    // 通过 staticcall 调用 getValue
    function callGetValue(address _callee) external view returns (uint256) {
        (, bytes memory data) = _callee.staticcall(
            abi.encodeWithSignature("getValue()")
        );
        return abi.decode(data, (uint256));
    }
}

4. delegatecall 示例(代理模式)

逻辑合约:Logic.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Logic {
    uint256 public value;

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

代理合约:Proxy.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Proxy {
    uint256 public value;

    function delegateSetValue(address _logic, uint256 _v) external {
        (bool success, ) = _logic.delegatecall(
            abi.encodeWithSignature("setValue(uint256)", _v)
        );
        require(success, "delegatecall failed");
    }
}

注意LogicProxy 必须有完全一致的存储布局,否则变量会错位(Storage Collision)。


5. Foundry 测试

test/LowLevelCall.t.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Callee.sol";
import "../src/Caller.sol";
import "../src/Logic.sol";
import "../src/Proxy.sol";

contract LowLevelCallTest is Test {
    Callee callee;
    Caller caller;
    Logic logic;
    Proxy proxy;

    function setUp() public {
        callee = new Callee();
        caller = new Caller();
        logic = new Logic();
        proxy = new Proxy();
    }

    function testCallSetValue() public {
        caller.callSetValue(address(callee), 42);
        assertEq(callee.value(), 42);
    }

    function testStaticCallGetValue() public {
        caller.callSetValue(address(callee), 99);
        uint256 v = caller.callGetValue(address(callee));
        assertEq(v, 99);
    }

    function testDelegateCall() public {
        proxy.delegateSetValue(address(logic), 123);
        assertEq(proxy.value(), 123);
        assertEq(logic.value(), 0); // Logic 本身不变
    }
}

执行测试命令:

代码语言:bash
复制
➜  counter git:(main) ✗ forge test --match-path test/LowLevelCall.t.sol -vvv
[⠊] Compiling...
[⠊] Compiling 2 files with Solc 0.8.29
[⠒] Solc 0.8.29 finished in 1.91s
Compiler run successful!

Ran 3 tests for test/LowLevelCall.t.sol:LowLevelCallTest
[PASS] testCallSetValue() (gas: 39545)
[PASS] testDelegateCall() (gas: 41920)
[PASS] testStaticCallGetValue() (gas: 41426)
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 11.37ms (7.34ms CPU time)

Ran 1 test suite in 616.04ms (11.37ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)

6. 常见陷阱

  • call 未检查返回值
代码语言:txt
复制
addr.call(data); // ❌ 忽略 success

必须:

代码语言:txt
复制
(bool success, bytes memory ret) = addr.call(data);
require(success, "call failed");
  • delegatecall 存储错乱:如果 Logic 的第一个状态变量是 address ownerProxyuint256 value,那么写入会覆盖错误的 slot。
  • call 触发重入攻击:外部调用前先更新状态(Checks-Effects-Interactions 模式)。
  • staticcall 不能修改状态:调用改状态的函数会直接 revert。

7. 最佳实践

场景

推荐方式

原因

调用外部合约并可能携带 ETH

call

灵活,可同时发送数据和 ETH

代理模式 / 可升级合约

delegatecall

保持存储一致,执行外部逻辑

只读查询外部合约数据

staticcall

只读,避免误改状态


8. 总结

  • call:像跨合同打电话,带钱和信息。
  • delegatecall:让别人用你的钱包执行代码。
  • staticcall:借别人的计算器算一算,不动任何钱。

低级调用是合约开发的“裸金属编程”,没有编译器的保护网,一旦出错,可能是重入漏洞资金丢失数据错乱

最重要的建议

  • 始终检查 success
  • 先修改状态再外部调用
  • 代理模式要保持存储一致
下一篇
举报
领券