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    }

程序的整体思路:

  1. 部署合约时,构造函数调用,传入所有者列表和所需确认数。

  2. receive 函数用于接收以太币,并记录到日志中。

  • 以太币是通过调用合约的 transfer 函数转入的。

  • external 表示该函数只能通过合约外部调用,不能通过合约内部调用。

  • payable 表示该函数可以接收以太币。

    实际上,如果不使用 payable 修饰,调用的时候会执行额外检查,确保 msg.value == 0 才会执行。因此从现象上看,加上 payable 反而让 gas 降低一丢丢。

  1. submitTransaction 函数用于提交交易,记录到日志中。transactions 是一个动态数组。调用这个函数会创建一个新数组项,除此之外什么也不做。调用者可以利用这个函数创建交易,得到 txIndex。

  2. confirmTransaction 确认交易,作用是增加确认数。为了避免被别人确认,使用 onlyOwner 修饰器限制只有所有者才能调用。同时,使用 txExists 修饰器限制只能对已存在的交易进行确认。使用 notExecuted 修饰器限制只能对未执行的交易进行确认。使用 notConfirmed 修饰器限制只能对未确认的交易进行确认。

  3. 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);
    
  4. revokeConfirmation 取消确认。只能对已确认的交易进行取消确认。

剩下的函数就比较简单了。我们就到这里。