事件

事件是能方便地调用以太坊虚拟机日志功能的接口。应用程序可以通过以太坊客户端的 RPC 接口订阅和监听这些事件。

重点:记录区块链的日志,可以使用状态变量,也可以使用事件 Event,但 Event 使用的 gas 费比状态变量低。

原则:改变状态变量时,一定要触发事件。

Soliddity Event 事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中。这些日志与合约的地址关联,并记录到区块链中。每个交易收据包含 0 到多个 log 记录,log 表明着智能合约所触发的事件。

Event 语法

事件的定义:使用 event 关键字来定义一个事件 Event,语法如下:

event EventName(<parameter list>);

事件的触发:只能使用 emit 关键字来触发事件 Event,语法如下:

emit EventName(<parameter list>);

四种事件定义方式

  1. 不带参数的 event

  2. 带参数的 event

  3. 带参数名的 event

  4. 带 indexed 参数名的 event

    1. 这种事件也被称为索引事件

    2. 语法:event EventName(TypeName indexed varibleName....);

    3. 事件中 indexed 标记过的参数,可以在链外进行搜索查询。

    4. 一个事件中 indexed 标记过的参数最多有 3 个。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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);
    }
}

不带参数的 event

[
	{
		"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
		"topic": "0x1732d0c17008d342618e7f03069177d8d39391d79811bb4e706d7c6c84108c0f",
		"event": "Log1",
		"args": {}
	}
]

带参数的 event

[
	{
		"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
		"topic": "0x54010eb0426bdddd13273086604fca7ba750a84093c6839732d954056646e81b",
		"event": "Log2",
		"args": {
			"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"1": "Log222"
		}
	}
]

带参数名的 event

[
	{
		"from": "0x7874d94b8f9E2a28FCceCE404666C984f33a82b8",
		"topic": "0x940879bf2d29cdfe8084f2f033d2168f5859a6e10530b61fb84dc1c5ddc9ca40",
		"event": "Log3",
		"args": {
			"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"1": "Log333",
			"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"msg": "Log333"
		}
	}
]

带 indexed 参数名的 event

[
	{
		"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
		"topic": "0xf485c071883274befba21423da7f60203f9df753bf614bca26c4763ed4b240fb",
		"event": "Log4",
		"args": {
			"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"1": "Log444",
			"ads": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"msg": "Log444"
		}
	}
]
[
	{
		"from": "0xfB72aAdB17a855D27A68B565ee0a84CB30A387e4",
		"topic": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
		"event": "Transfer",
		"args": {
			"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"1": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"2": "1",
			"from": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"to": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"amount": "1"
		}
	}
]

indexed 的作用

indexed 数据会被记录到 topics 中,可以用于检索。已索引的部分,最多有 3 个(对于非匿名事件)或 4 个(对于匿名事件)

对于非匿名事件,最多三个参数可以接收 indexed属性(它是一个特殊的名为: “主题” 的数据结构,而不作为日志的数据部分)。主题仅有 32 字节, 因此如果:引用类型 标记为索引项,则它们的 keccak-256 哈希值会被作为 主题(topic) 保存。

主题(topic)让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。

例如, 使用如下的 web3.js subscribe("logs")方法 去过滤符合特定地址的 主题(topic) :

var options = {
	fromBlock: 0,
	address: web3.eth.defaultAccount,
	topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null],
};
web3.eth
	.subscribe("logs", options, function (error, result) {
		if (!error) console.log(result);
	})
	.on("data", function (log) {
		console.log(log);
	})
	.on("changed", function (log) {});

主要用在链下服务,可以通过 RPC 获取,比如 web3 的以下方法:

  • myContract.once

    • https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html

  • myContract.events.MyEvent

    • https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html#contract-events

  • myContract.getPastEvents

    • https://web3js.readthedocs.io/en/v1.7.5/web3-eth-contract.html#getpastevents

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 调用事件的用法如下:

var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);

var depositEvent = clientReceipt.Deposit();

// 监听变化
depositEvent.watch(function(error, result) {
    // 结果包含 非索引参数 以及 主题 topic
    if (!error)
        console.log(result);
});

// 或者通过传入回调函数,立即开始听监
var depositEvent = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

上面的输出如下所示(有删减):

{
	"returnValues": {
		"from": "0x1111…FFFFCCCC",
		"id": "0x50…sd5adb20",
		"value": "0x420042"
	},
	"raw": {
		"data": "0x7f…91385",
		"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
	}
}

Log 重载

Log 可以像函数一样重载

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

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.18;

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 一次活动

  • doneeMap 查询 id 1 信息

  • getStatus 查询 id 1 是否开始

  • address2 donate 6

  • address3 donate 7

  • doneeMap 查询 id 1 信息