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()`来演示两种使用库合约中函数的办法。
- 利用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. 测试步骤与效果
- 部署
LogicContract,记录其地址(如0x123...); - 部署
CallerContract,调用setLogicAddr(0x123...)关联逻辑合约; - 调用
delegateSetVars(100),然后查询CallerContract的num→ 结果为100(数据存在 CallerContract 中); - 查询
CallerContract的sender→ 结果为你的钱包地址(而非 CallerContract 地址,保留了原始上下文); - 调用
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;
}
}
- 部署
LogicV1,得到地址LogicV1Addr; - 部署
Proxy,传入LogicV1Addr作为初始逻辑合约; - 调用
Proxy.increment()→ 调用Proxy.getCount()得到1(状态存在 Proxy 中); - 部署
LogicV2,得到地址LogicV2Addr; - 调用
Proxy.upgradeTo(LogicV2Addr)(仅 owner 可操作),完成升级; - 调用
Proxy.decrement()→ 调用Proxy.getCount()得到0(新增功能生效,状态未丢失)。
9、在合约中创建新合约(工厂合约)
工厂合约是专门用于部署其他合约(称为「子合约 / 目标合约」)的智能合约,它的核心职责是:
- 封装子合约的部署逻辑(比如用
new关键字); - 对外提供统一的部署接口;
- 通常会记录所有已部署的子合约地址(方便管理 / 追溯)。
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
- 在
UserInfoFactory实例中,找到deployUserContract函数,点击「transact」; - 交易成功后,复制返回的
UserInfoContract地址(如0x8A791620dd6260079BF849Dc5567aDC3F2FdC318); - 验证:调用
UserInfoFactory的getDeployedCount(),返回1(表示已部署 1 个合约);调用deployedUserContracts(0),返回上述复制的地址(验证记录成功)。
步骤 3:操作并验证 UserInfoContract
- 在「Deploy & Run Transactions」的「At Address」输入框粘贴步骤 4 复制的地址,「Contract」下拉框选择
UserInfoContract,点击「At Address」加载合约实例; - 初始化用户信息
- 调用
initUser函数,参数输入你的用户名(如"Alice"),点击「transact」; - 调用
getUserInfo函数,点击「call」,返回结果为("Alice", 工厂合约地址)(验证部署者和用户名设置成功);
- 调用
- 修改用户名
- 调用
updateUserName函数,参数输入"Bob",点击「transact」; - 再次调用
getUserInfo,返回("Bob", 工厂合约地址)(验证用户名修改成功);
- 调用
- 重复初始化验证 :再次调用
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 |
CREATE(new 底层) |
|---|---|---|
| 地址生成依据 | 工厂地址 + salt + 子合约字节码哈希 | 工厂地址 + 工厂 nonce(部署次数) |
| 地址可预测性 | ✅ 部署前可精确计算 | ❌ 不可预测(nonce 随部署变化) |
| Solidity 封装语法 | new 合约名{salt: _salt}() |
new 合约名() |
| 核心优势 | 地址确定、提前规划 | 简单、无需自定义 salt |
| 适用场景 | 需提前知地址的场景(跨链、DEX) | 普通批量部署(无地址预判需求) |
2.案例
-
子合约(保持不变,复用之前的
UserInfoContract) -
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:预计算子合约地址(核心验证)
- 在工厂合约实例中,找到
computeContractAddress函数,输入 salt 值12345,点击「call」; - 记录返回的预计算地址 (比如
0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199); - 此时该地址还未部署任何合约(只是预计算结果)。
步骤 4:用 CREATE2 部署子合约(验证地址一致)
- 在工厂合约实例中,找到
deployWithCREATE2函数,输入相同的 salt 值12345,点击「transact」; - 交易成功后,复制返回的实际部署地址;
- 对比「预计算地址」和「实际部署地址」------ 两者完全一致(
CREATE2核心优势体现)。
步骤 5:验证子合约功能
- 在「At Address」输入实际部署地址,选择
UserInfoContract,加载合约实例; - 调用
initUser("Charlie"),再调用getUserInfo()→ 返回("Charlie", 工厂合约地址); - 调用
updateUserName("Dave")→ 验证用户名修改成功。
11、删除合约
1、销毁合约的核心原理(selfdestruct)
Solidity 中销毁合约的唯一方式是调用 selfdestruct(address payable recipient)(旧称 suicide,已废弃),这是 EVM 级别的低级操作,核心特性如下:
- 核心行为
- 永久销毁当前合约的代码 和存储数据(所有状态变量被清空,无法恢复);
- 将合约账户中剩余的所有 ETH 转移到指定的
recipient地址; - 合约地址不会消失,但地址对应的代码 / 存储被清空,后续调用该地址的任何函数都会失败。
- 权限要求 :仅能由合约自身调用(或通过合约内的函数间接调用,需加权限控制);
- 不可逆性:销毁后无法恢复,是永久性操作,需谨慎使用。
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 {}
}
- 管理销毁的工厂合约(
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:部署工厂合约
- 在 Remix 中选择 DestructibleUserInfoFactory 合约
- 点击「Deploy」按钮部署合约
- 等待交易确认,获得工厂合约地址
步骤 2:调用工厂的 deployAndInitContract
- 在已部署的工厂合约界面,找到
deployAndInitContract函数 - 输入参数
_userName: "TestFactory" - 点击「transact」执行交易
- 等待交易成功,复制返回的子合约地址
步骤 3:验证子合约 deployer
- 在 Remix 的「At Address」输入框中粘贴子合约地址
- 从下拉菜单中选择 DestructibleUserInfo 合约类型
- 点击「At Address」按钮加载子合约
- 调用子合约的
deployer()函数 - 验证返回的地址是否为工厂合约地址
步骤 4:测试工厂销毁子合约
4.1 向子合约发送 ETH
- 调用工厂的
sendEthToContract函数 - 参数
_contractAddr: 粘贴子合约地址 - 在 Value 字段输入
1 - 单位选择 ether
- 点击「transact」执行交易
4.2 销毁子合约
- 调用工厂的
destroyUserContract函数 - 参数
_contractAddr: 子合约地址(payable 类型) - 参数
_recipient: 你的钱包地址 - 点击「transact」执行交易
- 等待交易成功确认
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.data是Solidity中的一个全局变量,值为完整的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个字节,当selector与method id相匹配时,即表示调用该函数,那么函数签名是什么?
函数签名,为"函数名(逗号分隔的参数类型)"举个例子,上面代码中mint的函数签名为"mint(address)"。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。
注意 ,在函数签名中,uint和int要写为uint256和int256。
我们写一个函数,来验证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,参数类型分别为uint256和bool。所以,只需要计算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)"));
}
映射类型参数
映射类型参数通常有:contract、enum、struct等。在计算method id时,需要将该类型转化成为ABI类型。
例如,如下函数mappingParamSelector中DemoContract需要转化为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.encodeWithSelector将elementaryParamSelector函数的method id作为selector和参数打包编码,传给call函数:
scss
// 使用selector来调用函数
function callWithSignature() external{
...
// 调用elementaryParamSelector函数
(bool success1, bytes memory data1) = address(this).call(abi.encodeWithSelector(0x3ec37834, 1, 0));
...
}