SC Day 17
Solidity - library + using for + assembly 初探 + 低级调用(call/delegatecall/staticcall)
### 1. Library:可复用的代码库
2026-04-17
第一阶段:基础构建soliditylibraryassemblycalldelegatecalllow-level
日期: 2026-04-17 方向: Solidity 阶段: 第一阶段:基础构建 标签: #solidity #library #assembly #call #delegatecall #low-level
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 掌握 library 的两种链接模式、inline assembly 基础、三种低级调用的区别和安全性 |
| 实操 | 实现 SafeMath 库、用 call 发送 ETH、用 delegatecall 实现代理模式 |
| 产出 | 完整的库代码 + 低级调用示例 + assembly 操作 + 安全分析 |
核心概念
1. Library:可复用的代码库
Solidity 的 library 是一种特殊的合约,它不能有状态变量,不能接收 ETH,不能被销毁。Library 有两种使用模式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 数学工具库
/// @notice 所有函数都是 pure/view,不修改状态
library MathLib {
/// @notice 安全的除法(Solidity 0.8+ 已内置溢出检查,但不检查除零)
function safeDiv(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0, "Division by zero");
return a / b;
}
/// @notice 计算百分比(basis points, 1 bp = 0.01%)
function percentageBps(uint256 amount, uint256 bps) internal pure returns (uint256) {
return (amount * bps) / 10000;
}
/// @notice 平方根(Babylonian 方法)
function sqrt(uint256 x) internal pure returns (uint256 y) {
if (x == 0) return 0;
y = x;
uint256 z = (x + 1) / 2;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
}
/// @notice 最小值
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/// @notice 最大值
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
}
/// @title 地址工具库
library AddressLib {
/// @notice 检查地址是否为合约
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
/// @notice 安全发送 ETH
function sendETH(address payable to, uint256 amount) internal {
require(address(this).balance >= amount, "Insufficient balance");
(bool success, ) = to.call{value: amount}("");
require(success, "ETH transfer failed");
}
}
2. using for:把库函数附加到类型上
contract TokenSale {
// using for 语法:让 uint256 类型可以直接调用 MathLib 的函数
using MathLib for uint256;
using AddressLib for address;
using AddressLib for address payable;
uint256 public price = 100; // 100 wei per token
uint256 public platformFeeBps = 250; // 2.5% 平台手续费
function calculateCost(uint256 amount) public view returns (uint256 cost, uint256 fee) {
cost = amount * price;
// 直接在 uint256 上调用 MathLib.percentageBps
// 等同于 MathLib.percentageBps(cost, platformFeeBps)
fee = cost.percentageBps(platformFeeBps);
}
function buy(uint256 amount) external payable {
(uint256 cost, uint256 fee) = calculateCost(amount);
require(msg.value >= cost, "Insufficient payment");
// 使用 AddressLib 检查
require(!msg.sender.isContract(), "No contracts allowed");
// 发送手续费
payable(address(0xFee)).sendETH(fee);
}
function sqrtPrice() public view returns (uint256) {
return price.sqrt(); // MathLib.sqrt(price)
}
}
3. 三种低级调用:call / delegatecall / staticcall
这三种调用是 Solidity 与外部合约交互的底层机制。理解它们的区别对安全至关重要。
contract LowLevelCalls {
uint256 public value;
address public lastCaller;
// ====== call:最常用的外部调用 ======
// 特点:在目标合约的上下文中执行
// msg.sender = 调用方(本合约)
// 存储上下文 = 目标合约
function callExample(address target, uint256 amount) external {
// 调用目标合约的 setValue 函数
(bool success, bytes memory returnData) = target.call(
abi.encodeWithSignature("setValue(uint256)", amount)
);
require(success, "Call failed");
// 带 ETH 的 call
(bool sent, ) = target.call{value: 1 ether}("");
require(sent, "ETH send failed");
// 带 Gas 限制的 call
(bool ok, bytes memory data) = target.call{gas: 50000}(
abi.encodeWithSignature("expensiveFunction()")
);
}
// ====== delegatecall:代理模式的核心 ======
// 特点:在调用方(本合约)的上下文中执行目标合约的代码
// msg.sender = 保持不变(原始调用者)
// 存储上下文 = 本合约(不是目标合约!)
// 极度危险:目标合约可以修改本合约的存储
function delegateCallExample(address implementation) external {
(bool success, bytes memory data) = implementation.delegatecall(
abi.encodeWithSignature("setValue(uint256)", 42)
);
require(success, "Delegatecall failed");
// 注意:如果 implementation 修改了 slot 0 的存储,
// 那么本合约的 value(slot 0)会被修改!
}
// ====== staticcall:只读调用 ======
// 特点:不允许修改状态(view/pure)
// 如果目标函数试图修改状态,调用会 revert
function staticCallExample(address target) external view returns (uint256) {
(bool success, bytes memory data) = target.staticcall(
abi.encodeWithSignature("getValue()")
);
require(success, "Staticcall failed");
return abi.decode(data, (uint256));
}
}
三种调用对比表
┌─────────────────┬─────────────┬──────────────┬──────────────┐
│ 特性 │ call │ delegatecall │ staticcall │
├─────────────────┼─────────────┼──────────────┼──────────────┤
│ 代码来源 │ 目标合约 │ 目标合约 │ 目标合约 │
│ 存储上下文 │ 目标合约 │ 调用方 │ 目标合约 │
│ msg.sender │ 调用方 │ 原始调用者 │ 调用方 │
│ msg.value │ 可指定 │ 保持原值 │ 0 │
│ 可修改状态 │ 是 │ 是(调用方的) │ 否 │
│ 可发送 ETH │ 是 │ 否 │ 否 │
│ 主要用途 │ 外部调用 │ 代理/升级模式 │ 只读查询 │
│ 安全风险 │ 重入 │ 存储冲突 │ 最安全 │
└─────────────────┴─────────────┴──────────────┴──────────────┘
4. abi.encode 系列函数
contract ABIEncoding {
// ====== abi.encode:标准 ABI 编码(带填充到32字节)======
function encodeExample() public pure returns (bytes memory) {
return abi.encode(uint256(1), address(0x123), "hello");
// 每个参数填充为 32 字节
}
// ====== abi.encodePacked:紧凑编码(不填充)======
function encodePackedExample() public pure returns (bytes memory) {
return abi.encodePacked(uint8(1), address(0x123), "hello");
// 参数紧密排列,不填充
// 注意:不同参数可能产生相同编码(碰撞风险)
}
// ====== abi.encodeWithSignature:编码函数调用 ======
function encodeCallExample() public pure returns (bytes memory) {
return abi.encodeWithSignature("transfer(address,uint256)",
0x1234567890AbCdEf1234567890abCdEf12345678,
1000
);
// = bytes4(keccak256("transfer(address,uint256)")) + abi.encode(args)
}
// ====== abi.encodeWithSelector:用 selector 编码 ======
function encodeSelectorExample() public pure returns (bytes memory) {
return abi.encodeWithSelector(
bytes4(keccak256("transfer(address,uint256)")),
0x1234567890AbCdEf1234567890abCdEf12345678,
1000
);
}
// ====== abi.decode:解码返回数据 ======
function decodeExample(bytes memory data) public pure returns (uint256, address) {
return abi.decode(data, (uint256, address));
}
}
5. Inline Assembly 初探
Solidity 中的 inline assembly 使用 Yul 语言,可以直接操作 EVM。在需要极致 Gas 优化时使用。
contract AssemblyBasics {
uint256 private storedValue;
// ====== 基础操作 ======
/// @notice 用 assembly 读取存储
function getValueAssembly() external view returns (uint256 result) {
assembly {
// sload:从存储槽读取
// storedValue 在 slot 0
result := sload(0)
}
}
/// @notice 用 assembly 写入存储
function setValueAssembly(uint256 newValue) external {
assembly {
// sstore:写入存储槽
sstore(0, newValue)
}
}
/// @notice 获取调用者地址
function getCallerAssembly() external view returns (address result) {
assembly {
result := caller() // 等同于 msg.sender
}
}
/// @notice 获取当前合约余额
function getBalanceAssembly() external view returns (uint256 result) {
assembly {
result := selfbalance() // 等同于 address(this).balance
}
}
/// @notice 高效的 keccak256
function hashAssembly(uint256 a, uint256 b) external pure returns (bytes32 result) {
assembly {
// 将数据写入内存
mstore(0x00, a)
mstore(0x20, b)
// 对 64 字节数据做 keccak256
result := keccak256(0x00, 0x40)
}
}
// ====== 实用场景:高效的地址检查 ======
/// @notice 检查地址是否为零地址(比 require(addr != address(0)) 更省 Gas)
function isZeroAddress(address addr) external pure returns (bool result) {
assembly {
result := iszero(addr)
}
}
// ====== 实用场景:自定义 revert 消息 ======
/// @notice 用 assembly 实现高效的 require
function efficientRequire(bool condition) external pure {
assembly {
if iszero(condition) {
// 存储 Error(string) selector
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(0x04, 0x20) // 字符串偏移
mstore(0x24, 0x0e) // 字符串长度 (14 bytes)
mstore(0x44, "Check failed!") // 错误消息
revert(0x00, 0x64)
}
}
}
}
代码实战:ETH 发送的三种方式 + 代理合约模式
ETH 发送对比
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ETHSender {
// ====== 方法1:transfer(不推荐)======
// - 固定 2300 gas
// - 失败自动 revert
// - 问题:2300 gas 可能不够接收方合约执行逻辑
function sendViaTransfer(address payable to) external payable {
to.transfer(msg.value);
}
// ====== 方法2:send(不推荐)======
// - 固定 2300 gas
// - 失败返回 false(不自动 revert)
// - 问题:同 transfer,且容易忘记检查返回值
function sendViaSend(address payable to) external payable {
bool success = to.send(msg.value);
require(success, "Send failed");
}
// ====== 方法3:call(推荐)======
// - 转发所有可用 gas(可指定)
// - 失败返回 (false, data)
// - 最灵活,但需注意重入攻击
function sendViaCall(address payable to) external payable {
(bool success, ) = to.call{value: msg.value}("");
require(success, "Call failed");
}
receive() external payable {}
}
代理合约(delegatecall 核心应用)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 逻辑合约 V1
contract LogicV1 {
// 存储布局必须与代理合约一致!
uint256 public value;
address public admin;
function setValue(uint256 _value) external {
value = _value;
}
function getValue() external view returns (uint256) {
return value;
}
function version() external pure returns (string memory) {
return "V1";
}
}
/// @title 逻辑合约 V2(升级后的版本)
contract LogicV2 {
uint256 public value;
address public admin;
function setValue(uint256 _value) external {
value = _value * 2; // V2: 存储值翻倍
}
function getValue() external view returns (uint256) {
return value;
}
function version() external pure returns (string memory) {
return "V2";
}
// V2 新增函数
function increment() external {
value += 1;
}
}
/// @title 简易代理合约
/// @notice 所有调用通过 delegatecall 转发到逻辑合约
contract SimpleProxy {
// 存储槽:与逻辑合约相同的布局
uint256 public value;
address public admin;
// 逻辑合约地址(使用特殊存储槽避免冲突,简化版直接用变量)
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}
/// @notice 升级逻辑合约
function upgradeTo(address newImplementation) external {
require(msg.sender == admin, "Not admin");
require(newImplementation != address(0), "Zero address");
implementation = newImplementation;
}
/// @notice fallback:所有未匹配的调用都 delegatecall 到逻辑合约
fallback() external payable {
address impl = implementation;
require(impl != address(0), "No implementation");
assembly {
// 复制 calldata 到内存
calldatacopy(0, 0, calldatasize())
// delegatecall 到逻辑合约
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
// 复制返回数据
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
// delegatecall 失败,revert
revert(0, returndatasize())
}
default {
// 成功,返回数据
return(0, returndatasize())
}
}
}
receive() external payable {}
}
// 使用示例:
// 1. 部署 LogicV1
// 2. 部署 SimpleProxy(LogicV1.address)
// 3. 通过 Proxy 调用 setValue(42) → 实际修改的是 Proxy 的存储
// 4. 部署 LogicV2
// 5. 调用 proxy.upgradeTo(LogicV2.address)
// 6. 通过 Proxy 调用 setValue(42) → 现在存储的是 84(翻倍逻辑)
关键要点总结
| 要点 | 说明 |
|---|---|
| library 不能有状态变量 | 纯工具函数集合 |
using A for B | 让类型 B 可以直接调用 A 的函数 |
| call 推荐用于发送 ETH | transfer/send 的 2300 gas 限制太严格 |
| delegatecall 存储上下文是调用方 | 代理模式核心,但极度危险 |
| staticcall 用于只读 | 保证不会修改状态 |
| 存储布局必须一致 | delegatecall 时存储槽位必须与代理合约对齐 |
| assembly 用于极致优化 | 直接操作 EVM,节省 Gas 但牺牲可读性 |
常见误区
误区 1:delegatecall 的存储槽冲突
// 代理合约
contract Proxy {
address public implementation; // slot 0
address public admin; // slot 1
}
// 逻辑合约
contract Logic {
uint256 public value; // slot 0 → 会覆盖 Proxy 的 implementation!
}
// 解决方案:EIP-1967 使用随机存储槽
// bytes32 constant IMPLEMENTATION_SLOT =
// bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
误区 2:忘记检查 call 的返回值
// 危险!忽略返回值,ETH 可能发送失败但代码继续执行
target.call{value: 1 ether}("");
// 正确
(bool success, ) = target.call{value: 1 ether}("");
require(success, "Transfer failed");
误区 3:在 library 中使用 this
Library 中的 this 指向调用它的合约,而不是 library 自身。Library 没有自己的地址和存储。
误区 4:delegatecall 中 msg.sender 改变
// 用户 → ProxyA → LogicB (via delegatecall)
// 在 LogicB 中:
// msg.sender = 用户(不是 ProxyA!)
// address(this) = ProxyA(不是 LogicB!)
面试关联
Q: call 和 delegatecall 的区别?为什么 delegatecall 对代理模式至关重要?
30 秒回答:call 在目标合约的存储上下文中执行代码,delegatecall 在调用方的存储上下文中执行目标合约的代码。代理模式需要 delegatecall,因为它允许代理合约使用逻辑合约的代码来操作自己的存储。这样升级时只需更换逻辑合约地址,用户数据(存储在代理合约中)不受影响。
追问:delegatecall 有什么安全风险?
- 存储槽冲突:逻辑合约和代理合约的存储布局必须完全一致
- 选择器冲突:代理合约和逻辑合约可能有同名函数
- 初始化漏洞:逻辑合约的 constructor 不会在代理上下文中执行
- 解决方案:EIP-1967 定义标准存储槽位,OpenZeppelin 的 TransparentProxy/UUPS 模式
Q: Solidity 中发送 ETH 的三种方式?推荐哪种?
transfer:2300 gas,失败 revert。不推荐,gas 限制太死板send:2300 gas,失败返回 false。不推荐,容易忘检查返回值call{value: ...}(""):转发所有 gas,失败返回 (false, data)。推荐,最灵活- 但用
call时必须注意重入攻击,使用 CEI 模式或 ReentrancyGuard
参考资源
| 资源 | 说明 |
|---|---|
| Solidity Library 文档 | 官方库文档 |
| Yul 语言规范 | Assembly 语法参考 |
| EIP-1967 | 代理合约存储槽标准 |
| OpenZeppelin Proxy | 代理合约最佳实践 |
| Solidity by Example - Delegatecall | 图解 delegatecall |
| Trail of Bits - Building Secure Contracts | 安全最佳实践 |