
一.内容提要
-
call, delegatecall, staticcall 和 multicall 基本使用
-
跨合约的调用方式
-
函数选择器
-
create2 底层原理与实现机制
-
合约删除(合约自毁,历史版本)
-
Solidity 内联汇编
-
OZ 代码库讲解
二.call, delegatecall, staticcall 和 multicall 基本使用
call, delegatecall, staticcall 和 multicall 是 solidity 里面用于合约交互的四种重要的机制, call 和 delegatecall 是用于执行外部合约调用的低 level 函数,staticcall 是一种只读调用,用于调用另一个合约的只读函数, Multicall 是一种实现,可以一次性执行多个合约调用,减少交易成本和提高效率。
Call
【图示 1】原文此处为流程图/白板图示,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/Ojdudecv7o96bIxvh6qlwFphg7e
Call 的特点
-
基本含义:call 是 solidity 里面最底层的函数的调用方法,用于调用另一个合约的方法,可以发送 ETH 并附带数据,前面已经讲过了
-
特点
-
调用目标合约的函数,代码在目标合约的上下文中执行,改变的是目标合约的状态
-
可以附加 ETH(value)
-
可以调用未知的合约或动态指定调用数据
-
调用成功与否需要手动检查返回值
代码案例
- 合约代码
typescript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract CallFunctionContract {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
- Test
solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {CallFunctionContract, CallFunctionContractImplement } from "../src/CallFunctionContract.sol";
contract CounterTest is Test {
-
结果
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade % forge test -vvv
[⠊] Compiling...
No files changed, compilation skippedRan 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_callSetNumber() (gas: 42367)
Logs:
100
101Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.64ms (1.17ms CPU time)
Ran 1 test suite in 11.82ms (6.64ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
delegatecall
【图示 2】原文此处为流程图/白板图示,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/Ojdudecv7o96bIxvh6qlwFphg7e
基本使用
-
定义:delegatecall 是一种特殊的调用方式,用于在当前合约的上下文中执行另一个合约的代码,发生状态改变的是当前合约。
-
特性
-
目标合约的代码在调用合约的上下文中执行(msg.sender 和 msg.value 保持不变)
-
目标合约的代码操作的是调用合约的存储。
-
适用场景
-
实现代理合约(Proxy Contract)
-
支持合约升级
代码案例
- 合约代码
solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract DelegateCallFunctionContract {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
contract DelegateCallFunctionContractImplement {
uint256 public number;
- Test
typescript
// SPDX-License-Identifier: UNLICENSED
- 执行结果
solidity
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade % forge test -vvv
[⠊] Compiling...
[⠰] Compiling 1 files with Solc 0.8.28
[⠔] Solc 0.8.28 finished in 366.38ms
Compiler run successful!
Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_delegatecall() (gas: 49139)
Logs:
dcfc contract 0
dcfci contract 100
dcfc contract 0
dcfci contract 101
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.72ms (1.51ms CPU time)
Ran 1 test suite in 3.51ms (1.72ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Staticcall
基本使用
-
定义
-
staticcall 是一种只读调用,用于调用另一个合约的只读函数
-
引入于 Solidity 0.4.21,用于防止调用中对状态进行修改
-
特性
-
只读:无法在调用中更改状态变量
-
安全性:确保调用的函数不会意外修改状态
-
用途:适用于需要执行计算或获取数据但不希望更改状态的场景
代码案例
- 合约代码
typescript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract StaticCallContract {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
function getNumber() public view returns (uint256) {
return number;
}
}
contract StaticCallContractImplement {
uint256 public number;
function callSetNumber(address _target, uint256 _number) public {
(bool success, ) = _target.call(
abi.encodeWithSignature("setNumber(uint256)", _number)
);
require(success, "call set number fail");
}
function callIncrement(address _target) public {
(bool success, ) = _target.call(
abi.encodeWithSignature("increment()")
);
require(success, "call increment fail");
}
function callReadNumber(address _target) public view returns (uint256) {
bytes memory data = abi.encodeWithSignature("number()");
(bool success, bytes memory result) = _target.staticcall(data);
- Test
typescript
}
}
-
执行结果
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade % forge test -vvv
[⠊] Compiling...
[⠰] Compiling 1 files with Solc 0.8.28
[⠔] Solc 0.8.28 finished in 391.12ms
Compiler run successful!Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_delegatecall() (gas: 47277)
Logs:
ssci staticcall number 100
ssci staticcall number 101Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 660.17µs (286.17µs CPU time)
Ran 1 test suite in 4.04ms (660.17µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Multicall
- 代码案例
typescript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import { console} from "forge-std/Test.sol";
contract TargetContract {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
contract TargetContractOne {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
contract MultiCall {
struct Call {
address targetAddress;
bytes callData;
}
// 最重要的问题,无限 for 循环可能存在
// - gas 消耗很大
// - 区块链区块大小是有现在,不懂的用户 calls 传得很多,会撑破区块
function multicall(Call[] memory calls) public {
for(uint256 i = 0; i < calls.length; i++) {
(bool success, ) = calls[i].targetAddress.call(calls[i].callData);
require(success, "call item failed");
}
}
}
contract MultiCaller {
MultiCall public multiCall;
constructor(address _multiCall){
multiCall = MultiCall(_multiCall);
}
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade % forge test -vvv
[⠊] Compiling...
[⠔] Compiling 2 files with Solc 0.8.28
[⠒] Solc 0.8.28 finished in 414.11ms
Compiler run successful!
Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_SetNumbers() (gas: 86328)
Logs:
target== 0x2e234DAe75C793f67A35089C9d99245E1C58470b
data== 50
targetOne== 0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
dataOne== 60
=============================
targetContract.number()= 50
targetContractOne.number()= 60
==============================
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.07ms (1.51ms CPU time)
Ran 1 test suite in 13.52ms (7.07ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
三.特殊的全局变量
获取全局变量的方式
-
tx:交易相关的
-
block: 区块相关的数据
-
Msg: 合约调用者的信息
-
address(this): 代表的是当前合约的地址
常用的特殊的全局变量
-
address(this): 当前合约地址
-
tx.origin: 合约调用的源地址
-
msg.sender: 合约的调用者
-
msg.value: 合约调用者要转入合约的 NativeToken 的数量
-
msg.data: 合约的调用者携带的 data
-
block.number: 合约部署所在链最新区块高度
-
block.gasLimit: 合约部署所在链最新区块 gasLimit
-
block.timestamp: 合约部署所在链最新区块 timestamp
-
tx.gasprice: 合约当前交易的 gasprice
三个特殊的全局变量
- tx.origin, msg.sender 和 address(this)
【图示 3】原文此处为流程图/白板图示,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/Ojdudecv7o96bIxvh6qlwFphg7e
案例代码
- 合约代码
- Test
typescript
// SPDX-License-Identifier: UNLICENSED
sql
Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_gb() (gas: 25593)
Logs:
=============================
0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
=============================
===============B==============
0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
===============B==============
===============C==============
0x2e234DAe75C793f67A35089C9d99245E1C58470b
0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38
0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496
===============C==============
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.31ms (1.13ms CPU time)
Ran 1 test suite in 12.75ms (6.31ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
四.跨合约的调用方式
-
第一种:合约调用合约,参考我们学过的所有测试脚本的方式
-
第二种:call, delegatecall, 和 staticall 可以实现跨脚本的调用,参考第二部分内容
-
第三种:通过 interface 的方式调用
前面两种我们都讲过,虽然讲过 interface,但是没有讲过痛过 interface 的方式调用合约
通过 interface 调用合约
- 合约代码
java
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface IInterfaceCall {
event UpdateValueEvent(uint256 oldValue, uint256 newValue);
function updateValue(uint256 _value) external;
function getValue() external view returns(uint256);
}
abstract contract InterfaceCallStorage is IInterfaceCall {
uint256 public value;
}
contract InterfaceCall is InterfaceCallStorage {
function updateValue(uint256 _value) external {
uint256 oldValue = value;
value = _value;
emit UpdateValueEvent(oldValue, _value);
}
function getValue() external view returns(uint256) {
return value;
}
}
contract InterfaceCaller {
IInterfaceCall public caller;
constructor(IInterfaceCall _caller) {
caller = _caller;
}
function updateInterfaceValue (uint256 number) public {
caller.updateValue(number);
}
function getInterfaceValue () public view returns (uint256){
return caller.getValue();
}
}
- Test 文件
typescript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import "../src/InterfaceCall.sol";
contract CounterTest is Test {
- 执行结果
sql
uoshijiang@guoshijiangdeMacBook-Pro contract-upgrade %
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade % forge test -vvv
[⠊] Compiling...
[⠰] Compiling 1 files with Solc 0.8.28
[⠔] Solc 0.8.28 finished in 381.61ms
Compiler run successful!
Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
[PASS] test_gb() (gas: 40517)
Logs:
value=== 2000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 315.96µs (107.38µs CPU time)
Ran 1 test suite in 3.51ms (315.96µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
guoshijiang@guoshijiangdeMacBook-Pro contract-upgrade %
五.函数选择器
函数选择器(Function Selector)是 Solidity 中用于标识特定函数的机制。它是函数签名的哈希值的前 4 个字节,主要用于在低级调用(如 call、delegatecall,staticcall)中指定要调用的目标函数。
函数选择器的生成方式
第一步:提取函数的名字和参数类型
-
函数签名由函数名和参数类型组成,不包括返回值
functionName(typeN1,typeN2,typeN3....typeN)
-
functionName: 函数名字
-
typeN1,typeN2,typeN3...typeN: 函数参数列表类型
第二步: 计算函数哈希值
- 使用 keccake-256 对 functionName(typeN1,typeN2,typeN3...typeN)进行 Hash
第三步: 提取前 4 个字节
- 取第二步生成的 Hash 值做为函数调用的 calldata 的一部分
实际使用案例
-
abi.encodeCall
bytes memory initData = abi.encodeCall(
TheWebThreeToken.initialize,
(deployerAddress, managerAddress)
); -
abi.encodeWithSignature
(bool success, ) = _target.delegatecall(
abi.encodeWithSignature("increment()")
); -
abi.encodeWithSelector
treasureManagerProxyAdmin.upgradeAndCall(
ITransparentUpgradeableProxy(address(treasureManager)),
address(treasureManagerImplementation),
abi.encodeWithSelector(
TreasureManager.initialize.selector,
msg.sender,
Abi 编码的系列函数
在 Solidity 中,abi.encode 和相关的方法主要用于将数据编码成字节数组,便于存储、传输或计算哈希值。这些方法是以太坊 ABI 编码标准的一部分。
-
abi.encode:将参数编码为标准 ABI 格式的字节数组,不带任何长度信息,适合用于哈希计算。
-
abi.encodePacked:将参数紧凑编码为字节数组,去除了动态类型的填充字节,适合用于签名和哈希计算。
-
abi.encodeWithSelector:编码数据并添加函数选择器(4 字节的函数签名 hash),常用于构造函数调用数据。
-
abi.encodeWithSignature:根据函数签名(字符串形式)和参数编码数据,等价于 abi.encodeWithSelector 的扩展版。
abi.encode
-
功能:以标准 ABI 编码格式对参数编码。
-
特点
-
示例
java
- 编码结果:对 encodeExample(1, "hello") 的结果:
java
0x
0000000000000000000000000000000000000000000000000000000000000001 // uint256 a
0000000000000000000000000000000000000000000000000000000000000040 // 偏移量(字符串 b 的起始位置)
0000000000000000000000000000000000000000000000000000000000000005 // 字符串 b 的长度
68656c6c6f000000000000000000000000000000000000000000000000000000 // 字符串 b 的内容(hello)
abi.encodePacked
-
功能:以紧凑格式编码参数。
-
特点
-
示例:
java
pragma solidity ^0.8.0;
- 编码结果:对 encodePackedExample(1, "hello") 的结果
java
0x
010000000000000000000000000000000000000000000000000000000000000068656c6c6f
abi.encodeWithSelector
-
功能:将数据编码,并在开头添加函数选择器
-
特点
-
示例:
java
pragma solidity ^0.8.0;
contract ABIEncodeWithSelectorExample {
function encodeWithSelectorExample(address recipient, uint256 amount) public pure returns (bytes memory) {
return abi.encodeWithSelector(bytes4(keccak256("transfer(address,uint256)")), recipient, amount);
}
}
- 编码结果:对 encodeWithSelectorExample(0xAbCdEf0000000000000000000000000000000000, 100) 的结果:
java
0xa9059cbb // 函数选择器 transfer(address,uint256)
000000000000000000000000abcdef0000000000000000000000000000000000 // recipient
0000000000000000000000000000000000000000000000000000000000000064 // amount
abi.encodeWithSignature
-
功能:根据字符串形式的函数签名生成编码数据。
-
特点
-
示例:
java
pragma solidity ^0.8.0;
contract ABIEncodeWithSignatureExample {
function encodeWithSignatureExample(address recipient, uint256 amount) public pure returns (bytes memory) {
return abi.encodeWithSignature("transfer(address,uint256)", recipient, amount);
}
}
- 编码结果:对 encodeWithSignatureExample(0xAbCdEf0000000000000000000000000000000000, 100) 的结果
java
0xa9059cbb // 函数选择器 transfer(address,uint256)
000000000000000000000000abcdef0000000000000000000000000000000000 // recipient
0000000000000000000000000000000000000000000000000000000000000064 // amount
实践中的注意事项
-
abi.encodePacked 的哈希冲突:
-
函数选择器冲突
-
与哈希结合
案例说明
transfer 示范
- 函数
go
function transfer(address recipient, uint256 amount) public;
- 函数签名
go
transfer(address,uint256)
- Keccak-256 哈希计算
go
keccak256("transfer(address,uint256)")
- 函数选择器
假设哈希值为:
go
ddf252ad 1be2c89b69c2b068fc378daa952ba7f1638f98f9f543ef38e70eb4cc
- 选择器为前 4 个字节
go
0xddf252ad
自定义合约
- 代码
solidity
pragma solidity ^0.8.0;
contract ExampleContract {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
- setValue 函数的函数选择器计算示例:
函数选择器的用途
智能合约调用
在低级调用(如 call)中,函数选择器用于标识目标函数。
go
contract Example {
function callTransfer(address target, address recipient, uint256 amount) public {
bytes memory data = abi.encodeWithSelector(
bytes4(keccak256("transfer(address,uint256)")),
recipient,
amount
);
(bool success, ) = target.call(data);
require(success, "Call failed");
}
}
Fallback 函数
当合约接收到调用但没有匹配函数时,会触发 fallback 函数,开发者可以通过 msg.data 获取函数选择器。
go
contract FallbackExample {
receive() external payable {}
bytes4 selector = bytes4(msg.data[:4]); // depositETH 函数选择器
// 根据 selector 执行相应逻辑
}
receive() external payable {
depositETH()
}
EIP-2535(Diamond Standard)
在模块化合约(如钻石标准)中,函数选择器用于路由到不同的实现模块。
生成函数选择器的工具
- 使用 Solidity 内置工具
go
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
- 使用 JavaScript/TypeScript
go
- 使用 Python
go
from web3 import Web3
selector = Web3.keccak(text="transfer(address,uint256)").hex()[:10]
print(selector) # 输出: 0xa9059cbb
注意事项
-
函数签名的唯一性
-
参数顺序和类型敏感
-
不包括返回值
应用场景
- 动态函数调用:通过函数选择器动态调用任意函数
go
contract DynamicCaller {
function callFunction(address target, bytes4 selector, bytes memory args) public {
(bool success, ) = target.call(abi.encodePacked(selector, args));
require(success, "Call failed");
}
}
- 插件化合约:在模块化合约中,使用函数选择器映射到具体实现
go
mapping(bytes4 => address) public selectorToImplementation;
function delegateCall(bytes4 selector, bytes memory args) public {
六.自毁函数(Solidity 0.8.18 以后的版本是废弃了的,只做了解)
-
Solidity 0.8.18(2023年2月):根据 EIP-6049,selfdestruct 被标记为弃用。编译器会对其使用发出警告,建议开发者避免使用该函数。
-
以太坊 Cancun 升级:依据 EIP-6780,selfdestruct 的行为将被修改。除了在合约部署交易中立即调用的情况外,selfdestruct 将不再删除合约的代码和存储,仅会转移合约中的以太币余额。
即使你是低版本的合约,一般不建议使用合约自毁,建议在合约里面加开关,可以通过开关来暂停合约的业务功能。
以下合约自毁的内容可以阅读以下,了解,能应付面试即可,不用做过多的关注
合约删除的基本概念
selfdestruct 的作用
-
删除合约的代码
-
释放存储空间
-
将合约余额发送到指定地址。
语法
go
使用场景
-
合约生命周期结束
-
合约需要升级或替换
-
合约遭受攻击或存在安全问题
案例代码分析
基本示例
go
pragma solidity ^0.8.0;
contract SelfdestructCase {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
-
只有合约所有者可以调用 destroyContract
-
合约被销毁后,余额会转移到 recipient
合约删除的影响
-
合约地址仍然存在
-
事件和交易记录
selfdestruct 的优点与风险
-
优点
-
风险
-
存储清除:所有存储的数据都会被清除,无法恢复
使用 selfdestruct 的场景
合约生命周期结束
合约完成所有功能或达到预期目的时,销毁以释放存储资源
go
function terminateContract() public onlyOwner {
selfdestruct(payable(owner));
}
升级合约
在需要替换合约逻辑时,可以销毁旧合约,部署新合约
go
function upgradeContract(address payable newContract) public onlyOwner {
selfdestruct(newContract);
}
注意事项
-
使用权限控制
-
资金接收地址
-
避免与 Proxy 合约冲突
-
存在的问题
七.create2 底层原理与实现机制
CREATE2 是以太坊的一条 EVM 指令,用于部署智能合约。与传统的 CREATE 指令不同,CREATE2 允许通过计算得到合约地址,而不是依赖发送方的 nonce。这种方式使得合约地址在部署之前就可以被预测,方便一些高级用例,例如 工厂模式和合约钱包的预部署地址。
Create 和 Creat2
Create 指令:部署合约 EOA 地址 + nonce, 保证合约地址的一致性,例如要保证主网和测试网的合约地址一致,必须保证部署合约的 EOA 和 nonce 是一样的; create 指令的合约地址的计算公式如下:
address = keccak256(rlp(sender, nonce))[12:]
typescript
uint256 value = ac.setAndGet(200);
console.log(ac.data());
bytes4 selector = ac.getSelector();
console.logBytes4(selector);
}
}
常见的操作指令
【配图 1】原文此处为图片示意,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/Ojdudecv7o96bIxvh6qlwFphg7e
在合约里面如何判断地址是 EOA 地址还是合约地址
Extcodesize:
-
EXTCODESIZE
-
stateDB.getCodeSize()
size := extcodesize(addr)
- EXTCODESIZE
Assemble 高级应用
动态数组
直接操作内存中的动态数组。
- 代码案例
javascript
contract DynamicArray {
function createArray(uint256 size) public pure returns (uint256[] memory) {
assembly {
let ptr := mload(0x40) // 获取空闲内存位置
mstore(ptr, 0x20) // ABI 动态返回值的 offset
mstore(add(ptr, 0x20), size) // 设置数组长度
let data := add(ptr, 0x40) // 指向数组内容区域
for { let i := 0 } lt(i, size) { i := add(i, 1) } {
mstore(add(data, mul(i, 0x20)), i) // 存储数组元素
}
mstore(0x40, add(data, mul(size, 0x20))) // 更新空闲内存指针
return(ptr, add(0x40, mul(size, 0x20))) // 返回 ABI 编码后的数组
}
}
}
- Test
solidity
function test_assembly() public {
uint256[] memory aa = da.createArray(100000);
for (uint i = 0; i < aa.length; i++) {
console.log(aa[i]);
}
}
实现低级调用
通过 call 指令与其他合约交互。
go
let result := call(gas(), target, 0, add(data, 0x20), mload(data), 0, 0)
switch result
case 0 { revert(0, 0) }
default {
let returndata_size := returndatasize()
let returndata := mload(0x40)
returndatacopy(returndata, 0, returndata_size)
return(returndata, returndata_size)
}
}
}
}
注意事项
-
安全性
-
可读性:内联汇编代码较难阅读和维护,尽量仅用于性能关键或 Solidity 不支持的场景。
-
Gas 优化:虽然汇编可以减少部分冗余代码,但并非总能显著优化 Gas 消耗。需要根据具体场景分析。
-
版本限制:Solidity 0.8.x 开始增强了安全性检查,某些低级操作可能被限制。
总结
-
优点
-
缺点
-
适用场景
内联汇编是一把双刃剑,需谨慎使用。如果不是性能或功能瓶颈,应优先使用 Solidity 提供的高级语法。
九.OZ 代码库讲解
OpenZeppelin 是 Solidity 开发的标准工具库,覆盖了从代币实现到安全工具的各个方面。其模块化设计帮助开发者快速实现安全、标准化的智能合约,同时减少了开发和审计的成本。通过合理使用 OpenZeppelin 的模块,可以显著提高智能合约的开发效率和安全性。
Oz Github: https://github.com/Openzeppelin
Oz 提供的最为主流的两个代码库
-
openzeppelin-contracts:用于不可升级的普通合约
-
openzeppelin-contracts-upgradeable:用于支持可升级逻辑的合约
核心区别
【配图 2】原文此处为图片示意,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/Ojdudecv7o96bIxvh6qlwFphg7e
openzeppelin-contracts
-
access: 权限控制板块
-
account
-
finance
-
governance
-
metatx: ERC2771 的实现
-
proxy
-
Token
-
Utils
openzeppelin-contracts-upgradeable
模块和不可升级的差不多,部分合约代码在不可升级里面实现,upgradeable 里面不会再实现,而是做扩展实现
十.总结
-
call,delegatecall, staticcall
-
特殊全局变量
-
跨合约调用方式
-
Create 和 Create2
-
函数选择器
-
Assembly: 非必要的情况下不要使用,除非能进行大量代码的优化并且不引入安全性问题
-
tx.origin
-
tx.gasprice
-
block.number
-
block.timestamp
-
...
-
msg.sender
-
msg.value
-
...
-
所有数据被编码为 32 字节的倍数
-
动态数据(如字符串、数组)在编码中会包含指向其内容的偏移量
-
移除了动态类型的填充字节,编码结果更短。
-
适合用于哈希计算(如 keccak256)或签名场景
-
需要注意不同类型可能导致编码结果冲突。
-
用于构造函数调用数据
-
选择器是目标函数签名的前 4 字节(keccak256("functionName(arg1Type,arg2Type)") 的前 4 字节)。
-
等价于 abi.encodeWithSelector,但更直观
-
可直接传入函数签名字符串,省去手动计算选择器的步骤。
-
动态类型(如 string 和 bytes)的编码结果可能导致冲突
-
例如:abi.encodePacked("abc", "def") 和 abi.encodePacked("ab", "cdef") 的结果相同。
-
解决方法:加入额外信息,如长度。
-
确保选择器唯一,避免多个函数签名产生相同选择器。
-
keccak256(abi.encode(...)) 和 keccak256(abi.encodePacked(...)) 用途不同。
-
标准 ABI 用于数据验证,紧凑编码多用于签名和验证。
-
ABI 编码:setValue(uint256) 的 ABI 编码为 0x552410770000000000000000000000000000000000000000000000000000000000000042。
-
计算哈希:对 ABI 编码进行 keccak256 哈希计算得到 0x55241077...。
-
函数选择器:取哈希值的前 4 个字节,即 0x55241077。
-
不同函数可能会生成相同的函数选择器(哈希碰撞),但这种概率极低。
-
开发时避免设计过于相似的函数签名。
-
transfer(address,uint256) 和 transfer(uint256,address) 的选择器不同。
-
返回值类型不会影响函数选择器。
-
合约地址将不再关联任何代码
-
合约的所有存储数据将被清除
-
减少状态存储的消耗,释放 Gas(适用于存储退款机制)
-
虽然代码被移除,但地址仍然可以接收 ETH。
-
再次部署到相同地址是不可能的,除非使用 CREATE2。
-
合约的销毁不会影响之前的交易记录。
-
销毁事件可以通过链上交易日志(Event Logs)追踪。
-
节约存储成本: 删除合约后释放存储空间,可能获得部分 Gas 退款。
-
清除无用合约: 对废弃或不再需要的合约进行销毁
-
潜在误操作:如果代码设计不当,可能意外销毁合约。
-
恶意使用:攻击者可能利用 selfdestruct 将 ETH 强制发送到目标地址。
-
示例
go
-
确保只有受信任的用户(例如合约所有者)可以调用 selfdestruct
-
使用 modifier 或 require 语句进行访问限制。
-
确保 recipient 是有效的地址,避免丢失资金。
-
如果合约使用了代理模式(如 delegatecall),需要特别注意删除逻辑可能影响代理行为。
-
状态清理的复杂性:现有实现会立即清除合约存储,但在未来的状态分片中可能会增加复杂性
-
合约地址的可重用性:CREATE2 可预测的地址可能引发潜在安全问题
-
stateDB.getCodeSize() // 合约:size > 0
-
stateDB.getCodeSize() // EOA:size == 0
-
内联汇编直接操作底层资源,容易引发安全问题,如存储错位或越界访问
-
确保变量访问和内存操作严格符合预期。
-
精细控制 EVM 操作,提升灵活性
-
实现 Solidity 无法直接支持的功能
-
在性能关键场景中降低 Gas 消耗
-
可读性差,调试和维护困难
-
易引入安全漏洞,尤其是内存和存储操作
-
需要深入了解 EVM 和 Solidity 原理
-
性能优化:减少冗余逻辑或复杂循环
-
特殊需求:操作内存、存储或实现复杂的低级逻辑
-
与 EVM 指令直接交互:如 CALL、DELEGATECALL
-
AccessControl: 角色权限授权与检查
-
Ownable 和 Ownable2Step:Owner 的权限管理,包含 owner 权限初始化,权限转移,权限销毁等
-
对抽象账户钱包的封装,包括 EIP4337 和 EIP7702 的内容
-
Vesting Wallet 的模块
-
治理板块,时间锁控制等
-
透明代理
-
UUPS
-
信标代理
-
ERC1967 代理
-
最小代理
-
ERC20
-
ERC721
-
ERC1155
-
ERC6909
-
密码学库
-
address
-
create2
-
这里面很多很多工具类,自己可以下去看一下