solidity进阶

1、函数重载

1.1函数重载

solidity中允许函数进行重载,即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数,注意,silidity不允许修饰器重载。

我们定义两个都叫saySomething() 的函数,一个没有任何参数,输出Nothing;另一个接收一个string参数,输出这个string

solidity 复制代码
function saySomething() public pure returns(string memory){
		return("Nothing")
}
function saySomething(string memory something) public pure returns(string memory){
		return("something")
}

返回了不同的结果,被区分为不同的函数。

1.2实参匹配

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。如果出现多个匹配的

2、库合约

我们用Strings库合约的toHexString()`来演示两种使用库合约中函数的办法。

  1. 利用using for指令 指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数:
solidity 复制代码
// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
    // 库合约中的函数会自动添加为uint256型变量的成员
    return _number.toHexString();
}

2.通过库合约名称调用函数

solidity 复制代码
// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
    return Strings.toHexString(_number);
}

3、Import

方式 1:相对路径直接导入(最常用,本地文件)

solidity 复制代码
// 示例:当前文件在 ./contracts/ 下,导入同目录的 IERC20.sol 接口
import "./IERC20.sol";

// 示例:导入上级目录的 utils/Context.sol
import "../utils/Context.sol";

// 示例:导入子目录的 library/SafeMath.sol
import "./library/SafeMath.sol";

contract MyERC20 is IERC20 { // 直接使用导入的接口
    // ...
}

方式 2:按需导入指定内容

solidity 复制代码
// 示例:从 ERC20.sol 中仅导入 ERC20 合约和 IERC20 接口,其他内容不导入
import { ERC20, IERC20 } from "./ERC20.sol";

// 示例:从公共文件中导入结构体和枚举
import { TokenType, TokenInfo } from "./common/Structs.sol";

contract MyToken is ERC20 { // 直接使用按需导入的合约
    TokenInfo public tokenInfo; // 直接使用导入的结构体
}

方式 3:npm 包路径导入(生产主流,第三方库)

solidity 复制代码
// 示例1:导入 OpenZeppelin 的 ERC20 标准合约(最常用)
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// 示例2:导入 OpenZeppelin 的安全转账库
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// 示例3:导入 Uniswap V2 工厂合约接口
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

contract MyOpenZeppelinToken is ERC20 {
    constructor() ERC20("OZToken", "OZT") {
        _mint(msg.sender, 1000 * 10 ** decimals());
    }
}

4、接收ETH

4.1 接收ETH receive

receive()函数是合约收到ETH转账时被调用得函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable{...}.receive() 函数不能有任何的参数,不能返回任何值,必须包含external 和 payable.

当合约接收ETH的时候,receive() 会被触发。receive()最后不要执行太多的逻辑因为如果别人用send 和 transfer 方法发送ETH的话,gas会限制在 2300,receive()太复杂可能会触发Out of Gas报错。如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。

我们在receive() 里发送一个event,例如:

solidity 复制代码
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
    emit Received(msg.sender, msg.value);
}

4.2 回退函数fallback

fallback()函数会在调用合约不存在的函数时触发。可用于接收ETH,也可以用于代理合约proxy contract. fallback() 声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收接收ETH: fallback() external payable{...}.

我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:

solidity 复制代码
event fallbackCalled(address Sender, uint Value, bytes Data);

// fallback
fallback() external payable{
    emit fallbackCalled(msg.sender, msg.value, msg.data);
}

4.3 receive和fallback的区别

receive和fallback都能够用于接收ETH,他们触发的规则如下:

scss 复制代码
触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。

receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。

案例

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// receive/Fallback  案例
contract ReceiveFallbackTest{
    //记录receive 的触发
    event ReceiveTriggered(address indexed sender, uint256 amount);

    //记录fallback 的触发
    event FaballTriggered(address indexed sender, uint256 amount, bytes data);

    //无calldata的转账
    receive() external payable { 
        emit ReceiveTriggered(msg.sender, msg.value); //触发事件,打印发送者和ETH金额
    }

    fallback() external payable {
        emit FaballTriggered(msg.sender, msg.value, msg.data); //打印发送者、金额、附带的calldata
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }


}

5、发送ETH

call没有gas限制,最为灵活,是最提倡的方法

用法是接收方地址.call{value: 发送ETH数额}("")。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。 call()如果转账失败,不会revert。 call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下。

结合接收ETH的案例

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract CallTest{
    //事件: 记录call调用结果
    event CallResult(address indexed target,bool success, uint256 sendAmount, bytes data);

    //场景一:用call纯转ETH(触发目标合约receive)
    //参数:_target 目标合约地址(触发目标合约的receive)
    //参数:_amount 转ETH金额
    function callTransferETH(address payable _target, uint256 _amount) external payable {
        //核心:call纯转ETH,calldata传空字符串
        (bool success, bytes memory data) = _target.call{value:_amount}("");

        //必须判断success,失败则回滚
        require(success,"Call transfer ETH failed");

        //触发事件,记录调用结果
        emit CallResult(_target,success, _amount,data);
    }

    // 场景一:用call纯转ETH(触发目标合约fallback)
    // 参数:_target 目标合约地址;_amount 转ETH金额;_data 自定义calldata(比如0x1234)
    function callTransferETHWithData(address payable _target, uint256 _amount, bytes calldata _data) external payable {
        // 核心:call带calldata转ETH,传入自定义_data
        (bool success, bytes memory data) = _target.call{value: _amount}(_data);
        
        require(success, "Call transfer ETH with data failed");

        emit CallResult(_target, success, _amount, data);
    }

    // 辅助函数:查询当前CallTest合约的ETH余额(测试后验证)
    function getCallContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

6、调用其他合约

4个调用合约的例子

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract OtherContract {
    uint256 private _x = 0;
    //收到ETh的事件,记录amount 和 gas
    event Log(uint amount, uint gas);

    //返回合约ETH余额
    function getBalance() view public returns (uint) {
        return address(this).balance;
    }

    //可以调整状态变量_x的函数,并且可以往合约转ETH(payable)
    function setX(uint256 x) external payable {
        _x = x;
        //如果转入ETH, 则释放Log事件
        if(msg.value > 0 ){
            emit Log(msg.value, gasleft());
        }
    }

    //读取_x
    function getX() external view returns(uint x){
        x = _x;
    }
}

contract CallContract{
    //1.传入合约地址
    //复制OtherContract合约的地址,填入callSetX函数的参数中,成功调用后,调用OtherContract合约中的getX验证x变为123
    function callSetX(address _Address,uint256 x) external {
        OtherContract(_Address).setX(x);
    }
    //2.传入合约变量
    //复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值
    function callCetX(OtherContract _Address) external view returns(uint x) {
        x = _Address.getX();
    }
    //3.创建合约变量
    //复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值
    function callGetX2(address _Address) external view returns (uint x){
        OtherContract oc = OtherContract(_Address);
        x = oc.getX();
    }
    //4.调用合约并发送ETH
    //复制OtherContract合约的地址,填入setXTransferETH函数的参数中,并转入10ETH
    function setXtransferETH(address otherContract,uint256 x) payable external {
        OtherContract(otherContract).setX{value:msg.value}(x);
    }
}

案例

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 被调用合约:简单计算器
contract Calculator {
    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }
}

contract CalculatorCaller {
    Calculator public calculator;

    constructor(address _calcAddress) {
        calculator = Calculator(_calcAddress);
    }

    // 调用计算器的 add 函数,直接返回结果
    function callAdd(uint256 a, uint256 b) external view returns (uint256) {
        return calculator.add(a, b);
    }
}

7、call

call是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool,bytes memory),分别对应call是否成功以及函数的返回值。

​ call是solidity官方推荐通过触发fallback或者receive函数发送ETH的方法。

​ 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数。

​ 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。

call的使用规则

ini 复制代码
目标合约地址.call(字节码);

​ 字节码利用结构化编程函数abi.encodeWithSignature获得:

arduino 复制代码
abi.encodeWithSignature("函数签名",逗号分隔的具体参数)

函数签名为 "函数名(逗号分隔的参数类型)"。 例如abi.encodeWithSignature("f(uint256,address)",_x,_addr)

另外call在调用合约时可以指定交易发送的ETH数额和gas数额:

css 复制代码
目标合约地址.call{value:发送数额,gas:gas数额}(字节码);

案例

目标合约

solidity 复制代码
contract OtherContract {
//                    返回值含义                   单位     类型
//msg.value	          调用函数时附带转入的 ETH 数量	   wei	 uint256
//address(this).balance	     合约当前的 ETH 余额	  wei	uint256
//gasleft()         执行到当前行的剩余可使用 Gas 量    gas 	uint256

    uint256 private _x = 0; // 状态变量x
    // 收到eth的事件,记录amount和gas
    event Log(uint amount, uint gas);X
    
    fallback() external payable{}

    // 返回合约ETH余额
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)
    function setX(uint256 x) external payable{
        _x = x;
        // 如果转入ETH,则释放Log事件
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // 读取x
    function getX() external view returns(uint x){
        x = _x;
    }
}

利用call调用目标合约

solidity 复制代码
contract call {
    // 定义Response事件,输出call返回的结果success和data
    event Response(bool success, bytes data);

    function callSetX(address payable _addr, uint256 x) public payable {
    // call setX(),同时可以发送ETH
    (bool success, bytes memory data) = _addr.call{value: msg.value}(
        abi.encodeWithSignature("setX(uint256)", x)
    );

    emit Response(success, data); //释放事件
}
    function callGetX(address _addr) external returns(uint256){
    // call getX()
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("getX()")
    );

    emit Response(success, data); //释放事件
    return abi.decode(data, (uint256));
}

function callNonExist(address _addr) external{
    // call 不存在的函数
    (bool success, bytes memory data) = _addr.call(
        abi.encodeWithSignature("foo(uint256)")
    );

    emit Response(success, data); //释放事件
}

}
1.调用setX函数

调用callSetX把状态变量_x改为5,参数为OtherContract地址和5,由于目标函数setX()没有返回值,因此Response事件输出的data为0x,也就是空。

2.调用getX函数
4.调用不存在的函数

上面例子中,我们call了不存在的foo函数。call仍能执行成功,并返回success,但其实调用的目标合约fallback函数。

8、delegatecall

特性 call(普通外部调用) delegatecall(委托调用)
存储(Storage) 执行目标合约的代码,读写目标合约的存储 执行目标合约的代码,读写当前合约的存储
上下文(Context) msg.sender = 当前合约地址;msg.value 传递 msg.sender = 原始调用者地址;msg.value 保留
代码归属 执行的是目标合约的代码,逻辑归属目标合约 执行的是目标合约的代码,逻辑归属当前合约
核心用途 普通调用外部合约功能(如调用 ERC20 转账) 代码复用、可升级合约(代理模式)
存储布局要求 无强制要求(目标合约自有存储) 严格匹配(当前合约与逻辑合约存储槽位需一致)

1.逻辑合约(被调用方:提供代码逻辑)

solidity 复制代码
pragma solidity ^0.8.20;

// 逻辑合约:只提供代码,不存储数据
contract LogicContract {
    // 注意:变量的顺序/类型必须和调用合约完全匹配(存储槽位对应)
    uint public num;       // 存储槽位 0
    address public sender; // 存储槽位 1

    // 修改状态的函数
    function setVars(uint _num) public {
        num = _num;
        // delegatecall 中,msg.sender 是「外部调用者」,而非调用合约地址
        sender = msg.sender;
    }

    // 纯计算函数(无状态修改)
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }
}

2.调用合约(调用方:提供存储,借用逻辑)

solidity 复制代码
pragma solidity ^0.8.20;

// 调用合约:存储数据,通过 delegatecall 借用 LogicContract 的逻辑
contract CallerContract {
    // 存储布局必须和 LogicContract 完全一致!(顺序、类型)
    uint public num;       // 存储槽位 0
    address public sender; // 存储槽位 1
    address public logicAddr; // 保存逻辑合约地址

    // 设置逻辑合约地址
    function setLogicAddr(address _logic) public {
        logicAddr = _logic;
    }

    // 用 delegatecall 调用 LogicContract 的 setVars 函数
    function delegateSetVars(uint _num) public returns (bool, bytes memory) {
        // delegatecall 语法:目标地址.delegatecall(ABI编码的函数调用)
        (bool success, bytes memory data) = logicAddr.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
        return (success, data); // 返回调用结果和数据
    }

    // 用 delegatecall 调用 LogicContract 的 add 函数
    function delegateAdd(uint a, uint b) public returns (bool, bytes memory) {
        (bool success, bytes memory data) = logicAddr.delegatecall(
            abi.encodeWithSignature("add(uint256,uint256)", a, b)
        );
        return (success, data);
    }
}

3. 测试步骤与效果

  1. 部署 LogicContract,记录其地址(如 0x123...);
  2. 部署 CallerContract,调用 setLogicAddr(0x123...) 关联逻辑合约;
  3. 调用 delegateSetVars(100),然后查询 CallerContractnum → 结果为 100(数据存在 CallerContract 中);
  4. 查询 CallerContractsender → 结果为你的钱包地址(而非 CallerContract 地址,保留了原始上下文);
  5. 调用 delegateAdd(5, 3),解码返回的 data → 结果为 8(复用了 LogicContract 的计算逻辑)。

4.代理合约案例

solidity 复制代码
contract Proxy{
	address public implementation;
	address public owner;
	
	constructor(address _logic){
		implementation = _logic;
		owner = msg.sender;
	}
	
	modifier onlyOnwner(){
	 	require(msg.sender == owner, "Proxy: not owner");
	 	_;
	}
	
	function upgradeTo(address _newLogic) public onlyOwner{
		implementation = _newLogic;
	}
	
	//回退函数:调用代理合约不存在的函数时,自动转发给逻辑合约
	fallback() external payable {
		(bool success,) implementation.delegatecall(msg.data);
        require(success, "Proxy: delegatecall failed");
	}
	
    // 接收 ETH
    receive() external payable {}
}

pragma solidity ^0.8.20;

// 逻辑合约 V1:实现基础功能
contract LogicV1 {
    // 存储布局必须和 Proxy 完全匹配(新增变量只能加在末尾)
    address public implementation; // 存储槽位 0
    address public owner;          // 存储槽位 1
    uint public count;             // 存储槽位 2(新增状态)

    // 计数+1
    function increment() public {
        count += 1;
    }

    // 获取计数
    function getCount() public view returns (uint) {
        return count;
    }
}

pragma solidity ^0.8.20;

// 逻辑合约 V2:在 V1 基础上新增功能
contract LogicV2 {
    // 存储布局必须和 Proxy/LogicV1 完全一致!
    address public implementation; // 存储槽位 0
    address public owner;          // 存储槽位 1
    uint public count;             // 存储槽位 2

    // 保留 V1 功能
    function increment() public {
        count += 1;
    }

    // 新增功能:计数-1
    function decrement() public {
        require(count > 0, "LogicV2: count is zero");
        count -= 1;
    }

    // 保留 V1 功能
    function getCount() public view returns (uint) {
        return count;
    }
}
  1. 部署 LogicV1,得到地址 LogicV1Addr
  2. 部署 Proxy,传入 LogicV1Addr 作为初始逻辑合约;
  3. 调用 Proxy.increment() → 调用 Proxy.getCount() 得到 1(状态存在 Proxy 中);
  4. 部署 LogicV2,得到地址 LogicV2Addr
  5. 调用 Proxy.upgradeTo(LogicV2Addr)(仅 owner 可操作),完成升级;
  6. 调用 Proxy.decrement() → 调用 Proxy.getCount() 得到 0(新增功能生效,状态未丢失)。

9、在合约中创建新合约(工厂合约)

工厂合约是专门用于部署其他合约(称为「子合约 / 目标合约」)的智能合约,它的核心职责是:

  1. 封装子合约的部署逻辑(比如用 new 关键字);
  2. 对外提供统一的部署接口;
  3. 通常会记录所有已部署的子合约地址(方便管理 / 追溯)。

create的用法很简单,就是new一个合约,并传入新合约构造函数所需的参数:

Contract x = new Contract{value: _value}(params) 其中Contract是要创建的合约名,x是合约对象(地址),如果构造函数是payable,可以创建时转入_value数量的ETH,params是新合约构造函数的参数。

1.目标合约

核心功能:存储用户名、记录合约部署者(即工厂合约地址),支持修改用户名,无构造函数。

solidity 复制代码
pragma solidity ^0.8.20;

// 用户信息合约(无构造函数,基础状态读写)
contract UserInfoContract {
    // 状态变量:用户名 + 部署者(工厂合约地址)
    string public userName;
    address public deployer; // 部署该合约的工厂合约地址

    // 初始化函数:设置用户名 + 记录部署者(首次调用)
    function initUser(string memory _userName) public {
        // 仅允许首次初始化(避免重复修改部署者)
        require(deployer == address(0), "UserInfo: already initialized");
        userName = _userName;
        deployer = msg.sender; // msg.sender = 工厂合约地址
    }

    // 修改用户名(任意地址可调用,仅演示基础功能)
    function updateUserName(string memory _newName) public {
        userName = _newName;
    }

    // 批量获取信息(简化调用)
    function getUserInfo() public view returns (string memory, address) {
        return (userName, deployer);
    }
}

2.工厂合约

专门部署 UserInfoContract 的合约

solidity 复制代码
pragma solidity ^0.8.20;

import "./UserInfoContract.sol";

// 典型的工厂合约结构
contract UserInfoFactory {
    // 特征1:记录所有已部署的子合约地址(可追溯、易管理)
    address[] public deployedUserContracts;

    // 特征2:对外提供统一的部署接口(封装部署逻辑)
    function deployUserContract() public returns (address) {
        // 核心:用 new 部署子合约(工厂合约的核心动作)
        UserInfoContract newUserContract = new UserInfoContract();
        // 特征3:记录新部署的子合约地址
        deployedUserContracts.push(address(newUserContract));
        // 特征4:返回子合约地址(方便调用方直接使用)
        return address(newUserContract);
    }

    // 特征5:辅助管理函数(查看部署数量)
    function getDeployedCount() public view returns (uint) {
        return deployedUserContracts.length;
    }
}

步骤 1:部署工厂合约

步骤 2:用工厂部署 UserInfoContract

  1. UserInfoFactory 实例中,找到 deployUserContract 函数,点击「transact」;
  2. 交易成功后,复制返回的 UserInfoContract 地址(如 0x8A791620dd6260079BF849Dc5567aDC3F2FdC318);
  3. 验证:调用 UserInfoFactorygetDeployedCount(),返回 1(表示已部署 1 个合约);调用 deployedUserContracts(0),返回上述复制的地址(验证记录成功)。

步骤 3:操作并验证 UserInfoContract

  1. 在「Deploy & Run Transactions」的「At Address」输入框粘贴步骤 4 复制的地址,「Contract」下拉框选择 UserInfoContract,点击「At Address」加载合约实例;
  2. 初始化用户信息
    • 调用 initUser 函数,参数输入你的用户名(如 "Alice"),点击「transact」;
    • 调用 getUserInfo 函数,点击「call」,返回结果为 ("Alice", 工厂合约地址)(验证部署者和用户名设置成功);
  3. 修改用户名
    • 调用 updateUserName 函数,参数输入 "Bob",点击「transact」;
    • 再次调用 getUserInfo,返回 ("Bob", 工厂合约地址)(验证用户名修改成功);
  4. 重复初始化验证 :再次调用 initUser("Charlie"),交易会失败(提示 UserInfo: already initialized,验证初始化仅允许一次)。

3.工厂合约的 5 个核心特征(对应案例)

特征 案例中的体现 实际价值
1. 封装部署逻辑 所有部署 UserInfoContract 的逻辑都在 deployUserContract 调用方无需知道「怎么部署」,只需调用函数即可
2. 批量部署 多次调用 deployUserContract 可创建多个 UserInfoContract 实例 一键批量生成同款合约,无需重复部署工厂
3. 可追溯 deployedUserContracts 数组记录所有子合约地址 随时查看 / 管理所有已部署的子合约
4. 标准化 所有子合约都基于同一版 UserInfoContract 代码 避免手动部署时的代码版本不一致
5. 简化调用 返回子合约地址,调用方直接用 At Address 加载 无需手动复制字节码、处理底层部署细节

10、create2

1.CREATE2 vs CREATE(new)核心对比

特性 CREATE2 CREATEnew 底层)
地址生成依据 工厂地址 + salt + 子合约字节码哈希 工厂地址 + 工厂 nonce(部署次数)
地址可预测性 ✅ 部署前可精确计算 ❌ 不可预测(nonce 随部署变化)
Solidity 封装语法 new 合约名{salt: _salt}() new 合约名()
核心优势 地址确定、提前规划 简单、无需自定义 salt
适用场景 需提前知地址的场景(跨链、DEX) 普通批量部署(无地址预判需求)

2.案例

  1. 子合约(保持不变,复用之前的 UserInfoContract

  2. CREATE2 工厂合约(核心)

solidity 复制代码
pragma solidity ^0.8.20;

import "./UserInfoContract.sol";

// CREATE2 版本的用户信息合约工厂(支持预计算地址)
contract UserInfoFactoryCreate2 {
    // 记录已部署的子合约(salt => 合约地址),方便追溯
    mapping(uint256 => address) public saltToContract;

    // ========== 核心1:用 CREATE2 部署子合约(高级封装方式) ==========
    function deployWithCREATE2(uint256 _salt) public returns (address) {
        // 语法:new 合约名{salt: 盐值}() ------ 底层调用 CREATE2
        UserInfoContract newUserContract = new UserInfoContract{salt: bytes(_salt)}();
        address contractAddr = address(newUserContract);
        
        // 记录 salt 和合约地址的映射
        saltToContract[_salt] = contractAddr;
        
        return contractAddr;
    }

    // ========== 核心2:预计算合约地址(部署前就能知道地址) ==========
    function computeContractAddress(uint256 _salt) public view returns (address) {
        // 步骤1:获取子合约的初始化字节码(编译后生成,固定值)
        bytes memory bytecode = type(UserInfoContract).creationCode;
        // 步骤2:计算字节码的 keccak256 哈希
        bytes32 bytecodeHash = keccak256(bytecode);
        
        // 步骤3:按 CREATE2 地址规则计算(核心公式)
        address predictedAddr = address(
            uint160( // 转成 160 位地址格式
                uint256(
                    keccak256(
                        abi.encodePacked(
                            bytes1(0xff), // 固定前缀
                            address(this), // 工厂合约地址
                            _salt, // 盐值
                            bytecodeHash // 子合约字节码哈希
                        )
                    )
                )
            )
        );
        
        return predictedAddr;
    }
}

步骤 1:部署 CREATE2 工厂合约

步骤 3:预计算子合约地址(核心验证)

  1. 在工厂合约实例中,找到 computeContractAddress 函数,输入 salt 值 12345,点击「call」;
  2. 记录返回的预计算地址 (比如 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199);
  3. 此时该地址还未部署任何合约(只是预计算结果)。

步骤 4:用 CREATE2 部署子合约(验证地址一致)

  1. 在工厂合约实例中,找到 deployWithCREATE2 函数,输入相同的 salt 值 12345,点击「transact」;
  2. 交易成功后,复制返回的实际部署地址
  3. 对比「预计算地址」和「实际部署地址」------ 两者完全一致(CREATE2 核心优势体现)。

步骤 5:验证子合约功能

  1. 在「At Address」输入实际部署地址,选择 UserInfoContract,加载合约实例;
  2. 调用 initUser("Charlie"),再调用 getUserInfo() → 返回 ("Charlie", 工厂合约地址)
  3. 调用 updateUserName("Dave") → 验证用户名修改成功。

11、删除合约

1、销毁合约的核心原理(selfdestruct

Solidity 中销毁合约的唯一方式是调用 selfdestruct(address payable recipient)(旧称 suicide,已废弃),这是 EVM 级别的低级操作,核心特性如下:

  1. 核心行为
    • 永久销毁当前合约的代码存储数据(所有状态变量被清空,无法恢复);
    • 将合约账户中剩余的所有 ETH 转移到指定的 recipient 地址;
    • 合约地址不会消失,但地址对应的代码 / 存储被清空,后续调用该地址的任何函数都会失败。
  2. 权限要求 :仅能由合约自身调用(或通过合约内的函数间接调用,需加权限控制);
  3. 不可逆性:销毁后无法恢复,是永久性操作,需谨慎使用。

2.案例

可销毁的子合约(DestructibleUserInfo

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DestructibleUserInfo {
    string public userName;
    address public deployer;
    bool public isDestroyed;

    modifier onlyDeployer() {
        require(msg.sender == deployer, "Only deploy can call");
        _;
    }

    modifier notDestroyed() {
        require(!isDestroyed, "Contract already destroyed");
        _;
    }

    function updateUserName(string memory _newName) public notDestroyed {
        userName = _newName;
    }

    function getUserInfo() public view returns (string memory, address){
        return (userName,deployer);
    }

    function destroyContract(address payable  _recipient) public onlyDeployer notDestroyed {
        isDestroyed = true;
        selfdestruct(_recipient);
    }

    receive() external payable {}
}
  1. 管理销毁的工厂合约(DestructibleUserInfoFactory
solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./DestructibleUserInfo.sol";

// 工厂合约:部署+销毁 可销毁的用户信息合约
contract DestructibleUserInfoFactory {
    // 记录已部署的子合约地址(防止销毁后误操作)
    address[] public deployedContracts;
    // 标记子合约是否已销毁
    mapping(address => bool) public isContractDestroyed;

      // 部署并初始化子合约(让工厂成为 deployer)
    function deployAndInitContract(string memory _userName) public returns (address) {
        DestructibleUserInfo newContract = new DestructibleUserInfo();
        // 工厂合约调用 initUser → 子合约的 deployer = 工厂地址
        newContract.initUser(_userName); 
        deployedContracts.push(address(newContract));
        return address(newContract);
    }

    // 工厂触发子合约销毁(核心:调用子合约的destroyContract)
    function destroyUserContract(address payable _contractAddr, address payable _recipient) public {
        // 检查合约是否由本工厂部署
        bool isDeployed = false;
        for (uint i = 0; i < deployedContracts.length; i++) {
            if (deployedContracts[i] == _contractAddr) {
                isDeployed = true;
                break;
            }
        }
        require(isDeployed, "Contract not deployed by this factory");
        // 检查合约未被销毁
        require(!isContractDestroyed[_contractAddr], "Contract already destroyed");

        // 现在类型匹配,编译通过
        DestructibleUserInfo(_contractAddr).destroyContract(_recipient);
        // 标记销毁状态
        isContractDestroyed[_contractAddr] = true;
    }

    // 给子合约转ETH(测试销毁时的ETH转移)
    function sendEthToContract(address _contractAddr) public payable {
        require(msg.value > 0, "ETH amount must be > 0");
        (bool success, ) = _contractAddr.call{value: msg.value}("");
        require(success, "ETH transfer failed");
    }
}

步骤 1:部署工厂合约

  1. 在 Remix 中选择 DestructibleUserInfoFactory 合约
  2. 点击「Deploy」按钮部署合约
  3. 等待交易确认,获得工厂合约地址

步骤 2:调用工厂的 deployAndInitContract

  1. 在已部署的工厂合约界面,找到 deployAndInitContract 函数
  2. 输入参数 _userName: "TestFactory"
  3. 点击「transact」执行交易
  4. 等待交易成功,复制返回的子合约地址

步骤 3:验证子合约 deployer

  1. 在 Remix 的「At Address」输入框中粘贴子合约地址
  2. 从下拉菜单中选择 DestructibleUserInfo 合约类型
  3. 点击「At Address」按钮加载子合约
  4. 调用子合约的 deployer() 函数
  5. 验证返回的地址是否为工厂合约地址

步骤 4:测试工厂销毁子合约

4.1 向子合约发送 ETH

  1. 调用工厂的 sendEthToContract 函数
  2. 参数 _contractAddr: 粘贴子合约地址
  3. 在 Value 字段输入 1
  4. 单位选择 ether
  5. 点击「transact」执行交易

4.2 销毁子合约

  1. 调用工厂的 destroyUserContract 函数
  2. 参数 _contractAddr: 子合约地址(payable 类型)
  3. 参数 _recipient: 你的钱包地址
  4. 点击「transact」执行交易
  5. 等待交易成功确认

12、ABI编码解码

ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。

Solidity中,ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode,用于解码abi.encode的数据。

ABI编码

我们将编码4个变量,他们的类型分别是uint256(别名 uint), address, string, uint256[2]

solidity 复制代码
uint x = 10;
address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
string name = "0xAA";
uint[2] array = [5, 6];
abi.encode

将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是abi.encode

solidity 复制代码
function encode() public view returns(bytes memory result) {
    result = abi.encode(x, addr, name, array);
}

编码的结果为0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,详细解释下编码的细节:

arduino 复制代码
000000000000000000000000000000000000000000000000000000000000000a    // x
0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71    // addr
00000000000000000000000000000000000000000000000000000000000000a0    // name 参数的偏移量
0000000000000000000000000000000000000000000000000000000000000005    // array[0]
0000000000000000000000000000000000000000000000000000000000000006    // array[1]
0000000000000000000000000000000000000000000000000000000000000004    // name 参数的长度为4字节
3078414100000000000000000000000000000000000000000000000000000000    // name

其中 name 参数被转换为UTF-8的字节值 0x30784141,在 abi 编码规范中,string 属于动态类型 ,动态类型的参数需要借助偏移量进行编码,可以参考动态类型的使用。由于 abi.encode 会将每个参与编码的参数元素(包括偏移量,长度)都填充为32字节(evm字长为32字节),所以可以看到编码后的数据中有很多填充的 0 。

abi.encodePacked

将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多0省略。比如,只用1字节来编码uint8类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash时。需要注意,abi.encodePacked因为不会做填充,所以不同的输入在拼接后可能会产生相同的编码结果,导致冲突,这也带来了潜在的安全风险。

solidity 复制代码
function encodePacked() public view returns(bytes memory result) {
    result = abi.encodePacked(x, addr, name, array);
}

编码的结果为0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006,由于abi.encodePacked对编码进行了压缩,长度比abi.encode短很多。

abi.encodeWithSignature

abi.encode功能类似,只不过第一个参数为函数签名,比如"foo(uint256,address,string,uint256[2])"。当调用其他合约的时候可以使用。

solidity 复制代码
function encodeWithSignature() public view returns(bytes memory result) {
    result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}

编码的结果为0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,等同于在abi.encode编码结果前加上了4字节的函数选择器1

abi.encodeWithSelector

abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。

solidity 复制代码
function encodeWithSelector() public view returns(bytes memory result) {
    result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}

编码的结果为0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000,与abi.encodeWithSignature结果一样。

ABI解码

abi.decode

abi.decode用于解码abi.encode生成的二进制编码,将它还原成原本的参数。

solidity 复制代码
function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
    (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
}

代码

solidity 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract ABIEncode{
    uint x = 10;
    address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    string name = "0xAA";
    uint[2] array = [5, 6]; 

    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }

    function encodePacked() public view returns(bytes memory result) {
        result = abi.encodePacked(x, addr, name, array);
    }

    function encodeWithSignature() public view returns(bytes memory result) {
        result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
    }

    function encodeWithSelector() public view returns(bytes memory result) {
        result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    }
    function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
        (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
    }
}

13.选择器

1.msg.data

msg.dataSolidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)。

在下面的代码中,我们可以通过Log事件来输出调用mint函数的calldata

solidity 复制代码
// event 返回msg.data
event Log(bytes data);

function mint(address to) external{
    emit Log(msg.data);
}

当参数为0x2c44b726ADF1963cA47Af88B284C06f30380fC78时,输出的calldata

复制代码
0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78

这段很乱的字节码可以分成两部分:

复制代码
前4个字节为函数选择器selector:
0x6a627842

后面32个字节为输入的参数:
0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78

其实calldata就是告诉智能合约,我要调用哪个函数,以及参数是什么。

2.method id、selector和函数签名

method id定义为函数签名Keccak哈希后的前4个字节,当selectormethod id相匹配时,即表示调用该函数,那么函数签名是什么?

函数签名,为"函数名(逗号分隔的参数类型)"举个例子,上面代码中mint的函数签名为"mint(address)"。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。

注意 ,在函数签名中,uintint要写为uint256int256

我们写一个函数,来验证mint函数的method id是否为0x6a627842。大家可以运行下面的函数,看看结果。

solidity 复制代码
function mintSelector() external pure returns(bytes4 mSelector){
    return bytes4(keccak256("mint(address)"));
}

结果正是0x6a627842

由于计算method id时,需要通过函数名和函数的参数类型来计算。在Solidity中,函数的参数类型主要分为:基础类型参数,固定长度类型参数,可变长度类型参数和映射类型参数。

基础类型参数

solidity中,基础类型的参数有:uint256(uint8, ... , uint256)、bool, address等。在计算method id时,只需要计算bytes4(keccak256("函数名(参数类型1,参数类型2,...)"))。例如,如下函数,函数名为elementaryParamSelector,参数类型分别为uint256bool。所以,只需要计算bytes4(keccak256("elementaryParamSelector(uint256,bool)"))便可得到此函数的method id

solidity 复制代码
// elementary(基础)类型参数selector
    // 输入:param1: 1,param2: 0
    // elementaryParamSelector(uint256,bool) : 0x3ec37834
    function elementaryParamSelector(uint256 param1, bool param2) external returns(bytes4 selectorWithElementaryParam){
        emit SelectorEvent(this.elementaryParamSelector.selector);
        return bytes4(keccak256("elementaryParamSelector(uint256,bool)"));
    }
固定长度类型参数

固定长度的参数类型通常为固定长度的数组,例如:uint256[5]等。例如,如下函数fixedSizeParamSelector的参数为uint256[3]。因此,在计算该函数的method id时,只需要通过bytes4(keccak256("fixedSizeParamSelector(uint256[3])"))即可。

solidity 复制代码
// fixed size(固定长度)类型参数selector
    // 输入: param1: [1,2,3]
    // fixedSizeParamSelector(uint256[3]) : 0xead6b8bd
    function fixedSizeParamSelector(uint256[3] memory param1) external returns(bytes4 selectorWithFixedSizeParam){
        emit SelectorEvent(this.fixedSizeParamSelector.selector);
        return bytes4(keccak256("fixedSizeParamSelector(uint256[3])"));
    }
可变长度类型参数

可变长度参数类型通常为可变长的数组,例如:address[]uint8[]string等。例如,如下函数nonFixedSizeParamSelector的参数为uint256[]string。因此,在计算该函数的method id时,只需要通过bytes4(keccak256("nonFixedSizeParamSelector(uint256[],string)"))即可。

solidity 复制代码
// non-fixed size(可变长度)类型参数selector
    // 输入: param1: [1,2,3], param2: "abc"
    // nonFixedSizeParamSelector(uint256[],string) : 0xf0ca01de
    function nonFixedSizeParamSelector(uint256[] memory param1,string memory param2) external returns(bytes4 selectorWithNonFixedSizeParam){
        emit SelectorEvent(this.nonFixedSizeParamSelector.selector);
        return bytes4(keccak256("nonFixedSizeParamSelector(uint256[],string)"));
    }
映射类型参数

映射类型参数通常有:contractenumstruct等。在计算method id时,需要将该类型转化成为ABI类型。

例如,如下函数mappingParamSelectorDemoContract需要转化为address,结构体User需要转化为tuple类型(uint256,bytes),枚举类型School需要转化为uint8。因此,计算该函数的method id的代码为bytes4(keccak256("mappingParamSelector(address,(uint256,bytes),uint256[],uint8)"))

solidity 复制代码
contract DemoContract {
    // empty contract
}

contract Selector{
    // Struct User
    struct User {
        uint256 uid;
        bytes name;
    }
    // Enum School
    enum School { SCHOOL1, SCHOOL2, SCHOOL3 }
    ...
    // mapping(映射)类型参数selector
    // 输入:demo: 0x9D7f74d0C41E726EC95884E0e97Fa6129e3b5E99, user: [1, "0xa0b1"], count: [1,2,3], mySchool: 1
    // mappingParamSelector(address,(uint256,bytes),uint256[],uint8) : 0xe355b0ce
    function mappingParamSelector(DemoContract demo, User memory user, uint256[] memory count, School mySchool) external returns(bytes4 selectorWithMappingParam){
        emit SelectorEvent(this.mappingParamSelector.selector);
        return bytes4(keccak256("mappingParamSelector(address,(uint256,bytes),uint256[],uint8)"));
    }
    ...
}

3.使用selector

我们可以利用selector来调用目标函数。例如我想调用elementaryParamSelector函数,我只需要利用abi.encodeWithSelectorelementaryParamSelector函数的method id作为selector和参数打包编码,传给call函数:

scss 复制代码
// 使用selector来调用函数
    function callWithSignature() external{
	...
        // 调用elementaryParamSelector函数
        (bool success1, bytes memory data1) = address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0));
	...
    }
相关推荐
devmoon3 天前
使用 Remix IDE 在 Polkadot Hub 测试网部署 ERC-20 代币(新手完整实战教程)
web3·区块链·智能合约·solidity·remix·polkadot·erc-20
devmoon3 天前
智能合约实战 - 水龙头哪里领和创建第一个智能合约地址
web3·区块链·测试用例·智能合约·solidity
devmoon4 天前
选择基于rust的以太坊虚拟机,还是基于RISC-V的虚拟机?一文了解他们的部署差异和部署机制
web3·区块链·智能合约·solidity·polkadot
devmoon11 天前
如何使用 Web3.py 与 Polkadot Hub 进行交互
web3·区块链·智能合约·交互·web3.py·solidity·polkadot
爱兜圈12 天前
写给 Web3 小白:一文看懂 AMM 原理与极简代码实现
web3·区块链·智能合约·solidity
gunner625 天前
LazyMinting是如何实现的?
solidity
Rockbean25 天前
3分钟Solidity: 11.11 抢先交易Front Running
web3·智能合约·solidity
DICOM医学影像1 个月前
3. go语言从零实现以太坊客户端 - 查询合约中账户余额
golang·区块链·智能合约·solidity·以太坊·web3.0
Rockbean1 个月前
3分钟Solidity: 11.10 蜜罐
web3·智能合约·solidity