Solidity 合约分析:1. Ether Wallet
介绍
你好,我是 Pluveto。我发现多数 Solidity 的教程都是从语法开始讲起,但是我觉得这样不够直观,而且很消耗耐心。我想先从一个简单的合约开始,然后一步步分析它的代码,让你能够直观地理解 Solidity 的语法。这是一个系列文章,我会在这个系列里分析一些简单的 Solidity 合约。
希望你能学到东西,让我们开始吧!
代码
本期例子来源:Multi-Sig Wallet | Solidity by Example | 0.8.20
这是一个多重签名钱包的智能合约,它可以让多个所有者共同管理一个以太坊账户,并且需要一定数量的所有者同意才能执行交易
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.20;
3
4contract MultiSigWallet {
5 // 这里定义了一些合约的事件,用于记录合约的活动,如存款、提交交易、确认交易、撤销确认和执行交易。
6 event Deposit(address indexed sender, uint amount, uint balance);
7 event SubmitTransaction(
8 address indexed owner,
9 uint indexed txIndex,
10 address indexed to,
11 uint value,
12 bytes data
13 );
14 event ConfirmTransaction(address indexed owner, uint indexed txIndex);
15 event RevokeConfirmation(address indexed owner, uint indexed txIndex);
16 event ExecuteTransaction(address indexed owner, uint indexed txIndex);
event 是一个用于记录合约活动的接口,它可以将事件名、参数等存储到交易的日志中,并且可以被外部的监听器捕获和处理。
注意:只能通过触发事件的方式将数据记录到日志中,而无法直接从合约中读取日志数据。这是因为日志数据的主要用途是在链下读取和分析,而不是在合约内部使用。
日志索引:为了提高事件日志的查询效率,区块链通常会对日志数据进行索引。索引可以根据事件名称、合约地址和其他关键字段来加速日志的查询。通过索引,可以快速定位和检索特定事件的日志数据。
日志和状态变量存放的方式相同吗?
合约的状态变量以一种紧凑的方式存储在区块链存储中,有时多个值会使用同一个存储槽. 除了动态大小的数组和映射mapping,数据的存储方式是从位置0开始连续放置在存储storage中. 对于每个变量,根据其类型确定字节大小。存储大小少于32字节的多个变量会被打包到一个存储插槽storage slot中.
状态变量在储存中的布局 — Solidity中文文档 — 登链社区
至于日志,日志是交易收据(transaction receipts)的一部分。它们由客户端在执行交易时生成,并与区块链一起存储以允许检索它们。日志本身不是区块链本身的一部分,因为它们不是共识所必需的(它们只是历史数据),但是它们由区块链验证,因为交易收据哈希存储在区块内。
交易收据的结构:
Object - A transaction receipt object, or null when no receipt was found:
-
块哈希(blockHash: String, 32 Bytes): 该交易所在的块的哈希。
-
块号(blockNumber: Number): 该交易所在的块的编号。
-
交易哈希(transactionHash: String, 32 Bytes): 该交易的哈希。
-
交易索引(transactionIndex: Number): 该交易在块中的索引位置。
-
发起方(from: String, 20 Bytes): 发送方地址。
-
接收方(to: String, 20 Bytes): 接收方地址。如果是合约创建交易则为null。
-
累计使用量(cumulativeGasUsed: Number): 在该块执行该交易时总共使用的gas量。
-
使用量(gasUsed: Number): 该交易本身使用的gas量。
-
合约地址(contractAddress: String, 20 Bytes): 如果交易是合约创建,则为创建的合约地址,否则为null。
-
日志(logs: Array): 该交易产生的日志对象数组。
-
状态(status: String): ‘0x0’表示交易失败,‘0x1’表示交易成功。
solidity - Where do contract event logs get stored in the Ethereum architecture? blockchain - Relationship between Transaction Trie and Receipts Trie - Ethereum Stack Exchange
1 // 所有者列表、所有者映射、所需确认数、交易结构、交易数组和交易确认映射。
2 address[] public owners;
3 mapping(address => bool) public isOwner;
4 uint public numConfirmationsRequired;
5
6 struct Transaction {
7 address to;
8 uint value;
9 bytes data;
10 bool executed;
11 uint numConfirmations;
12 }
13
14 // mapping from tx index => owner => bool
15 mapping(uint => mapping(address => bool)) public isConfirmed;
16
17 Transaction[] public transactions;
18
19 modifier onlyOwner() {
20 require(isOwner[msg.sender], "not owner");
21 _;
22 }
23
24 modifier txExists(uint _txIndex) {
25 require(_txIndex < transactions.length, "tx does not exist");
26 _;
27 }
28
29 modifier notExecuted(uint _txIndex) {
30 require(!transactions[_txIndex].executed, "tx already executed");
31 _;
32 }
33
34 modifier notConfirmed(uint _txIndex) {
35 require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
36 _;
37 }
38
39 constructor(address[] memory _owners, uint _numConfirmationsRequired) {
40 require(_owners.length > 0, "owners required");
41 require(
42 _numConfirmationsRequired > 0 &&
43 _numConfirmationsRequired <= _owners.length,
44 "invalid number of required confirmations"
45 );
46
47 for (uint i = 0; i < _owners.length; i++) {
48 address owner = _owners[i];
49
50 require(owner != address(0), "invalid owner");
51 require(!isOwner[owner], "owner not unique");
52
53 isOwner[owner] = true;
54 owners.push(owner);
55 }
56
57 numConfirmationsRequired = _numConfirmationsRequired;
58 }
59
60 receive() external payable {
61 emit Deposit(msg.sender, msg.value, address(this).balance);
62 }
63
64 function submitTransaction(
65 address _to,
66 uint _value,
67 bytes memory _data
68 ) public onlyOwner {
69 uint txIndex = transactions.length;
70
71 transactions.push(
72 Transaction({
73 to: _to,
74 value: _value,
75 data: _data,
76 executed: false,
77 numConfirmations: 0
78 })
79 );
80
81 emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
82 }
83
84 function confirmTransaction(
85 uint _txIndex
86 ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) notConfirmed(_txIndex) {
87 Transaction storage transaction = transactions[_txIndex];
88 transaction.numConfirmations += 1;
89 isConfirmed[_txIndex][msg.sender] = true;
90
91 emit ConfirmTransaction(msg.sender, _txIndex);
92 }
93
94 function executeTransaction(
95 uint _txIndex
96 ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
97 Transaction storage transaction = transactions[_txIndex];
98
99 require(
100 transaction.numConfirmations >= numConfirmationsRequired,
101 "cannot execute tx"
102 );
103
104 transaction.executed = true;
105
106 (bool success, ) = transaction.to.call{value: transaction.value}(
107 transaction.data
108 );
109 require(success, "tx failed");
110
111 emit ExecuteTransaction(msg.sender, _txIndex);
112 }
113
114 function revokeConfirmation(
115 uint _txIndex
116 ) public onlyOwner txExists(_txIndex) notExecuted(_txIndex) {
117 Transaction storage transaction = transactions[_txIndex];
118
119 require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
120
121 transaction.numConfirmations -= 1;
122 isConfirmed[_txIndex][msg.sender] = false;
123
124 emit RevokeConfirmation(msg.sender, _txIndex);
125 }
126
127 function getOwners() public view returns (address[] memory) {
128 return owners;
129 }
130
131 function getTransactionCount() public view returns (uint) {
132 return transactions.length;
133 }
134
135 function getTransaction(
136 uint _txIndex
137 )
138 public
139 view
140 returns (
141 address to,
142 uint value,
143 bytes memory data,
144 bool executed,
145 uint numConfirmations
146 )
147 {
148 Transaction storage transaction = transactions[_txIndex];
149
150 return (
151 transaction.to,
152 transaction.value,
153 transaction.data,
154 transaction.executed,
155 transaction.numConfirmations
156 );
157 }
158}
onlyOwner 是一个修饰器(modifier),用来限制必须所有者才能调用此函数。算是一种语法糖,避免大量重复的检查代码。
1 modifier onlyOwner() {
2 require(isOwner[msg.sender], "not owner");
3 _;
4 }
程序的整体思路:
-
部署合约时,构造函数调用,传入所有者列表和所需确认数。
-
receive 函数用于接收以太币,并记录到日志中。
-
以太币是通过调用合约的 transfer 函数转入的。
-
external 表示该函数只能通过合约外部调用,不能通过合约内部调用。
-
payable 表示该函数可以接收以太币。
实际上,如果不使用 payable 修饰,调用的时候会执行额外检查,确保 msg.value == 0 才会执行。因此从现象上看,加上 payable 反而让 gas 降低一丢丢。
-
submitTransaction 函数用于提交交易,记录到日志中。transactions 是一个动态数组。调用这个函数会创建一个新数组项,除此之外什么也不做。调用者可以利用这个函数创建交易,得到 txIndex。
-
confirmTransaction 确认交易,作用是增加确认数。为了避免被别人确认,使用 onlyOwner 修饰器限制只有所有者才能调用。同时,使用 txExists 修饰器限制只能对已存在的交易进行确认。使用 notExecuted 修饰器限制只能对未执行的交易进行确认。使用 notConfirmed 修饰器限制只能对未确认的交易进行确认。
-
executeTransaction 执行具体的转账。会检查确认数是否达标
1 (bool success, ) = transaction.to.call{value: transaction.value}( 2 transaction.data 3 );
这一步转账。transaction.to 是目标地址,call 方法是调用合约。address.call{value} 用于调用合约并转账。 省略的返回值:
1(bool success, bytes memory returnData) = address.call(data);
-
revokeConfirmation 取消确认。只能对已确认的交易进行取消确认。
剩下的函数就比较简单了。我们就到这里。