Solidity 官网
github
Solidity是一种静态类型的花括号(curly-braces
)编程语言,旨在开发运行在以太坊(Ethereum
)上的智能合约。
Solidity正在迅速发展
作为一门相对年轻的语言,Solidity正在迅速发展。我们的目标是每月定期发布(非突破性)版本,每年大约发布一个突破性版本。您可以在Solidity Github项目中跟踪新功能的实现状态。通过从默认分支(develop
)切换到breaking branch,您可以看到下一个破坏性版本即将发生的更改。您可以通过提供输入并参与语言设计来积极地塑造Solidity。
一、入门 (v0.8.17
)
Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态下账户行为的程序。
Solidity是一种以以太坊虚拟机(Ethereum Virtual Machine, EVM)为目标的大括号语言(curly-bracket language )。它受到c++, Python和JavaScript的影响。您可以在语言影响部分中找到有关Solidity受到哪些语言启发的更多细节。
Solidity是静态类型的,支持继承、库和复杂的用户定义类型。
使用Solidity,您可以创建用于投票、众筹、盲拍和多签名钱包等用途的合同。
在部署合约时,您应该使用最新发布的Solidity版本。除特殊情况外,只有最新版本才会收到安全补丁。此外,还会定期引入突破性的变化和新特性。我们目前使用的是0.y.z
版本号表示这种快速的更改速度。
1.1 了解智能合约基础知识
如果你对智能合约的概念不熟悉,我们建议你从“智能合约介绍”部分开始,其中包括:
- 一个用Solidity写的智能合约的简单例子。
- 区块链基础知识。
- 以太坊虚拟机。
1.2 了解Solidity
一旦你熟悉了基础,我们建议你阅读Solidity by Example和“语言描述”部分来理解语言的核心概念。
1.3 安装Solidity编译器
安装Solidity编译器有多种方法,只需选择您喜欢的选项,并按照安装页面上列出的步骤进行安装。
您可以使用Remix IDE直接在浏览器中尝试代码示例。Remix是一个基于web浏览器的IDE,允许您编写、部署和管理Solidity智能合约,而不需要在本地安装Solidity。
Warning
当人们编写软件时,它可能会有bug。在编写智能合约时,您应该遵循已建立的软件开发最佳实践。这包括代码审查、测试、审核和正确性证明。智能合约用户有时比他们的作者对代码更有信心,区块链和智能合约有自己独特的问题要注意,所以在使用生产代码之前,一定要阅读安全注意事项部分。
1.4 Learn More
如果你想了解更多关于在以太坊上构建去中心化应用程序的知识,以太坊开发者资源可以帮助你进一步了解以太坊的一般文档,以及广泛的教程、工具和开发框架。
如果您有任何问题,您可以尝试在以太坊StackExchange或我们的Gitter频道上搜索答案或询问。
Keyword Index
二、智能合约简介
2.1 一个简单的智能合约
让我们从一个基本示例开始,该示例设置一个变量的值,并将其公开给其他合约访问。如果您现在还不明白也没关系,我们稍后再详细说明。
2.1.1 Storage Example
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.4.16 <0.9.0;contract SimpleStorage {uint storedData;function set(uint x) public {storedData = x;}function get() public view returns (uint) {return storedData;}}
第一行告诉您源代码是在GPL 3.0版本下授权的。在默认发布源代码的设置中,机器可读许可说明符(license specifiers)非常重要。
下一行指定源代码是为Solidity版本0.4.16编写的,或该语言的更新版本,但不包括版本0.9.0。这是为了确保合约不能用新的(破坏的)编译器版本编译,在那里它可能会有不同的行为。pragma是编译器关于如何处理源代码的常用指令(例如pragma once
)。
从Solidity 的意义上讲,合约是位于以太坊区块链上特定地址的代码(其functions
)和数据(其state
)的集合。行uint storedData;
声明一个名为storedData
的状态变量,类型为uint
(256位无符号整数)。您可以把它看作数据库中的一个插槽(slot
),您可以通过调用管理数据库的函数来查询和修改它。在本例中,合约定义了可用于修改或检索变量值的函数set
和get
。
要访问当前合约的成员(如状态(state
)变量),通常不需要添加this.
前缀,你可以通过它的名字直接访问它。与其他一些语言不同的是,省略它不仅仅是一个风格问题,它导致了一种完全不同的访问成员的方式,但这将在后面详细介绍。
这个合约除了(由于以太坊构建的基础设施)允许任何人存储一个世界上任何人都可以访问的单个数字之外,没有(可行的)方法来阻止你发布这个数字。任何人都可以用不同的值再次调用set
并覆盖您的号码,但号码仍然存储在区块链的历史记录中。稍后,您将看到如何施加访问限制,以便只有您可以更改数字。
警告
使用Unicode文本时要小心,因为外观相似(甚至相同)的字符可能有不同的代码点,因此会被编码为不同的字节数组。
所有标识符(合约名、函数名和变量名)都限制在ASCII字符集内。可以将UTF-8编码的数据存储在字符串变量中。
2.1.2 子货币(Subcurrency)例子
下面的合约实现了最简单的加密货币形式。该合约只允许其创造者创造新的货币(可能有不同的发行方案)。任何人都可以互相发送硬币,而不需要注册用户名和密码,你所需要的只是一个以太坊对。
// SPDX-License-Identifier: GPL-3.0pragma solidity ^0.8.4;contract Coin {// The keyword "public" makes variables// accessible from other contractsaddress public minter;mapping (address => uint) public balances;// Events allow clients to react to specific// contract changes you declareevent Sent(address from, address to, uint amount);// Constructor code is only run when the contract// is createdconstructor() {minter = msg.sender;}// Sends an amount of newly created coins to an address// Can only be called by the contract creatorfunction mint(address receiver, uint amount) public {require(msg.sender == minter);balances[receiver] += amount;}// Errors allow you to provide information about// why an operation failed. They are returned// to the caller of the function.error InsufficientBalance(uint requested, uint available);// Sends an amount of existing coins// from any caller to an addressfunction send(address receiver, uint amount) public {if (amount > balances[msg.sender])revert InsufficientBalance({requested: amount,available: balances[msg.sender]});balances[msg.sender] -= amount;balances[receiver] += amount;emit Sent(msg.sender, receiver, amount);}}
这份合同引入了一些新概念,让我们一个一个地看一看。
address public minter;
声明一个address类型的状态变量。address
类型是一个160位的值,不允许任何算术操作。它适用于存储合约的地址,或属于外部帐户的keypair 的公共一半的散列。
关键字public
自动生成一个函数,允许您从合约外部访问状态变量的当前值。如果没有这个关键字,其他合约就无法访问这个变量。编译器生成的函数代码相当于以下内容(暂时忽略external
和view
):
function minter() external view returns (address) { return minter; }
你可以自己添加一个像上面那样的函数,但是你会有一个同名的函数和状态变量。你不需要这样做,编译器会为你计算出来。
下一行mapping (address => uint) public balances;
也创建一个公共状态变量,但它是一个更复杂的数据类型。映射 mapping 类型将地址映射为无符号整数(unsigned integers)。
映射可以看作是虚拟初始化的哈希表( hash tables),这样每个可能的键从一开始就存在,并映射到一个字节表示为全零的值。但是,既不可能获得映射的所有键的列表,也不可能获得所有值的列表。记录您添加到映射中的内容,或者在不需要的上下文中使用它。或者更好的是,保留一个列表,或者使用更合适的数据类型。
在映射的情况下,由public
关键字创建的getter函数更加复杂。如下图所示:
function balances(address account) external view returns (uint) {return balances[account];}
支持查询单个账户的余额情况。
这行event Sent(address from, address to, uint amount);
声明一个“事件”,该事件在函数send
的最后一行中发出。以太坊客户端(如web应用程序)可以在没有太多成本的情况下侦听区块链上发出的这些事件。一旦它被触发,侦听器就会接收from
、to
和amount
参数,这使得跟踪事务成为可能。
为了监听这个事件,你可以使用下面的JavaScript代码,它使用web3.js来创建Coin
合约对象,并且任何用户界面都从上面调用自动生成的balances
函数:
Coin.Sent().watch({}, '', function(error, result) {if (!error) {console.log("Coin transfer: " + result.args.amount +" coins were sent from " + result.args.from +" to " + result.args.to + ".");console.log("Balances now:\n" +"Sender: " + Coin.balances.call(result.args.from) +"Receiver: " + Coin.balances.call(result.args.to));}})
构造函数(constructor
)是一个特殊的函数,在创建合约期间执行,之后不能调用。在这种情况下,它永久存储了创建合同的人的地址。msg
变量(包括tx
和block
)是一个特殊的全局变量,它包含允许访问区块链的属性。msg.sender
始终是当前(外部)函数调用来自于哪的地址。
组成合约的函数以及用户和合约可以调用的函数是mint
和send
。
mint
函数将一定数量新创建的硬币发送到另一个地址。require函数调用定义条件,如果不满足,则恢复所有更改。在本例中,require(msg.sender == minter);
确保只有契约的创建者可以调用mint
。一般来说,创建者可以随心所欲地铸造任意多的代币,但在某种程度上,这将导致一种称为“溢出”的现象。注意,由于默认的Checked arithmetic,如果表达式alances[receiver] += amount;
溢出,事务将回滚,即当任意精度算术中的balances[receiver] + amount
大于 uint
最大值(2**256 - 1
)。对于在函数send
中语句balances[receiver] += amount;
也是如此。
错误允许您向调用者提供关于条件或操作失败原因的更多信息。错误与 revert statement一起使用。revert
语句无条件地中止并恢复与require
函数类似的所有更改,但它还允许您提供错误名称和其他数据,这些数据将提供给调用者(最终提供给前端应用程序或块资源管理器),以便更容易地调试或对故障作出反应。
send
函数可以被任何人(已经拥有一些这些币的人)用来向其他人发送币。如果发送者没有足够的币来发送,if
条件的计算结果为true。结果,revert
操作将导致操作失败,同时使用InsufficientBalance
错误向发送方提供错误详细信息。
注意:
如果您使用此合约将coins 发送到某个地址,当您在区块链资源管理器上查看该地址时,您将看不到任何东西,因为您发送硬币的记录和更改的余额仅存储在此特定coins 合约的数据存储中。通过使用事件,您可以创建一个“区块链资源管理器”来跟踪新硬币的交易和余额,但您必须检查coins 合同地址,而不是coins 所有者的地址。
2.2 区块链基础知识
区块链作为一个概念对于程序员来说并不难理解。原因是大多数复杂技术(挖掘、哈希、椭圆曲线加密、点对点网络等)只是为了为平台提供一组特定的功能和承诺。一旦你接受了这些给定的特性,你就不必担心底层技术——或者你必须知道亚马逊的AWS在内部是如何工作的才能使用它吗?
2.2.1 交易(Transactions
)
区块链是一个全球共享的交易数据库。这意味着每个人都可以通过参与网络来读取数据库中的条目。如果您想要更改数据库中的某些内容,则必须创建一个所谓的交易(transaction
),该交易必须被所有其他人接受。交易这个词意味着您想要进行的更改(假设您想同时更改两个值)要么根本没有完成,要么完全应用了。此外,当您的交易被应用到数据库时,没有其他交易可以更改它。
例如,假设有一个表列出了电子货币中所有账户的余额。如果请求从一个帐户转到另一个帐户,数据库的交易性质确保如果从一个帐户减去金额,它总是添加到另一个帐户。如果由于某种原因,无法将金额添加到目标帐户,源帐户也不会被修改。
此外,交易总是由发送方(创建者)加密签署的。这使得保护对数据库特定修改的访问变得简单。在电子货币的例子中,一个简单的检查可以确保只有持有账户钥匙的人才能从中转账。
2.2.2 Blocks
需要克服的一个主要障碍是(用比特币术语来说)所谓的“双花攻击”:如果网络中存在两个都想清空一个账户的交易,会发生什么?只有一个交易是有效的,通常是第一个被接受的交易。问题是“第一”在点对点网络中并不是一个客观的术语。
对此的抽象回答是,你不必在意。将为您选择一个全局接受的交易顺序,从而解决冲突。交易将被捆绑到所谓的“块”中,然后它们将被执行并分布到所有参与节点中。如果两笔交易相互矛盾,最后成为第二笔的交易将被拒绝,不会成为区块的一部分。
这些块在时间上形成线性序列,这就是单词“区块链”的来源。区块以相当规律的间隔被添加到链中——对于以太坊来说,这大约是每17秒一次。
作为“顺序选择机制”(称为“mining”)的一部分,可能会发生区块不时被还原的情况,但只是在链的“尖端”。在特定块上添加的块越多,该块恢复的可能性就越小。因此,您的交易可能会被恢复,甚至从区块链中删除,但等待的时间越长,这种情况发生的可能性就越小。
注意
交易不能保证包含在下一个区块或任何特定的未来区块中,因为它不是由交易的提交者决定的,而是由矿工决定哪个区块包含交易。
如果你想安排未来合约的调用,你可以使用智能合约自动化工具或oracle服务。
2.3 以太坊虚拟机(Ethereum Virtual Machine , EVM )
2.3.1 概述
以太坊虚拟机(EVM)是以太坊智能合约的运行时环境。它不仅是沙盒的,而且实际上是完全隔离的,这意味着在EVM中运行的代码不能访问网络、文件系统或其他进程。智能合约甚至可以有限地访问其他智能合约。
2.3.2 Accounts
以太坊中有两种帐户共享相同的地址空间:由公私钥对(即人)控制的外部帐户(External accounts)和由与帐户存储在一起的代码控制的合约帐户(contract accounts)。
外部帐户的地址是由公钥确定的,而合同的地址是在合同创建时确定的(它来源于创建者地址和从该地址发送的交易数量,即所谓的“nonce”)。
不管帐户是否存储代码,EVM都平等地对待这两种类型。
每个帐户都有一个持久的键值存储,将256位字映射到256位字,称为存储(storage
)。
此外,每个账户都有一个以太币(Ether
)余额(balance
)(Wei
更精确,1 ether
是10**18 wei
),可以通过发送包含以太币的交易来修改。
2.3.3 Transactions
交易是从一个帐户发送到另一个帐户的消息(该帐户可能相同或为空,请参见下文)。它可以包括二进制数据(称为“有效负载(payload
)”)和Ether
。
如果目标帐户包含代码,则执行该代码,并将有效负载作为输入数据提供。
如果没有设置目标帐户(事务没有收件人或收件人被设置为空),则交易将创建一个新合约。如前所述,该合约的地址不是零地址,而是由发送方及其发送的交易数量(“nonce”)派生的地址。这种合约创建交易的有效负载被视为EVM字节码并执行。这个执行的输出数据被永久地存储为合约的代码。这意味着为了创建合约,您不发送合约的实际代码,而是在执行时返回该代码的代码。
在创建合约时,其代码仍然为空。因此,在构造函数完成执行之前,不应该回调正在构造的合约。
2.3.4 Gas
在创建时,每笔交易都被收取一定数量的gas
,这些gas
必须由交易的发起者(tx.origin
)支付。当EVM执行交易时,gas
会根据特定的规则逐渐耗尽。如果gas
在任何时候被用完(即它将是负的),则触发out-of-gas
异常,该异常结束执行并恢复当前调用帧中对状态所做的所有修改。
这种机制激励了EVM执行时间的经济使用,也补偿了EVM执行者(即矿工/利益相关者)的工作。由于每个块都有最大的gas量,因此也限制了验证块所需的工作量。
gas price
是由交易的发起者设定的值,他必须预先向EVM执行者支付gas_price * gas
。如果执行后剩余一些气体,则将其退还给交易发起者。在恢复更改的例外情况下,已经用完的gas不退还。
由于EVM执行者可以选择包含或不包含交易,因此交易发送者不能通过设置低gas
价格来滥用系统。
2.3.5 Storage, Memory and the Stack
以太坊虚拟机有三个区域可以存储数据:存储、内存和堆栈。
每个帐户都有一个称为存储(storage
)的数据区域,该数据区域在函数调用和交易之间是持久的。存储是一个将256位字映射到256位字的键值存储。从合约中枚举存储是不可能的,读取存储的成本相对较高,初始化和修改存储的成本更高。由于这个成本,您应该将存储在持久存储中的内容最小化,以满足契约运行的需要。在合约之外存储派生计算、缓存和聚合等数据。合约既不能读也不能写除它自己以外的任何存储。
第二个数据区域称为内存(memory
),其中合约为每个消息调用获取一个新清除的实例。内存是线性的,可以在字节级寻址,但读取的宽度限制为256位,而写入的宽度可以是8位或256位。当访问(读或写)一个以前未触及的内存字(即字内的任何偏移量)时,内存以一个字(256位)扩展。在扩张的时候,必须支付天然气的费用。内存增长得越大,成本就越高(它的比例是2倍的)。
EVM不是寄存器机,而是堆栈机,因此所有计算都在称为堆栈(stack
)的数据区域上执行。它的最大大小为1024个元素,包含256 bit 的字。对堆栈的访问以以下方式限制在顶部:可以将最上面的16个元素中的一个复制到堆栈的顶部,或者将最上面的元素与它下面的16个元素中的一个交换。所有其他操作都从堆栈中取出最上面的两个(或一个或多个,取决于操作)元素,并将结果推入堆栈。当然,为了更深入地访问堆栈,可以将堆栈元素移动到存储或内存中,但不可能在不首先删除堆栈顶部的情况下,只访问堆栈中更深层的任意元素。
2.3.6 指令集
EVM的指令集保持最小,以避免可能导致共识问题的不正确或不一致的实现。所有指令都在基本数据类型、256位字或内存片(或其他字节数组)上操作。通常的算术、位、逻辑和比较操作都有。条件跳转和无条件跳转是可能的。此外,合约可以访问当前块的相关属性,如其编号和时间戳。
有关完整列表,请参阅作为内联程序集文档一部分的操作码列表。
2.3.7 Message Calls
合约可以调用其他合约,也可以通过消息调用的方式将Ether发送到非合约账户。消息调用类似于交易,因为它们有一个源、一个目标、数据有效负载、Ether、gas和返回数据。事实上,每个交易都由一个顶级消息调用组成,而这个顶级消息调用又可以创建更多的消息调用。
合约可以决定剩余gas
的多少应该随内部消息调用发送,以及它想保留多少。如果在内部调用中发生了out-of-gas
异常(或任何其他异常),将通过堆栈上的错误值发出信号。在这种情况下,只有与调用一起发送的气体被用完。在Solidity中,在这种情况下,调用合约默认会导致一个手动异常,因此异常会“冒泡”调用堆栈。
如前所述,被调用的合约(可以与调用方相同)将接收一个新清除的内存实例,并有权访问调用有效负载——调用有效负载将在称为calldata
的单独区域中提供。在它完成执行后,它可以返回数据,这些数据将存储在调用方预先分配的调用方内存中的某个位置。所有这些调用都是完全同步的。
调用的深度限制为1024,这意味着对于更复杂的操作,应该优先使用循环而不是递归调用。此外,只有63/64的gas可以在消息调用中转发,这导致在实践中深度限制略小于1000。
2.3.8 Delegatecall and Libraries
存在一种特殊的消息调用变体,名为delegatecall
,除了目标地址的代码在调用合约的上下文中执行,和msg.sender
和msg.value
不改变其值之外,它与消息调用是相同的。
这意味着合约可以在运行时从不同的地址动态加载代码。存储、当前地址和余额仍然引用调用合约,只是代码取自被调用地址。
这使得在Solidity中实现“库(library
)”功能成为可能:可重用的库代码,可以应用到合约的存储中,例如,为了实现复杂的数据结构。
2.3.9 Logs
可以将数据存储在一个特殊的索引数据结构中,该结构一直映射到块级别。这个被称为logs
的特性被Solidity用来实现事件。在创建日志数据后,合约不能访问日志数据,但是可以从区块链外部有效地访问它们。由于部分日志数据存储在bloom过滤器中,因此可以以一种高效且加密安全的方式搜索这些数据,因此没有下载整个区块链的网络对等端(所谓的“轻客户端”)仍然可以找到这些日志。
2.3.10 Create
合约甚至可以使用特殊的操作码创建其他合约(例如,它们不像交易那样简单地调用零地址)。这些创建调用(create calls
)和普通消息调用之间的唯一区别是执行有效负载数据,结果存储为代码,调用者/创建者接收堆栈上新合约的地址。
2.3.11 Deactivate and Self-destruct
从区块链中删除代码的唯一方法是当该地址的合约执行selfdestruct
操作时。存储在该地址的剩余Ether被发送到指定的目标,然后从状态中删除存储和代码。从理论上讲,删除合约听起来是个好主意,但它有潜在的危险,就像如果有人发送以太币到删除的合约,以太币就永远丢失了。
即使一个合约被
selfdestruct
删除,它仍然是区块链历史的一部分,可能被大多数以太坊节点保留。所以使用自毁和从硬盘中删除数据是不一样的。
即使合约的代码不包含调用selfdestruct
,它仍然可以使用delegatecall
或callcode
执行该操作。
如果你想停用你的合约,你应该通过改变一些内部状态来禁用它们,这会导致所有函数恢复。这使得它不可能使用合约,因为它立即返回以太币。
2.2.12 预编译的合约
有一小部分合约地址是特殊的:1到(包括)8之间的地址范围包含“预编译的合同”,可以被称为任何其他合约,但它们的行为(以及它们的gas 消耗)不是由存储在该地址的EVM代码定义的(它们不包含代码),而是在EVM执行环境中实现的。
不同的 EVM兼容链可能使用不同的预编译合约集。未来也有可能将新的预编译合约添加到以太坊主链中,但你可以合理地期望它们总是在1
到0xffff
(包括)之间的范围内。
三、安装Solidity编译器
3.1 Versioning
坚固版本遵循Semantic Versioning。此外,带有主要发行版0(如0.x.y
)的补丁级别发行版将不包含破坏性更改。这意味着使用版本0.x.y
编译的代码。可以使用0.x.z
(其中z > y)编译。
除了发布版本,我们还提供 nightly development builds
,目的是让开发人员更容易尝试即将到来的功能并提供早期反馈。然而,请注意,虽然夜间构建通常非常稳定,但它们包含来自开发分支的前沿代码,并且不能保证总是工作。尽管我们尽了最大的努力,但它们可能包含未记录的和/或不完整的更改,这些更改不会成为实际发布的一部分。它们不是用于生产的。
在部署合约时,您应该使用最新发布的Solidity版本。这是因为突破性的变化,以及新功能和错误修复是定期引入的。我们现在用的是0.x
版本号来表示这种快速的更改速度。
3.2 Remix
对于小合约和快速学习Solidity,我们推荐Remix。
Access Remix online,如果您想在不连接互联网的情况下使用它,请访问https://github.com/ethereum/remix-live/tree/gh-pages并下载页面上解释的.zip
文件。Remix也是测试夜间构建的一个方便选择,无需安装多个Solidity版本。
本页上的其他选项详细说明了在计算机上安装命令行Solidity编译器软件。如果您正在处理一个较大的合约,或者需要更多的编译选项,请选择命令行编译器。
四、Solidity 合约示例
4.1 投票合约
下面的合约相当复杂,但展示了很多Solidity的特性。它实现了一个投票合约。当然,电子投票的主要问题是如何将投票权分配给正确的人,如何防止操纵。我们不会在这里解决所有问题,但至少我们将展示如何进行委托投票,以便同时实现自动计票和完全透明。
这个想法是为每张选票创建一个合同,为每个选项提供一个简短的名称。然后,作为主席的合同创建者将赋予每个地址单独的投票权。
这些地址背后的人可以选择自己投票或将选票委托给他们信任的人。
在投票时间结束时,winningProposal()
将返回票数最多的提案。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0 <0.9.0;/// @title Voting with delegation.contract Ballot {// This declares a new complex type which will// be used for variables later.// It will represent a single voter.struct Voter {uint weight; // weight is accumulated by delegationbool voted;// if true, that person already votedaddress delegate; // person delegated touint vote; // index of the voted proposal}// This is a type for a single proposal.struct Proposal {bytes32 name; // short name (up to 32 bytes)uint voteCount; // number of accumulated votes}address public chairperson;// This declares a state variable that// stores a `Voter` struct for each possible address.mapping(address => Voter) public voters;// A dynamically-sized array of `Proposal` structs.Proposal[] public proposals;/// Create a new ballot to choose one of `proposalNames`.constructor(bytes32[] memory proposalNames) {chairperson = msg.sender;voters[chairperson].weight = 1;// For each of the provided proposal names,// create a new proposal object and add it// to the end of the array.for (uint i = 0; i < proposalNames.length; i++) {// `Proposal({...})` creates a temporary// Proposal object and `proposals.push(...)`// appends it to the end of `proposals`.proposals.push(Proposal({name: proposalNames[i],voteCount: 0}));}}// Give `voter` the right to vote on this ballot.// May only be called by `chairperson`.function giveRightToVote(address voter) external {// If the first argument of `require` evaluates// to `false`, execution terminates and all// changes to the state and to Ether balances// are reverted.// This used to consume all gas in old EVM versions, but// not anymore.// It is often a good idea to use `require` to check if// functions are called correctly.// As a second argument, you can also provide an// explanation about what went wrong.require(msg.sender == chairperson,"Only chairperson can give right to vote.");require(!voters[voter].voted,"The voter already voted.");require(voters[voter].weight == 0);voters[voter].weight = 1;}/// Delegate your vote to the voter `to`.function delegate(address to) external {// assigns referenceVoter storage sender = voters[msg.sender];require(sender.weight != 0, "You have no right to vote");require(!sender.voted, "You already voted.");require(to != msg.sender, "Self-delegation is disallowed.");// Forward the delegation as long as// `to` also delegated.// In general, such loops are very dangerous,// because if they run too long, they might// need more gas than is available in a block.// In this case, the delegation will not be executed,// but in other situations, such loops might// cause a contract to get "stuck" completely.while (voters[to].delegate != address(0)) {to = voters[to].delegate;// We found a loop in the delegation, not allowed.require(to != msg.sender, "Found loop in delegation.");}Voter storage delegate_ = voters[to];// Voters cannot delegate to accounts that cannot vote.require(delegate_.weight >= 1);// Since `sender` is a reference, this// modifies `voters[msg.sender]`.sender.voted = true;sender.delegate = to;if (delegate_.voted) {// If the delegate already voted,// directly add to the number of votesproposals[delegate_.vote].voteCount += sender.weight;} else {// If the delegate did not vote yet,// add to her weight.delegate_.weight += sender.weight;}}/// Give your vote (including votes delegated to you)/// to proposal `proposals[proposal].name`.function vote(uint proposal) external {Voter storage sender = voters[msg.sender];require(sender.weight != 0, "Has no right to vote");require(!sender.voted, "Already voted.");sender.voted = true;sender.vote = proposal;// If `proposal` is out of the range of the array,// this will throw automatically and revert all// changes.proposals[proposal].voteCount += sender.weight;}/// @dev Computes the winning proposal taking all/// previous votes into account.function winningProposal() public viewreturns (uint winningProposal_){uint winningVoteCount = 0;for (uint p = 0; p < proposals.length; p++) {if (proposals[p].voteCount > winningVoteCount) {winningVoteCount = proposals[p].voteCount;winningProposal_ = p;}}}// Calls winningProposal() function to get the index// of the winner contained in the proposals array and then// returns the name of the winnerfunction winnerName() external viewreturns (bytes32 winnerName_){winnerName_ = proposals[winningProposal()].name;}}
可能的改进
目前,需要许多交易将投票权分配给所有参与者。此外,如果两个或两个以上的提案拥有相同的票数,winningProposal()
将不能登记为平局。你能想到解决这些问题的方法吗?