事件是能方便地调用以太坊虚拟机日志功能的接口。应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。
重点:记录区块链的日志,可以使用状态变量,也可以使用事件 Event,但 Event 使用的 gas 费比状态变量低。
原则:改变状态变量时,一定要触发事件。
Soliddity Event 事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中。这些日志与合约的地址关联,并记录到区块链中。每个交易收据包含 0 到多个 log 记录,log 表明着智能合约所触发的事件。
1️⃣ Event 语法
事件的定义:使用 event
关键字来定义一个事件 Event,语法如下:
event EventName(<parameter list>);
事件的触发:只能使用 emit
关键字来触发事件 Event,语法如下:
emit EventName(<parameter list>);
2️⃣ 四种事件定义方式
- 不带参数的 event
- 带参数的 event
- 带参数名的 event
- 带 indexed 参数名的 event
- 这种事件也被称为索引事件
- 语法:
event EventName(TypeName indexed varibleName....);
- 事件中 indexed 标记过的参数,可以在链外进行搜索查询。
- 一个事件中 indexed 标记过的参数最多有 3 个。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Event {
// 普通 event
event Log1(address, string);
// 带名字的 event
event Log2(address ads, string msg);
// 带 indexed 的event
event Log3(address indexed ads, string msg);
// indexed 在一个事件内使用次数不能超过3次
event Transfer(
address indexed from,
address indexed to,
uint256 indexed amount
);
function log1() external {
emit Log1(msg.sender, "Log111");
}
function log2() external {
emit Log2(msg.sender, "Log222");
}
function log3() external {
emit Log3(msg.sender, "Log333");
}
function transfer(address _to, uint256 amount) external {
emit Transfer(msg.sender, _to, amount);
}
}
1 不带参数的 event
|
|
2 带参数的 event
|
|
3 带参数名的 event
|
|
4 带 indexed 参数名的 event
|
|
|
|
3️⃣ indexed 的作用
indexed 数据会被记录到 topics
中,可以用于检索。已索引的部分,最多有 3 个(对于非匿名事件)或 4 个(对于匿名事件)
对于非匿名事件,最多三个参数可以接收 indexed
属性(它是一个特殊的名为: “主题” 的数据结构,而不作为日志的数据部分)。主题仅有 32 字节, 因此如果:引用类型 标记为索引项,则它们的 keccak-256 哈希值会被作为 主题(topic) 保存。
主题(topic)让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。
例如, 使用如下的 web3.js subscribe("logs")方法
去过滤符合特定地址的 主题(topic) :
|
|
主要用在链下服务,可以通过 RPC 获取,比如 web3 的以下方法:
myContract.once
myContract.events.MyEvent
myContract.getPastEvents
4️⃣ log 的使用
除非你用 anonymous
声明事件,否则事件签名的哈希值是一个 主题(topic)。同时也意味着对于匿名事件无法通过名字来过滤,仅能按合约地址过滤。匿名事件的优势是他们部署和调用的成本更低。它也允许你声明 4 个索引参与而不是 3 个。
⚠️:由于交易日志只存储事件数据而不存储类型。你必须知道事件的类型,包括哪个参数被索引,以及该事件是否是匿名的,以便正确解释数据。尤其是,有可能使用一个匿名事件来"伪造"另一个事件的签名。
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// 事件使用 emit 触发事件。
// 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
emit Deposit(msg.sender, id, msg.value);
}
}
使用 JavaScript API 调用事件的用法如下:
|
|
上面的输出如下所示(有删减):
|
|
5️⃣ Log 重载
Log 可以像函数一样重载
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Event {
event Log(address ads);
event Log(address indexed ads, string msg); // 重载
function log1() external {
emit Log(msg.sender);
}
function log2() external {
emit Log(msg.sender, "Log111");
}
}
🆗 实战: 众筹合约
合约代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Croedfund {
/* ============ Type Declaration ============ */
// 出资人角色
// * 仅需记录地址/金额即可
struct Donor {
address addr; //出资人地址
uint256 amount; //出资人金额
}
// 募资人角色
// * 用于表示一个募资项目,其中包括募资人地址、目标金额、
// 已筹集金额、捐赠者人数、项目状态以及所有的出资人。
struct Donee {
address creator; // 募资人地址
uint256 goal; // 众筹目标数量
uint32 startAt; // 开始时间
uint32 endAt; // 结束时间
bool claimed; // 是否被领取
uint256 amount; // 已筹集金额
uint256 donorCount; // * 捐赠者人数
mapping(uint256 => Donor) donorMap; // * 出资人字典
}
/* ============ State Variables ============ */
address payable owner; //合约拥有者
uint256 public doneeCount; // 募资人数量
mapping(uint256 => Donee) public doneeMap; //募资人字典
/* ============ Events ============ */
event Launch(
uint256 id,
address indexed creator,
uint256 goal,
uint32 startAt,
uint32 endAt
);
event Cancel(uint256 id);
event Donate(uint256 indexed id, address indexed caller, uint256 amount);
event Unpledge(uint256 indexed id, address indexed caller, uint256 amount);
event Claim(uint256 id, address creator, uint256 amount);
event Refund(uint256 indexed id, address indexed caller, uint256 amount);
/* ============ Modifier ============ */
modifier onlyOwner() {
require(msg.sender == owner, "only owner");
_;
}
// 验证募捐活动ID是否有效
modifier validDonee(uint256 doneeID) {
require(doneeID > 0 && doneeID <= doneeCount);
_;
}
/* ============ Errors ============ */
error MyError(string);
/* ============ Constructor ============ */
constructor() {
owner = payable(msg.sender);
}
/* ============ Functions ============ */
// 启动新众筹
function launch(
address _addr,
uint256 _goal,
uint32 _startAt,
uint32 _endAt
) external onlyOwner {
require(_startAt >= block.timestamp, "start at < now");
require(_startAt <= _endAt, "start at > end at");
require(_endAt <= block.timestamp + 30 days, "end at > max duration");
doneeCount++;
Donee storage donee = doneeMap[doneeCount];
donee.creator = _addr;
donee.goal = _goal;
donee.startAt = _startAt;
donee.endAt = _endAt;
emit Launch(doneeCount, msg.sender, _goal, _startAt, _endAt);
}
// 取消指定ID的众筹
function cancel(uint256 _id) external onlyOwner {
// 不需要修改,需用 memeory ,但是包含mapping类型,所以需要用 storage
Donee storage campaign = doneeMap[_id];
require(block.timestamp < campaign.startAt, "started"); // 必须还没有开始
delete doneeMap[_id];
emit Cancel(_id);
}
// 出资人捐赠
function donate(uint256 _id) external payable validDonee(_id) {
Donee storage donee = doneeMap[_id]; // 需要修改,所以使用 storage
require(block.timestamp >= donee.startAt, "not start"); //
require(block.timestamp <= donee.endAt, "ended"); //
donee.donorCount++;
donee.amount += msg.value;
Donor storage donor = donee.donorMap[donee.donorCount];
donor.addr = msg.sender;
donor.amount = msg.value;
emit Donate(_id, msg.sender, msg.value);
}
// 完成目标给募资人转账
function transfer(uint256 doneeID) public onlyOwner validDonee(doneeID) {
Donee storage donee = doneeMap[doneeID];
require(!donee.claimed, "is claimed");
require(block.timestamp >= donee.endAt, "not ended");
require(donee.amount >= donee.goal, "amount < goal");
// 设置已经支付的状态
donee.claimed = true;
// 给募资人转账
payable(donee.creator).transfer(donee.goal);
emit Claim(doneeID, msg.sender, donee.amount);
}
/* ============ Helper ============ */
fallback() external {}
receive() external payable {}
// 获取当前合约的余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// 合约的余额转账到拥有者
function withdraw(uint256 doneeID) public onlyOwner {
Donee storage donee = doneeMap[doneeID];
require(donee.claimed, "not claimed");
require(block.timestamp >= donee.endAt, "not ended");
payable(msg.sender).transfer(address(this).balance);
}
// 获取项目状态
function getStatus(uint256 doneeID)
public
view
validDonee(doneeID)
returns (bool)
{
Donee storage donee = doneeMap[doneeID];
return (block.timestamp >= donee.startAt &&
block.timestamp <= donee.endAt);
}
}
测试合约
- address1 launch 一次活动
- goal 为 10
- 时间戳获取: https://tool.chinaz.com/tools/unixtime.aspx
- doneeMap 查询 id 1 信息
- getStatus 查询 id 1 是否开始
- address2 donate 6
- address3 donate 7
- doneeMap 查询 id 1 信息