[Web3] 智能合约 04: 高级开发实战与 OpenZeppelin

一.内容提要

  • 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 skipped

    Ran 1 test for test/CallFunctionContract.t.sol:CounterTest
    [PASS] test_callSetNumber() (gas: 42367)
    Logs:
    100
    101

    Suite 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 101

    Suite 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

  • 这里面很多很多工具类,自己可以下去看一下

相关推荐
笑容掩饰我的失落5 小时前
区块链从小白到精通(七):Solidity 语法精讲——数据类型、函数、修饰器与事件
算法·区块链·php·智能合约·哈希算法
OxYGC6 小时前
[Web3] 智能合约 03: 函数/事件/继承/接口/异常处理
web3·区块链·智能合约
笑容掩饰我的失落1 天前
区块链从小白到精通(八):从零实现一个标准 ERC-20 代币(完整可运行代码)
算法·区块链·php·智能合约·哈希算法
老谭说理财1 天前
股票市场全景评测与实战分析
区块链
OxYGC1 天前
[Web3] 智能合约 02: Solidity 基础语法
web3·区块链·智能合约
Bczheng12 天前
五十二.网络(2)--节点连接
区块链
Bczheng13 天前
五十一.网络(1)--加载节点地址和连接
区块链
OxYGC3 天前
[Web3] 基础第7篇: 说透比特币的基本框架与原理
web3·区块链·以太坊·比特币
混凝土拌意大利面3 天前
WEB3核心技术的一种实现:时间长河共识算法(TRC, Time-River Consensus)
web3·区块链·共识算法