Solidity 基础
建议参考 https://wtf.afox.cc/
原版英文有更多进阶内容
合约结构
Solidity 合约通常包含以下几个主要部分:
- SPDX 许可标识:指定代码的开源许可。
- pragma 指令:声明 Solidity 版本。
- 导入语句:引入其他合约或库。
- 合约声明:使用
contract
关键字。 - 状态变量:存储在区块链上的持久数据。
- 事件:用于记录重要操作,可被外部监听。
- 修饰符:用于修改函数行为的可重用代码。
- 函数:合约的可执行代码单元。
以下是一个简单的合约结构示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 public storedData;
constructor(uint256 initialValue) {
storedData = initialValue;
}
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
数据类型与数据结构
Solidity 支持多种数据类型,包括基础类型(如 uint
、int
、bool
)、复杂类型(如 struct
、enum
、数组、映射)以及地址类型 address
。了解这些数据类型的特性对于编写高效和安全的合约至关重要。
值类型
- uint: 无符号整数,
uint256
是默认类型,表示 0 到 2^256-1 的整数。可以使用不同的位宽,如uint8
、uint16
等。 - int: 有符号整数,范围为-2^(n-1)到 2^(n-1)-1。
- bool: 布尔类型,只有
true
和false
两个值。 - address: 20 字节的以太坊地址类型,分为
address
和address payable
(后者可用于接收以太币)。 - bytes1 ~ bytes32:固定大小字节数组
引用类型
- string:动态大小的 UTF-8 编码字符串
- bytes:动态大小的字节数组
- 数组:如
uint[]
(动态大小)或uint[5]
(固定大小) - 结构体 (Struct):自定义的复杂数据类型,例:
struct Person { string name; uint age; }
- 映射 (Mapping):键值对存储,如
mapping(address => uint)
注意事项
- Mapping 不支持直接遍历,需结合其他结构记录键值。
- 动态数组操作(如
push
)会增加 Gas,尽量减少不必要的操作。
参考资料
https://solidity-by-example.org/array/
https://solidity-by-example.org/mapping/
https://solidity-by-example.org/structs/
函数修饰符与类型
函数修饰符决定了函数的可见性和行为:
- 可见性修饰符:
public
:内部和外部都可调用private
:只能在定义的合约内部调用(虽然区块链上的数据是公开的,但限制了其他合约的直接访问)internal
:只能在内部和派生合约中调用external
:只能从外部调用
- 状态修饰符:
view
:不修改状态(但可以读取)pure
:不读取也不修改状态
- 支付相关:
payable
:允许函数接收以太币
注意事项
- 使用
private
并不意味着数据绝对安全,仍需注意数据泄露的可能性。 external
函数比public
函数消耗更少 Gas,适用于只需外部访问的函数。view
和pure
声明的函数直接执行不会消耗 Gas,只是做了个调用,没有发送交易,但如果是别的需要消耗 Gas 的函数调用了view
或者pure
的函数,还是会消耗对应的 Gas 的。
参考资料
https://docs.soliditylang.org/en/v0.8.23/cheatsheet.html#function-visibility-specifiers
https://solidity-by-example.org/view-and-pure-functions/
内存管理与数据位置
Solidity 中的数据存储位置决定了数据的生命周期和 Gas 消耗:
- Storage: 永久存储,数据保存在区块链上。默认的状态变量存储位置,Gas 成本高。
- Memory: 临时数据位置,函数调用结束即释放。适合在函数内处理临时数据。
- Calldata: 只读数据位置,通常用于外部函数调用的参数。不可修改,效率高。
注意事项
- 尽量减少 Storage 的读写次数以节省 Gas。
- 在复杂数据操作中,优先考虑 Memory。
- 静态数据类型如固定大小的数组或基本类型不需要指定数据位置。
参考资料:https://docs.alchemy.com/docs/when-to-use-storage-vs-memory-vs-calldata-in-solidity
从 storage 中存取数据的 gas 开销要远大于直接从 memory 中存取(相差 33 倍)
参考资料:https://www.evm.codes/#54?fork=shanghai
高级特性与优化
常量与不可变变量
使用 constant
和 immutable
可以优化 gas 使用:
constant
不允许赋值(除初始化以外),在编译时确定的常量,不占用存储空间。immutable
可在合约构造时赋值,之后不可更改,存储在代码中。
参考资料:https://docs.soliditylang.org/zh/v0.8.16/cheatsheet.html#index-3
特殊函数: Receive 和 Fallback
receive
的功能是当合约收到纯以太币(无数据)时,就会触发此函数。该函数还必须标记为“payable”。
receive() external payable {
// This function is executed when a contract receives plain Ether (without data)
}
fallback
函数是一个特殊函数,当合约收到 Ether 并调用合约中不存在的函数时,或者交易中没有提供任何数据时,就会执行该函数。如果希望合约能够以这种方式接收以太币,则必须将此函数标记为payable
。
fallback() external payable {
// This function is executed on a call to the contract if none of the other
// functions match the given function signature, or if no data is supplied at all
}
修饰器(Modifier)
修饰器用于在函数执行前后添加检查或修改行为:
modifier 修饰符名称(参数) {
// 前置条件检查
require(条件, "错误消息");
_; // 表示被修饰函数的代码
// 后置操作(如果有)
}
使用示例
modifier onlyOwner() {
require(msg.sender == owner, "只有合约拥有者才能调用此函数");
_;
}
function withdrawFunds() public onlyOwner {
// 提款逻辑
}
注意事项
- 可以组合多个 modifier
- 执行顺序:从左到右依次执行 modifier
- 可以在 modifier 中使用参数
_;
表示被修饰函数的代码插入点
错误处理与安全性
Solidity 提供了多种错误处理机制:
- require: 用于输入验证和外部调用的错误检测。
- assert: 用于内部一致性检查。
- revert: 提供自定义错误消息,回滚状态。
使用示例
contract ErrorHandlingExample {
function requireExample(uint x) public pure {
require(x > 10, "x must be greater than 10");
}
function assertExample(uint x) public pure {
assert(x != 0); // 用于内部错误检查
}
function revertExample(uint x) public pure {
if (x <= 10) {
revert("x must be greater than 10");
}
}
// 自定义错误
error InsufficientBalance(uint requested, uint available);
function withdraw(uint amount) public {
uint balance = address(this).balance;
if (amount > balance) {
revert InsufficientBalance({
requested: amount,
available: balance
});
}
// 处理提款
}
}
安全性注意事项
- 避免重入攻击:使用“检查-效果-交互”模式。
- 防止整数溢出:使用 Solidity 0.8+的内置检查或
SafeMath
库。
常用全局变量
msg
对象
msg.sender
: 当前调用者的地址,常用于权限验证。msg.value
: 当前交易发送的以太币数量,常用于支付逻辑。msg.data
: 调用数据的完整字节数组,适用于低级调用。msg.sig
: 调用数据的前 4 字节函数选择器。
block
对象
block.timestamp
: 当前区块的时间戳(Unix 时间),常用于时间限制。block.number
: 当前区块的编号,可以用于获取链上数据的时间顺序。block.difficulty
: 当前区块的难度。
tx
对象
tx.origin
: 交易发起者的原始地址,通常不建议用于权限验证,因为可能导致安全问题。
其他
gasleft()
: 剩余的 Gas 量,用于监控 Gas 消耗。
合约间交互与继承
合约导入
使用 import
语句导入其他合约或库:
// File: ImportExample.sol
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./MyOtherContract.sol";
contract ImportExample is ERC20, MyOtherContract {
constructor() ERC20("MyToken", "MTK") {
// 构造函数逻辑
}
}
合约继承
如果要继承某个 contract 的话,使用 is 关键词
contract AddFiveStorage is SimpleStorage
Solidity 支持多重继承:
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
}
contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
}
contract MyContract is Ownable, Pausable {
function doSomething() public onlyOwner whenNotPaused {
// 函数逻辑
}
}
如果想修改继承过来的合约里的函数,则需要用 override 关键词,并且父级合约中的函数需要带上 virtual 关键词,没有 virtual 的函数都无法被重写
// 函数中有 virtual 修饰符才能被继承改写
function store(uint256 _favoriteNumber) public virtual {
myFavoriteNumber = _favoriteNumber;
}
// 要改写继承过来的函数,需要带上 override 关键词
function store(uint256 _newNumber) public override {
myFavoriteNumber = _newNumber + 5;
}
接口与抽象合约
接口和抽象合约用于定义合约的标准结构:
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
// 其他 ERC20 函数...
}
abstract contract ERC20Base is IERC20 {
mapping(address => uint256) private _balances;
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
// 其他实现...
}
contract MyToken is ERC20Base {
// 实现剩余的抽象函数
}
入门案例
FavoriteNumber
本案例实现了用户存储和检索与名字关联的喜好数字:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FavoriteNumber {
mapping(string => uint256) private nameToFavoriteNumber;
function createOrUpdateFavoriteNumber(string memory name, uint256 number) public {
nameToFavoriteNumber[name] = number;
}
function getNumber(string memory name) public view returns(uint256) {
return nameToFavoriteNumber[name];
}
}
ProfileStatus
本案例实现了用户设置和检索个人信息
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ProfileStatus {
struct Status{
string name;
string message;
}
mapping (address => Status) private userStatus;
function createOrUpdateStatus (string memory _name, string memory _message) public {
userStatus[msg.sender].name = _name;
userStatus[msg.sender].message = _message;
}
function getStatus() public view returns (string memory, string memory) {
return (userStatus[msg.sender].name, userStatus[msg.sender].message);
}
}
TipJar
本案例实现了一个简单的小费罐功能:可以支付小费,提取余额
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract TipJar {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "You are not owner!");
_;
}
function tip() public payable {
require(msg.value > 0, "You should send a tip to use this function");
}
function withdraw() public onlyOwner {
uint256 contractBalance = address(this).balance;
require(contractBalance > 0, "There are no tips to withdraw");
payable(owner).transfer(contractBalance);
}
function getBalance() public onlyOwner view returns (uint256) {
return address(this).balance;
}
}
ABI
在以太坊生态系统中,ABI(Application Binary Interface,应用二进制接口) 是连接智能合约与外部应用(如前端 DApp)的关键桥梁。对于刚接触区块链和 Solidity 的初学者来说,理解 ABI 的概念和作用至关重要。
什么是 ABI?
ABI,全称为 Application Binary Interface,即应用二进制接口。它定义了合约中的函数和事件如何与外部系统进行交互。换句话说,ABI 描述了合约的接口,包括函数的名称、参数类型、返回值以及事件的结构等信息。
智能合约部署到以太坊区块链后,其源代码对外不可见,外部应用需要一种方式与合约进行交互。ABI 就是这种桥梁,它让外部应用知道如何调用合约的函数、如何解码合约返回的数据以及如何监听合约的事件。
ABI 的组成
一个标准的 Solidity ABI 是一个 JSON(JavaScript Object Notation)数组,每个数组元素描述了合约中的一个函数、构造函数或事件。每个元素包含以下字段:
type
: 描述类型,如function
、constructor
、event
。name
: 函数或事件的名称。inputs
: 输入参数的详细信息,包括类型和名称。outputs
: (仅限函数)输出参数的详细信息。stateMutability
: 函数的状态可变性,如view
(只读)、nonpayable
(不接受以太币)等。anonymous
: (仅限事件)是否为匿名事件。
ABI 的作用
- 函数调用:ABI 描述了如何编码函数调用的参数,并解码合约返回的数据。外部应用通过 ABI 知道如何与合约的函数进行交互。
- 事件监听:ABI 定义了事件的结构,外部应用可以根据 ABI 监听和解析合约触发的事件。
- 接口兼容性:ABI 使得不同编程语言和工具能够一致地与合约进行交互,确保兼容性和互操作性。
当我们要调用一个函数时,使用 ABI JSON 的规范的要求,进行编码,传给 EVM, 同时在 EVM 层生成的字节数据(如时间日志等),ABI JSON 的规范进行解码。
合约 ABI 示例
假设有以下简单的 Solidity 合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private data;
event DataStored(address indexed sender, uint256 data);
function storeData(uint256 _data) public {
data = _data;
emit DataStored(msg.sender, _data);
}
function getData() public view returns (uint256) {
return data;
}
}
在编写 Solidity 合约后,编译工具(如 Solidity Compiler 或 Remix IDE)会自动生成 ABI。
对应的 ABI 如下:
[
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "data",
"type": "uint256"
}
],
"name": "DataStored",
"type": "event"
},
{
"inputs": [],
"name": "getData",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_data",
"type": "uint256"
}
],
"name": "storeData",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
编码函数
Solidity 提供了一组内置的编码函数,用于将不同类型的数据打包成二进制格式,以便于在合约内部或与外部系统(如前端应用、其他合约)进行交互。
函数 | 编码格式 | 用途 | 优缺点 |
---|---|---|---|
abi.encode(...) | 标准 ABI 格式 | 函数调用、数据传输 | 可避免歧义,适用于大多数场景 |
abi.encodePacked(...) | 紧凑格式 | 哈希生成、紧凑数据存储 | 节省空间,但可能导致参数歧义 |
abi.encodeWithSignature(...) | 函数签名编码 | 低级别函数调用、外部交互 | 自动生成函数选择器,适合简化调用 |
abi.encodeWithSelector(...) | 选择器编码 | 灵活构建低级别调用数据 | 手动指定选择器,适合需要额外控制的场景 |
abi.encode()
功能:按照 Solidity 的标准 ABI 编码规则,将输入的参数编码为紧凑的字节数组(bytes
)。
用途:
- 构建函数调用的数据负载。
- 在合约内部传递复杂的数据结构。
示例:
pragma solidity ^0.8.0;
contract EncoderExample {
function encodeData(uint256 _num, address _addr) public pure returns (bytes memory) {
return abi.encode(_num, _addr);
}
}
解释:
- 调用
encodeData(123, 0xAbC...)
会返回一个编码后的字节数组,包含123
和地址0xAbC...
的 ABI 编码。
abi.encodePacked(...)
功能:将输入的参数按照紧凑格式(packed)进行编码,减少数据冗余。
用途:
- 生成哈希(如
keccak256
)的输入。 - 创建更紧凑的数据表示。
注意:abi.encodePacked
可能导致参数间的歧义,尤其是在动态类型(如 string
、bytes
)和可变长度的数组中使用时。因此,使用时需谨慎,确保编码后的数据不会导致解析错误。
示例:
pragma solidity ^0.8.0;
contract PackedEncoderExample {
function encodePackedData(uint256 _num, address _addr) public pure returns (bytes memory) {
return abi.encodePacked(_num, _addr);
}
}
解释:
- 调用
encodePackedData(123, 0xAbC...)
会返回一个更紧凑的字节数组,与abi.encode
相比,减少了填充字节。
abi.encodeWithSignature(...)
功能:根据给定的函数签名,将参数编码为 ABI 格式的字节数组,通常用于构建低级别的函数调用数据。
用途:
- 在合约间进行低级别的函数调用(如通过
call
)。 - 与外部应用程序(如前端、脚本)进行交互,构建函数调用数据。
示例:
pragma solidity ^0.8.0;
contract WithSignatureExample {
function getData(bytes memory _encoded) public pure returns (bytes32) {
return keccak256(_encoded);
}
}
前端构建调用数据:
const { ethers } = require("ethers");
const abi = [
"function storeData(uint256 _data) public",
];
const iface = new ethers.utils.Interface(abi);
const data = iface.encodeFunctionData("storeData", [123]);
// data = "0x<function_selector><encoded_parameters>"
解释:
encodeFunctionData
内部使用abi.encodeWithSignature
来生成函数调用的数据。- 生成的数据包括函数选择器和编码后的参数,适用于合约的低级别调用。
abi.encodeWithSelector(...)
功能:根据给定的函数选择器,将参数编码为 ABI 格式的字节数组。
用途:
- 类似于
abi.encodeWithSignature
,但更灵活,可以手动指定函数选择器。 - 在合约内部进行低级别调用时使用。
示例:
pragma solidity ^0.8.0;
contract WithSelectorExample {
function getData(bytes4 _selector, uint256 _num) public pure returns (bytes memory) {
return abi.encodeWithSelector(_selector, _num);
}
}
解释:
- 调用
getData(bytes4(keccak256("storeData(uint256)")), 123)
会返回编码后的字节数组,包含指定的函数选择器和参数。
实际应用示例
合约间低级别调用
假设有两个合约:Caller
和 Callee
。Caller
需调用 Callee
的 storeData
函数,但使用低级别的 call
。
Callee 合约:
pragma solidity ^0.8.0;
contract Callee {
uint256 public data;
event DataStored(address indexed sender, uint256 data);
function storeData(uint256 _data) public {
data = _data;
emit DataStored(msg.sender, _data);
}
}
Caller 合约:
pragma solidity ^0.8.0;
contract Caller {
function callStoreData(address _callee, uint256 _data) public returns (bool, bytes memory) {
// 使用 abi.encodeWithSignature
bytes memory payload = abi.encodeWithSignature("storeData(uint256)", _data);
(bool success, bytes memory returnData) = _callee.call(payload);
return (success, returnData);
}
function callStoreDataWithSelector(address _callee, uint256 _data) public returns (bool, bytes memory) {
// 手动构建选择器
bytes4 selector = bytes4(keccak256("storeData(uint256)"));
bytes memory payload = abi.encodeWithSelector(selector, _data);
(bool success, bytes memory returnData) = _callee.call(payload);
return (success, returnData);
}
}
解释:
Caller
合约通过abi.encodeWithSignature
和abi.encodeWithSelector
构建调用Callee
合约的storeData
函数的数据负载。- 使用低级别的
call
方法执行函数调用,捕获返回值和执行状态。
前端与合约交互
使用 Ethers.js 构建函数调用数据,并通过低级别调用与合约交互。
const { ethers } = require("ethers");
// 合约ABI和地址
const contractABI = [
"function storeData(uint256 _data) public",
];
const contractAddress = "0xC2eF4Beb82626190C6E80605e9f95CD3aC55583B";
// 创建接口
const iface = new ethers.utils.Interface(contractABI);
// 编码函数调用数据
const data = iface.encodeFunctionData("storeData", [123]);
// 结果: "0x<function_selector><encoded_parameters>"
// 发送交易
const provider = new ethers.providers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID");
const signer = provider.getSigner();
const tx = {
to: contractAddress,
data: data,
};
signer.sendTransaction(tx).then((transaction) => {
console.log("Transaction sent:", transaction.hash);
});
解释:
- 使用 Ethers.js 的
Interface
对象,简化函数调用数据的编码过程。 encodeFunctionData
自动处理函数选择器和参数编码,生成适用于交易的数据字段。
事件与日志
事件概述
事件是智能合约与外部世界交互的重要机制,广泛应用于以下场景:
- 状态变更通知:当合约状态发生变化时,通过事件通知外部应用。例如,代币转账事件。
- 调试与记录:开发者可以使用事件来记录合约执行过程中的关键信息,便于调试和审计。
- 触发前端更新:前端应用可以监听特定事件,以动态更新界面或执行特定操作。
事件与日志的关系
在以太坊生态系统中,事件和日志是紧密相连的概念。**日志(Logs)**是存储在以太坊区块链上的数据结构,用于记录智能合约中触发的事件。在 Solidity 中,事件是通过日志(Logs)来实现的。当合约中的事件被触发时,相应的日志条目会被创建并添加到当前的区块中。日志具有如下几个重要特点:
- 不可修改性:一旦日志被记录到区块链上,它们就变成了不可更改或删除的记录。这保证了事件数据的完整性和可信度。
- 低成本:相比于将数据存储在合约的状态变量中,记录日志的gas成本要低得多。这使得事件成为一种经济高效的数据存储方式,特别是对于不需要直接被合约访问的数据。
- 可查询性:外部应用可以方便地查询和订阅这些日志,从而获取合约中发生的重要事件信息。
- 特殊的数据结构:日志不存储在区块链的全局状态中,而是作为元数据附加在区块上。这种设计使得日志的存储和检索更加高效。
事件的结构与特性
事件的定义和使用涉及几个关键概念:签名、主题和索引参数。理解这些概念对于有效使用事件至关重要。
每个事件都有一个唯一的签名,它包括事件名称和参数类型。例如,一个事件 Transfer(address indexed from, address indexed to, uint256 value)
,它带有 address
类型的 from
和 to
参数,以及 uint256
类型的 value
参数,其签名就是 Transfer(address,address,uint256)
。
事件签名经过 Keccak-256 哈希运算后,得到的哈希值被称为事件的主题(Topic)。这个主题用于在区块链上唯一标识该事件,使得外部应用能够快速定位和过滤特定类型的事件。
Solidity 允许使用 indexed
关键字标记事件参数。这些被标记的参数称为索引参数,它们具有特殊的属性:
- 索引参数允许外部应用基于这些参数值高效地过滤和查询相关事件。
- 每个事件最多可以有三个索引参数。
- 索引参数的值被存储在日志的主题部分,而不是数据部分,这使得它们更容易被检索。
日志的结构可以概括如下:
+-----------------+
| Address |
+-----------------+
| Topics |
| +-------------+ |
| | Topic 0 | | // 事件签名的哈希
| | Topic 1 | | // 第一个 `indexed` 参数
| | Topic 2 | | // 第二个 `indexed` 参数
| +-------------+ |
+-----------------+
| Data | // 未 `indexed` 的参数
+-----------------+
topics[0]
:始终存储事件签名的 Keccak-256 哈希值,用于唯一标识事件类型。topics[1..3]
:存储被标记为indexed
的参数。每个indexed
参数占用一个topic
索引位置,最多支持三个indexed
参数。data
:存储所有未被标记为indexed
的参数,按照 ABI 编码格式打包存储。
匿名事件
匿名事件通过在事件定义中添加 anonymous
关键字实现。默认情况下,事件是非匿名的,即事件签名的哈希值会自动作为第一个主题(topics[0]
)存储在日志中。而将事件标记为 anonymous
后,事件签名的哈希值将 不 被存储在 topics[0]
中。
以下是如何定义一个匿名事件的示例:
pragma solidity ^0.8.0;
contract AnonymousEventExample {
// 定义一个匿名事件
event AnonymousDataStored(address indexed sender, uint256 data) anonymous;
function storeData(uint256 _data) public {
emit AnonymousDataStored(msg.sender, _data);
}
}
匿名事件的特性
- 不存储事件签名哈希:
- 匿名事件不会将事件签名的 Keccak-256 哈希值存储在
topics[0]
中。这意味着外部应用在监听和过滤事件时,无法通过事件名称直接过滤匿名事件。
- 匿名事件不会将事件签名的 Keccak-256 哈希值存储在
- 主题位置:
- 由于事件签名哈希不作为
topics[0]
存储,匿名事件的topics
数组通常从topics[0]
开始存储被标记为indexed
的参数。
- 由于事件签名哈希不作为
- 用途限制:
- 匿名事件适用于那些不需要通过事件名称过滤的场景,如内部逻辑记录或特定的链上操作记录。
- 由于缺少事件签名哈希,匿名事件在被外部应用强制过滤时不如非匿名事件直观和高效。
匿名事件生成的日志结构示例:
+-----------------+
| Address |
+-----------------+
| Topics |
| +-------------+ |
| | Topic 0 | | // 第一个 `indexed` 参数
| | Topic 1 | | // 第二个 `indexed` 参数(如果有)
| +-------------+ |
+-----------------+
| Data | // 未 `indexed` 的参数
+-----------------+
topics[0]
:不包含事件签名哈希,而是直接存储第一个indexed
参数的值。topics[1..n]
:继续存储其他indexed
参数的值。data
:存储所有未被标记为indexed
的参数,按照 ABI 编码格式打包。
使用注意事项
- 事件过滤困难:
- 由于匿名事件不包含事件签名哈希,无法通过事件名称进行过滤。外部应用需要依赖
indexed
参数或其他条件来筛选感兴趣的事件。
- 由于匿名事件不包含事件签名哈希,无法通过事件名称进行过滤。外部应用需要依赖
- 参数过滤:
- 可以通过监听特定的
indexed
参数值来间接过滤事件。例如,监听特定地址发送的匿名事件。
- 可以通过监听特定的
- 限制
indexed
参数数量:- 匿名事件允许声明四个索引参数,而不是三个。
- 用途场景:
- 匿名事件的优点是,它们的部署和调用都比较便宜。
- 适用于不需要基于事件名称过滤事件的内部记录或特定逻辑操作记录。
- 适用于只有一个事件的合约。监听合约中的所有事件是有意义的,因为只有这一个事件将出现在事件日志中。
匿名事件与非匿名事件的比较
特性 | 匿名事件 (anonymous ) | 非匿名事件 |
---|---|---|
事件签名哈希存储 | 不存储在 topics[0] 中 | 存储在 topics[0] 中 |
事件过滤 | 需依赖 indexed 参数或其他条件进行过滤 | 可通过事件名称直接过滤 |
用途 | 内部记录、特定逻辑操作 | 状态变更通知、广泛的事件监听 |
实现方式 | 在事件定义中添加 anonymous 关键字 | 默认事件,无需额外关键字 |
应用案例
下面是一个简单的 Solidity 合约示例,展示如何定义和触发事件,以及如何在函数中使用它们。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
// 定义一个名为 DataStored 的事件
event DataStored(address indexed sender, uint256 data);
uint256 private data;
// 存储数据并触发事件
function storeData(uint256 _data) public {
data = _data;
emit DataStored(msg.sender, _data); // 触发事件
}
// 读取数据
function getData() public view returns (uint256) {
return data;
}
}
在这个合约中,我们定义了一个 DataStored
事件,它有两个参数:一个索引的 sender
地址和一个非索引的 data
值。每当 storeData
函数被调用时,这个事件就会被触发。
当我们调用 storeData(5)
时,会生成如下的日志:
[
{
"logIndex": "0x1",
"blockNumber": "0x13",
"blockHash": "0xe1d6fc3a880e206bd50af0b3dd60f00b6e99a2a20f436c84f7ca717310d941db",
"transactionHash": "0xe772082aa1798e9676858d0925fb1a2b3d4ad3c25013acd795613da8f0e475c2",
"transactionIndex": "0x0",
"address": "0xaE036c65C649172b43ef7156b009c6221B596B8b",
"data": "0x0000000000000000000000000000000000000000000000000000000000000005",
"topics": [
"0xe42ab83e51dcfb436887e998d12b1585d6eea49b2900b0b3bcd0591dec7c3d19",
"0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4"
]
}
]
日志具体对应关系如下具体对应关系
原始日志字段 | 解释 | Solidity 对应 |
---|---|---|
logIndex | 日志在区块中的索引位置 | storeData 函数触发的第二个日志(如果有多个事件) |
blockNumber | 日志所在区块编号 | 第19个区块 |
blockHash | 日志所在区块的哈希值 | 区块的唯一标识符 |
transactionHash | 触发日志的交易哈希值 | 唯一标识具体触发 storeData 函数调用的交易 |
transactionIndex | 交易在区块中的索引位置 | 交易是该区块中的第一个交易 |
address | 触发事件的合约地址 | SimpleStorage 合约地址 |
data | 事件的非 indexed 参数 data 的值 | uint256 data = 5 ,以 ABI 编码格式存储 |
topics[N] | 事件签名哈希和 indexed 参数 | topics[0] : 事件签名哈希topics[1] : sender 地址 |
这里的topic数组可以更详细的解释下:
topics[0]
:"0xe42ab83e51dcfb436887e998d12b1585d6eea49b2900b0b3bcd0591dec7c3d19"
- 解释:这是事件签名的 Keccak-256 哈希值,用于唯一标识
DataStored
事件。 - 计算方式:
- 事件签名字符串为
"DataStored(address,uint256)"
。 - 计算其 Keccak-256 哈希值,得到
0xe42ab83e51dcfb436887e998d12b1585d6eea49b2900b0b3bcd0591dec7c3d19
。
- 事件签名字符串为
- 解释:这是事件签名的 Keccak-256 哈希值,用于唯一标识
topics[1]
:"0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4"
- 解释:这是第一个
indexed
参数sender
的值。由于 Solidity 中sender
被标记为indexed
,它被存储在topics
中。
- 解释:这是第一个
前端事件监听
在前端应用中,可以使用 Web3.js 或 Ethers.js 等库来监听合约事件。以下是使用 Ethers.js 的简单示例:
function ContractListener() {
const [events, setEvents] = useState([]);
useEffect(() => {
const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID");
const contract = new ethers.Contract(contractAddress, contractABI, provider);
const filter = contract.filters.DataStored();
const handleEvent = (sender, data, event) => {
setEvents(prev => [...prev, { sender, data: data.toString(), txHash: event.transactionHash }]);
};
// 实时监听新事件
contract.on(filter, handleEvent);
// 查询过去的事件
contract.queryFilter(filter, -1000, "latest").then(pastEvents => {
pastEvents.forEach(event => {
const { sender, data } = event.args;
setEvents(prev => [...prev, { sender, data: data.toString(), txHash: event.transactionHash }]);
});
});
// 清理函数
return () => {
contract.off(filter, handleEvent);
};
}, []);
// 渲染事件列表的 JSX ...
}
- 连接 Provider:通过 Infura 连接到 Sepolia 测试网。
- 创建合约实例:使用合约地址和 ABI 创建 Ethers.js 合约实例。
- 监听事件:
- 使用
contract.on
监听DataStored
事件。 - 当事件触发时,将事件数据添加到
events
状态中。
- 使用
- 查询历史事件:
- 使用
queryFilter
查询过去 1000 个区块内的DataStored
事件,并添加到events
状态中。
- 使用
Call 与 Delegatecall
在 Solidity 中,合约之间的交互不仅可以通过接口和继承实现,还可以利用低级别的函数调用机制,如 call
和 delegatecall
call
:一种低级别的函数调用方式,允许合约调用另一个合约的函数,同时可以发送以太币。调用目标合约的代码在目标合约的上下文中执行,拥有自己的存储、以太币余额和地址。delegatecall
:类似于call
,但在调用过程中保持调用者(当前合约)的上下文。目标合约的代码在调用者的上下文中执行,因此它们共享相同的存储、以太币余额和地址。
Call
call
是 Solidity 提供的低级别函数调用方法,允许一个合约调用另一个合约的函数。它的基本语法如下:
(bool success, bytes memory data) = address(target).call{value: amount}(abi.encodeWithSignature("functionName(type1,type2)", arg1, arg2));
address(target).call{value: amount}(...)
:目标地址执行call
,并可选择发送一定的以太币。abi.encodeWithSignature
:对函数名和参数进行编码,构建调用数据。- 返回值包括:
success
:调用是否成功的布尔值。data
:返回的数据,以字节数组形式存储。
使用示例
假设有两个合约:Caller
和 Callee
。Caller
通过 call
调用 Callee
的 storeData
函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Callee {
uint256 public data;
event DataStored(address indexed sender, uint256 data);
function storeData(uint256 _data) public payable {
data = _data;
emit DataStored(msg.sender, _data);
}
}
contract Caller {
event CallResult(bool success, bytes data);
function callStoreData(address _callee, uint256 _data) public payable {
(bool success, bytes memory returnData) = _callee.call{value: msg.value}(
abi.encodeWithSignature("storeData(uint256)", _data)
);
emit CallResult(success, returnData);
}
}
优点
- 灵活性高:可以动态调用任何合约的任何函数,无需预先知道目标合约的具体接口。
- 支持以太币发送:可以在调用过程中发送以太币给目标合约。
缺点
- 缺乏类型检查:由于是低级调用,编译器无法对函数选择器和参数进行类型检查,容易导致调用失败或意外行为。
- 返回数据处理复杂:需要手动解码返回的数据,增加了代码复杂性。
- 安全风险高:未正确处理
call
的返回值和重入攻击等安全问题,可能导致合约被攻击。
Delegatecall
delegatecall
是 Solidity 提供的一个低级别函数,用于在当前合约的上下文中调用另一个合约的代码。与普通的 call
不同,delegatecall
会保持调用者(当前合约)的存储、地址和余额,而目标合约的代码将在调用者的上下文中执行。这使得 delegatecall
成为实现代理合约和可升级合约的重要工具。
当执行 delegatecall
时,以下几点尤为重要:
- 上下文保持:
delegatecall
保持调用者的上下文,包括msg.sender
和msg.value
,而目标合约在调用者的存储中操作。 - 存储访问:目标合约的代码会读取和修改调用者合约的存储,而不是目标合约自己的存储。
- 函数执行:目标合约的函数通过调用者的地址和存储进行执行。
就比如:
(bool success, bytes memory returnData) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
implementation
:目标合约的地址。msg.data
:调用数据,包含以下部分:- 函数选择器(Function Selector):前4个字节,用于识别要调用的函数。
- 参数编码(Encoded Parameters):函数参数按 ABI 编码规则打包的字节数据。
使用示例
假设有两个合约:Proxy
和 Implementation
。Proxy
通过 delegatecall
调用 Implementation
的 storeData
函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Implementation {
uint256 public data;
event DataStored(address indexed sender, uint256 data);
function storeData(uint256 _data) public {
data = _data;
emit DataStored(msg.sender, _data);
}
}
contract Proxy {
uint256 public data;
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
}
调用流程示例
- 部署合约:
- 部署
Implementation
合约,获得其地址,例如0xImplAddress
. - 部署
Proxy
合约,初始化_implementation
为0xImplAddress
, 获得代理合约地址0xProxyAddress
.
- 部署
- 调用代理合约的
storeData
函数:- 前端或用户通过
Proxy
地址调用storeData(uint256)
,例如传入42
。 - 调用数据
msg.data
包含函数选择器0x6d4ce63c
(假设是storeData(uint256)
的选择器)和参数42
的 ABI 编码。
- 前端或用户通过
- 代理合约执行
delegatecall
:Proxy
合约的fallback
函数被触发,执行implementation.delegatecall(msg.data)
.delegatecall
解析msg.data
,找到函数选择器对应的storeData
函数,并在Proxy
的上下文中执行。
- 状态更新:
storeData
函数在Proxy
合约的存储中更新data
变量,而不是Implementation
合约。- 事件
DataStored
也是在Proxy
触发,因为msg.sender
是原始调用者。
- 事件日志:
- 事件日志记录在
Proxy
合约的地址下,data
值为42
。
- 事件日志记录在
优点
- 代码复用:允许多个合约共享相同的实现代码,减少部署成本和代码冗余。
- 升级性:通过更改
implementation
地址,可以轻松升级合约逻辑,而无需迁移存储数据。 - 保持状态一致:相同的存储布局保证了调用者和被调用者共享相同的状态变量。
缺点
- 复杂性高:需要仔细设计存储布局,确保调用者和被调用者的状态变量一致,避免存储冲突。
- 安全风险高:错误的
delegatecall
实现可能导致存储被篡改或合约被攻击,需要做好权限控制。 - 调试困难:由于代码在调用者上下文中执行,追踪和调试问题更加复杂。
存储冲突
存储冲突发生在代理合约和实现合约使用相同的存储槽存储不同的变量或数据时,导致数据被覆盖或混淆。
原因:
- 存储槽重叠:
- 如果 Proxy 合约和 Implementation 合约在相同的存储槽中定义了不同的变量,执行
delegatecall
时会导致变量值被错误地覆盖。
- 如果 Proxy 合约和 Implementation 合约在相同的存储槽中定义了不同的变量,执行
- 不一致的存储布局:
- 代理合约和实现合约的状态变量声明顺序和类型不一致,导致变量在同一存储槽中的映射错误。
具体示例解析
错误示例:存储槽冲突导致的实现地址被覆盖
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Implementation {
string public female;
string public male;
function setFemaleName(string memory _name) public {
female = _name;
}
function setMaleName(string memory _name) public {
male = _name;
}
}
contract Proxy {
string public male;
string public female;
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
receive() external payable { }
}
问题:
- Proxy 合约:
- 槽0:
string public male;
- 槽1:
string public female;
- 槽2:
address public implementation;
- 槽0:
- Implementation 合约:
- 槽0:
string public female;
- 槽1:
string public male;
- 槽0:
- 执行过程:
- 用户通过 Proxy 调用
setFemaleName("Alice")
。 delegatecall
将调用转发到 Implementation 合约。- Implementation 的
setFemaleName
函数在 Proxy 合约的上下文中执行:female = "Alice"
被写入 Proxy 合约的槽0,而不是槽1。
- 用户通过 Proxy 调用
- 后果:
- Proxy 合约的
male
变量(槽0)被错误地设置为 “Alice”。 - Proxy 合约的
female
变量(槽1)保持不变。 - 状态变量
female
被错误地存储在 Proxy 合约的male
位置,导致数据不一致。
- Proxy 合约的
- 潜在问题:
- 存储布局不匹配导致数据存储位置错误。
- 后续对
male
和female
变量的读取将返回不正确的值。 - 可能引发意外的行为和数据混淆。
比较
特性 | call | delegatecall |
---|---|---|
执行上下文 | 目标合约的上下文(存储、地址、余额) | 调用者的上下文(存储、地址、余额) |
存储访问 | 访问目标合约的存储 | 访问调用者的存储 |
使用场景 | 动态函数调用、发送以太币、跨合约交互 | 代理合约模式、合约升级、代码复用 |
调用结果返回 | 与目标合约相关的返回值和状态 | 通过调用者的存储共享返回值和状态标志 |
安全性 | 较高风险,需谨慎处理返回值和防重入攻击 | 高风险,存储布局一致性和权限控制尤为重要 |
函数签名和参数 | 需要手动编码函数签名和参数,缺乏编译器支持 | 同 call ,但由于共享调用者的存储,参数使用需谨慎 |
Gas 消耗 | 通常稍高,因为涉及函数签名和参数编码 | 类似于 call ,但因共享存储可能增加复杂性 |
代理合约
代理合约 是一种智能合约,其主要职责是将调用转发到另一个逻辑合约(通常称为实现合约)。通过这种方式,代理合约本身保持不变,而实现合约可以根据需要进行升级或修改,实现合约逻辑的可升级性。
为什么需要代理合约?
智能合约一旦部署到区块链上,就无法修改。随着时间的推移,可能会发现合约存在漏洞、需要添加新功能或优化性能。在这种情况下,代理合约提供了一种解决方案,使得开发者可以升级合约逻辑而无需更改合约地址,从而保持与用户和其他合约的兼容性。
代理合约的工作原理
代理合约通过**委托调用(delegatecall)**机制,将函数调用和数据转发到实现合约。delegatecall
允许代理合约在自己的存储空间中执行实现合约的代码,这意味着实现合约的代码可以操作代理合约的状态变量。
透明代理 是最常见的代理模式,由 OpenZeppelin 实现。其主要特点是:
- 权限控制:代理合约有一个管理员账户,仅管理员可以升级实现合约。
- 透明性:对普通用户来说,代理合约看起来与实现合约相同,但管理员与普通用户的接口不同,避免调用冲突。
实现特点:
- 存储实现合约地址的槽(通常使用 EIP-1967 标准)。
fallback
函数中使用delegatecall
转发调用。
以下是透明代理模式的简单实现示例:
实现合约(Logic Contract)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LogicV1 {
uint256 public data;
event DataStored(address indexed sender, uint256 data);
function storeData(uint256 _data) public {
data = _data;
emit DataStored(msg.sender, _data);
}
}
代理合约(Proxy Contract)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
// EIP-1967 Slots
bytes32 private constant IMPLEMENTATION_SLOT = keccak256("eip1967.proxy.implementation") - 1;
bytes32 private constant ADMIN_SLOT = keccak256("eip1967.proxy.admin") - 1;
constructor(address _implementation) {
_setAdmin(msg.sender);
_setImplementation(_implementation);
}
modifier onlyAdmin() {
require(msg.sender == _getAdmin(), "Proxy: Not admin");
_;
}
function _getAdmin() internal view returns (address admin) {
bytes32 slot = ADMIN_SLOT;
assembly {
admin := sload(slot)
}
}
function _setAdmin(address _admin) internal {
bytes32 slot = ADMIN_SLOT;
assembly {
sstore(slot, _admin)
}
}
function _getImplementation() internal view returns (address impl) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
impl := sload(slot)
}
}
function _setImplementation(address _impl) internal {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
sstore(slot, _impl)
}
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_setImplementation(_newImplementation);
}
fallback() external payable {
_delegate(_getImplementation());
}
receive() external payable {
_delegate(_getImplementation());
}
function _delegate(address _impl) internal virtual {
assembly {
// 将输入复制到内存
calldatacopy(0, 0, calldatasize())
// 执行 delegatecall
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
// 获取返回数据
returndatacopy(0, 0, returndatasize())
// 根据结果决定是否回退
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
部署与升级流程
- 部署 Logic 合约(LogicV1)。
- 部署 Proxy 合约,传入 LogicV1 的地址作为实现合约。
- 通过 Proxy 合约调用
storeData
函数:代理合约转发调用到 LogicV1,存储数据在 Proxy 的存储中。 - 升级逻辑:
- 部署 Logic 合约的新版本(LogicV2)。
- 以管理员身份调用 Proxy 合约的
upgradeTo
函数,更新实现合约地址为 LogicV2。
- 通过 Proxy 合约调用新逻辑:Proxy 现在转发调用到 LogicV2,实现合约逻辑的升级。
可升级合约
**UUPS Proxy(Universal Upgradeable Proxy Standard)**是一种轻量级且高效的代理模式,允许智能合约在部署后进行逻辑升级。与传统的透明代理(Transparent Proxy)不同,UUPS Proxy的升级逻辑由 实现合约 自身管理,而不是由代理合约处理。这种设计减少了代理合约的复杂性和存储需求,提高了灵活性和效率。
为什么选择 UUPS Proxy?
- 节省存储空间:由于升级逻辑在实现合约中,代理合约本身更为简洁,节省了存储空间。
- 降低 Gas 成本:更简洁的代理合约意味着部署和交互时的 Gas 成本更低。
- 灵活性高:升级逻辑集中在实现合约,便于管理和扩展。
UUPS Proxy 的工作流程可以分为三个主要阶段:部署、调用和升级。以下将详细解释每个阶段。
部署阶段
部署实现合约(Implementation Contract):
- 实现合约包含实际的业务逻辑。
- 使用 OpenZeppelin 的
UUPSUpgradeable
和OwnableUpgradeable
基类,确保安全的升级机制。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract ImplementationV1 is UUPSUpgradeable, OwnableUpgradeable { uint256 public data; event DataStored(address indexed sender, uint256 data); function initialize() initializer public { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); } function storeData(uint256 _data) public { data = _data; emit DataStored(msg.sender, _data); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }
部署代理合约(Proxy Contract):
- 使用 OpenZeppelin 的
ERC1967Proxy
实现,指向初始实现合约并进行初始化。 - 代理合约负责接收用户调用,并通过
delegatecall
将调用转发给实现合约。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract ProxyUUPS is ERC1967Proxy { uint256 public data; constructor(address _logic, bytes memory _data) ERC1967Proxy(_logic, _data) {} receive() external payable { } }
部署步骤:
- 部署
ImplementationV1
合约。 - 准备初始化数据,调用
initialize
函数(如果需要)。 - 部署
ProxyUUPS
合约,传入ImplementationV1
的地址和初始化数据。
使用 Hardhat 部署示例:
const { ethers, upgrades } = require("hardhat"); async function main() { // 部署 ImplementationV1 const ImplementationV1 = await ethers.getContractFactory("ImplementationV1"); const implementationV1 = await ImplementationV1.deploy(); await implementationV1.deployed(); console.log("ImplementationV1 deployed to:", implementationV1.address); // 部署 ProxyUUPS,并初始化 ImplementationV1 const ProxyUUPS = await ethers.getContractFactory("ProxyUUPS"); const proxy = await ProxyUUPS.deploy(implementationV1.address, "0x"); await proxy.deployed(); console.log("ProxyUUPS deployed to:", proxy.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
- 使用 OpenZeppelin 的
调用阶段
用户与代理合约交互:
- 用户通过代理合约地址与合约交互,调用如
storeData
和getData
等函数。
- 用户通过代理合约地址与合约交互,调用如
代理合约转发调用:
- 代理合约的
fallback
函数捕获所有调用,并通过delegatecall
转发给当前实现合约(ImplementationV1
)。
- 代理合约的
实现合约执行逻辑:
- 实现合约逻辑在代理合约的存储和上下文中执行,修改代理合约的状态变量。
事件日志记录:
- 事件由代理合约触发,记录在代理合约的地址下,
msg.sender
为原始调用者。
调用示例:
使用 Ethers.js 与代理合约交互,调用
storeData
函数:const { ethers } = require("ethers"); // 连接到以太坊节点 const provider = new ethers.providers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_INFURA_PROJECT_ID"); const signer = provider.getSigner(); // 代理合约地址和 ABI const proxyAddress = "0xProxyAddress"; const proxyABI = [ "function storeData(uint256 _data)", "function getData() view returns (uint256)", "event DataStored(address indexed sender, uint256 data)" ]; // 创建合约实例 const proxyContract = new ethers.Contract(proxyAddress, proxyABI, signer); // 调用 storeData async function storeDataExample() { const tx = await proxyContract.storeData(42); await tx.wait(); console.log("Data stored successfully"); } storeDataExample();
- 事件由代理合约触发,记录在代理合约的地址下,
升级阶段
部署新版本实现合约(Implementation Contract):
- 部署包含新逻辑的实现合约(
ImplementationV2
)。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract ImplementationV2 is UUPSUpgradeable, OwnableUpgradeable { uint256 public data; string public message; event DataAndMessageStored(address indexed sender, uint256 data, string message); function initializeV2() reinitializer(2) public { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); } function storeData(uint256 _data, string memory _message) public { data = _data; message = _message; emit DataAndMessageStored(msg.sender, _data, _message); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }
- 部署包含新逻辑的实现合约(
执行代理合约的升级:
- 作为拥有者,通过调用实现合约中的升级函数,将代理指向新的实现合约。
const { ethers, upgrades } = require("hardhat"); async function upgrade() { const proxyAddress = "0xProxyAddress"; const ImplementationV2 = await ethers.getContractFactory("ImplementationV2"); await upgrades.upgradeProxy(proxyAddress, ImplementationV2); console.log("Proxy upgraded to ImplementationV2"); } upgrade() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); });
使用新版本实现合约:
- 用户通过代理合约地址调用新功能,调用将通过
delegatecall
转发到ImplementationV2
。 - 新的函数如
storeData(uint256, string)
被执行,状态变量和事件更新。
调用新功能:
async function storeDataV2Example() { const tx = await proxyContract.storeData(100, "Hello, UUPS!"); await tx.wait(); console.log("Data and message stored successfully"); } storeDataV2Example();
- 用户通过代理合约地址调用新功能,调用将通过