可靠性编程概述
Solidity 是一种智能合约编程语言。它是由 Gavin Wood、Christian Reitwiessner、Alex Beregszaszi 和几个以太坊核心贡献者开发的。它是一种类似 JavaScript 的通用语言,旨在针对以太坊虚拟机 ( EVM )。Solidity 是以太坊协议中处于同一抽象级别的四种语言之一,其他语言是 Serpent(类似于 Python)、 LLL ( 类似 Lisp 的语言)、Vyper(实验性的)和 Mutan(已弃用)。这个社区已经慢慢趋于稳定。通常,如果今天有人在以太坊谈论智能合约,他们隐含的意思是可靠。
在本章中,我们将讨论以下主题:
- 什么是扎实?
- solidity 开发环境的工具
- 智能合同简介
- 常见智能合同模式
- 智能合同安全性
- 案例研究——众筹活动
什么是扎实?
Solidity 是一种静态类型的契约语言,包含状态变量、函数和公共数据类型。开发人员能够编写在智能合约中实现业务逻辑功能的分散式应用程序(DApps)。该契约在编译时验证并实施约束,而不是在运行时。Solidity 编译成 EVM 可执行字节码。一旦被编译,合同就被上传到以太网。区块链将为智能合同分配一个地址。区块链网络上的任何许可用户都可以调用合约函数来执行智能合约。
下面是一个典型的流程图,展示了从编写契约代码到在以太网上部署和运行它的过程:
solidity 开发环境的工具
智能合约开发仍处于初级阶段。创建这样的契约并以方便的方式与它们交互可以通过多种方式来完成。以下强大的工具可用于构建、监控和部署您的智能合约,以便在以太坊平台上进行开发。
基于浏览器的 IDE
在本节中,我们将了解基于 onlien 浏览器的工具,如 Remix 和 EthFiddle。
再搅拌
Remix 是一个强大的、开源的、智能的契约工具,可以帮助你只通过浏览器就能编写出可靠的代码。它支持编译、运行、分析、测试和调试器选项。在开发和测试时,Remix 提供了以下三种环境:
-
JavaScript VM : Remix 自带五个以太坊账号,每个账号默认存 100 个以太。这便于在开发阶段测试智能合约。不需要挖掘,因为它是自动完成的。如果你是初学者,这个选项是个不错的选择。
-
注入的 Web3 :该选项将直接调用注入的浏览器 Web3 实例,如以太坊网络浏览器扩展 MetaMask。MetaMask 为您提供了许多功能和特性,并且像普通的以太坊钱包一样,它允许您与 DApps 进行交互。
-
Web3 提供者 : Remix 也支持 Web3 提供者。web3.js 库是官方的以太坊 JavaScript API。它用于与以太坊智能合约进行交互。你可以通过 web3j API 连接到区块链网络。Web3j 支持三种不同的提供者:HTTPProvider、WebsocketProvider 和 IpcProvider。在 Remix Web3 中,您可以给出 HTTP URL 来连接远程区块链实例。该 URL 可以指向您的本地私有区块链、test-net 和其他实例端点。
首先使用混音固化 IDE:https://remix.ethereum.org。以下是 Remix 的用户界面截图:
以太网
EthFiddle 是一个非常简单的基于浏览器的 solidity 开发工具。您可以快速测试和调试智能合约代码,并共享指向您的代码的永久链接。使 EthFiddle 脱颖而出的一个特性是它执行安全审计的潜力。以下截图显示了软件界面:
EthFiddle 软件界面
下面是 EthFiddle solidity IDE 链接:https://ethfiddle.com。
命令行开发管理工具
命令行工具是服务器端以太坊工具,用于创建 DApp 项目的基本结构。
松露
Truffle 是一个流行的开发环境和测试框架,是以太坊的资产管道。松露的主要特点包括以下几点:
- 内置智能合约编译、链接、部署和二进制管理
- 摩卡和柴的松露网站链接:http
- 可脚本化的部署和迁移框架
- 部署到许多公共和私有网络的网络管理
- 用于直接合同通信的交互式控制台
- 我们将在下一章更详细地讨论,并且我们将使用 Truffle 来为 ERC20 令牌开发 DApp
- 这是松露的网站链接:https://truffleframework.com/
智能合同简介
让我们从最基本的智能合约示例HelloWorld.sol
开始,如下所示:
pragma solidity ^0.4.24;
contract HelloWorld {
string public greeting;
constructor() public {
greeting = 'Hello World';
}
function setNewGreeting (string _newGreeting) public {
greeting = _newGreeting;
}
}
Solidity 的文件扩展名是.sol
。它类似于 JavaScript 文件的.js
和 HTML 模板的.html
。
实体源文件的布局
solidity 源文件通常由以下结构组成:杂注、注释和导入。
杂注
包含关键字 pragma 的第一行简单地说明了源代码文件不能用低于 0.4.24 版本的编译器编译。任何更新的东西都不会破坏功能。^
符号暗示了另一种情况——源文件也不能在 0.5.0 版以上的编译器上运行。
评论
注释是用来让源代码更容易让人理解程序的功能。多行注释用于代码的大文本描述。编译器会忽略注释。多行评论以/*
开头,以*/
结尾。
在HelloWorld
示例中,有对set
和get
方法的注释:
- 方法:
setNewGreeting (string _newGreeting) {}
函数 @param
:这是用来表示什么参数被传递给一个方法,以及它们应该有什么值
导入
solidity 中的 import 关键字与 JavaScript 过去的版本 ES6 非常相似。它用于将库和其他相关特征导入到 solidity 源文件中。Solidity 不支持导出语句。
以下是一些重要的例子:
import * as symbolName from “solidityFile”
前面显示的行将创建一个名为symbolName
的全局符号,包含来自导入文件的全局符号成员:solidityFile
。
与前面的导入等效的另一个特定于实体的语法如下:
import solidityFile as symbolName;
您还可以导入多个符号,并将其中一些符号命名为 alias,如下所示:
import {symbol1 as alias, symbol2} from " solidityFile";
下面是一个使用 zeppelin solidity 库创建 ERC20 令牌的示例:
pragma solidity ^0.4.15;
import 'zeppelin/contracts/math/SafeMath.sol';
….
contract ExampleCoin is ERC20 {
//SafeMath symbol is from imported file SafeMath.sol'
using SafeMath for uint256;
…
}
对于前面代码片段中显示的例子,我们从 Zeppelin 导入了SafeMath
库,并将其应用于uint256
。
小路
导入实体文件时,文件路径遵循一些简单的语法规则:
- 绝对路径:
/folder1/ folder2/xxx.sol
从/
开始,路径位置是从同一个实体文件位置到导入的文件。在我们的 ERC 20 的例子中,显示如下:
import 'zeppelin/contracts/math/SafeMath.sol';
实际的项目结构如下所示:
相对路径
../folder1/folder2/xxx.sol
:这些路径被解释为相对于当前文件的位置,.
为当前目录,..
为父目录。
在可靠性路径中,可以指定路径前缀重新映射。举个例子,如果要导入github.com/ethereum/dapp-bin/library
,可以先将 GitHub 库克隆到/usr/local/dapp-bin/library
,然后运行编译器命令,如下图:
solc github.com/ethereum/dapp-bin/library=/usr/local/dapp-bin/library
然后,在我们的 solidity 文件中,可以使用下面的import
语句。它将重新映射到/usr/local/dapp-bin/library/stringUtils.sol
:
import "github.com/ethereum/dapp-bin/library/stringUtils.sol " as stringUtils;
编译器将从那里读取文件。
合同的结构
契约包括以下构造:状态变量、数据类型、函数、事件、修饰符、枚举、结构和映射。
状态变量
状态变量是永久存储在协定存储中的值,用于维护协定的状态。
以下是该代码的一个示例:
contract SimpleStorage {
uint storedData; // State variable
//…
}
数据类型
Solidity 是一种静态类型的语言。熟悉 JavaScript 和 Python 等语言的开发人员会发现 Solidity 语法很容易掌握。每个变量都需要指定数据类型。变量总是通过值传递,称为值类型,它是内置的或预定义的数据类型。
solidity 中的值类型如下:
| 类型 | 操作员 | 例子 | 注 |
| Bool
| !, &&, ||, ==, != | bool a = true;
| 布尔值是真或假的表达式。 |
| Int
(int8 至 int256) | 比较运算符:<=, =,>,位运算符:&,|,^,+,-,一元-,一元+,,/,%,,<> | int a = 1;
| 有符号整数,有符号的 8 到 256 位,步长为 8。 |
| Uint
(uint8 至 uint256) | 比较运算符:<=, =,>位运算符:&,|,^,+,-,一元-,一元+,,/,%,**,<> | uint maxAge = 100;
| 无符号整数,无符号 8 到 256 位,步长为 8。 |
| 地址 | <=, =,> |
address owner = msg.sender;
address myAddress = 0xE0f5206B…437b9;
| 保存 20 字节的值(以太坊地址的大小)。 |
| <address>.balance
| | address.balance()
| Addresses members 和<address>.transfer
| | beneficiary.transfer(highestBid)
| 寻址成员并将以太(以卫为单位)发送到一个地址。如果转移操作失败,它<indexentry content="value types, solidity:.transfer">
抛出一个异常,事务中的所有更改都被恢复。 |
| <address>.send
| | msg.sender.send(amount)
| 寻址成员并将以太(单位为威)发送到一个地址。如果发送操作失败,它将返回 false。 |
| <address>.call
| |
someAddress.call.
value(1ether)
.gas(100000) ("register", "MyName")
| 执行另一个契约的代码,在失败的情况下返回 false,转发所有可用的气体,可调,应该在需要控制转发多少气体时使用。 |
| <address>.delegatecall
| | , _library.delegatecall(msg.data);
| 已执行另一个协定的代码,但带有调用协定的状态(存储)。 |
| 固定大小字节数组(位元组 1、位元组 2、…、位元组 32) | 比较运算符:<=, =,>,位运算符:&,|,^,~,<>,获取数组数据:数组[索引] | uint8[5] memory traits = [1,2,3,4,5];
| 固定大小的字节数组是使用关键字 byteN 定义的,N 是 1 到 32 之间的任何数字,它限制了大小,它会便宜得多,并会节省您的汽油。 |
| 动态大小的数组字节字符串 | |
/**bytes array **/
bytes32[] dynamicArray
function f() {
bytes32[] storage storageArr = dynamicArray
storageArr.length++;
}
/**string array **/
bytes32[] public names
| Solidity 支持动态大小的字节数组和动态大小的 UTF 8 编码字符串。 |
| 十六进制文字 | | hex"1AF34A"
| 十六进制文字以关键字 hex 为前缀,用单引号或双引号括起来。 |
| 地址文字 | | 0x5eD8Cee6b63b1c6AFce``3AD7c92f4fD7E1B8fAd9F
| 通过地址校验和测试的是十六进制文字。 |
| 字符串文字 | | "Hello”
| 字符串通常用单引号或双引号括起来。 |
枚举类型
枚举是一种具有一组有限常数值的类型。下面是一个例子,如下:
pragma solidity ^0.4.24;
contract ColorEnum {
enum Color {RED,ORANGE,YELLOW, GREEN}
Color color;
function construct() public {
color = Color.RED;
}
function setColor(uint _value) public {
color = Color(_value);
}
function getColor() public view returns (uint){
return uint(color);
}
}
结构类型
结构是包含命名字段的类型。新类型可以使用 struct 声明。下面是以下代码中的一个示例:
struct person {
uint age;
string fName;
string lName;
string email;
}
绘图
映射充当由键类型和相应的值类型对组成的哈希表。下面是一个例子,如下:
pragma solidity ^0.4.24;
contract StudentScore {
struct Student {
uint score;
string name;
}
mapping (address => Student) studtents;
address[] public studentAccts;
function setStudent(address _address, uint _score, string _name) public {
Student storage studtent = studtents[_address];
studtent.score = _score;
studtent.name = _name;
studentAccts.push(_address) -1;
}
function getStudents() view public returns(address[]) {
return studentAccts;
}
function getStudent(address _address) view public returns (uint, string) {
return (studtents[_address].score, studtents[_address].name);
}
function countStudents() view public returns (uint) {
return studentAccts.length;
}
}
功能
函数是契约中可执行的代码单元。以下是 solidity 中的函数结构,如下所示:
function (<Input parameters>) {access modifiers} [pure|constant|view|payable] [returns (<return types>)]
输入参数
函数可以传递输入参数。输入参数的声明方式与变量相同。
在前面的HelloWorld
示例中,我们使用输入参数string _newGreeting
来定义setNewGreeting
。以下是此步骤的一个示例:
function setNewGreeting (string _newGreeting) {
greeting = _newGreeting;
}
访问修饰符
坚固性访问修饰符用于在坚固性中提供访问控制。
Solidity 中有四种类型的访问修饰符,如下所示:
- Public :可从此契约、继承契约和外部访问
- Private :只能从本合同中访问
- 内部:只能从该合同及其继承的合同中访问
- 外部:内部不能访问,只能外部访问
输出参数
输出参数可以在return
关键字之后声明,如下面的代码片段所示:
function getColor() public view returns (uint){
return uint(color);
}
在 solidity 中,pure
函数是承诺不修改或读取状态的函数。
pure|constant|view|payable
如果函数修饰符定义为 view,则表示该函数不会改变存储状态。
如果函数修饰符定义为 pure,则表示函数不会读取存储状态。
如果函数修饰符定义为常量,则表示该函数不会修改合同存储。
如果功能修改量被定义为应付款,修改量可以接收资金。
uint amount =0;
function buy() public payable{
amount += msg.value;
}
在前面的例子中,buy
函数有一个 payable 修饰符,它确保您可以向buy
函数发送 ethers。一个没有名字的函数,用一个 payable 关键字注释,叫做 payable fallback 函数。
pragma solidity ^0.4.24;
// this is a contract, which keeps all Ether to it with not way of
// retrieving it.
contract MyContract {
function() public payable { }
}
修饰语
在 solidity 中,修改器用于改变函数的行为。它们可以在执行函数之前自动检查条件。下面是一个例子,如下:
pragma solidity ^0.4.24;
contract Modifiers {
address public admin;
function construct () public {
admin = msg.sender;
}
//define the modifiers
modifier onlyAdmin() {
// if a condition is not met then throw an exception
if (msg.sender != admin) revert();
// or else just continue executing the function
_;
}
// apply modifiers
function kill() onlyAdmin public {
selfdestruct(admin);
}
}
事件
事件用于跟踪发送给合同的事务的执行。与 EVM 测井设备有方便的接口。下面是一个例子,如下:
pragma solidity ^0.4.24;
contract Purchase {
event buyEvent(address bidder, uint amount); // Event
function buy() public payable {
emit buyEvent(msg.sender, msg.value); // Triggering event
}
}
构造器
构造函数方法是创建和初始化协定的一种特殊方法。在 solidity v0.4.23 中,solidity 引入了这种新的构造函数符号,旧的符号已被弃用。
//new
pragma solidity ^0.4.24;
contract HelloWorld {
function constructor() public {
// ...
}
}
//deprecated
pragma solidity ^0.4.22;
contract HelloWorld {
function HelloWorld () public {
// ...
}
}
恒定状态变量、单位和函数
常量的值不能通过重新赋值来改变,也不能在编译后重新声明。在 solidity 中,状态变量可以声明为常量。它不允许重新分配给区块链数据(例如,此.balance
、block.blockhash
)或执行数据(tx.gasprice
),或调用外部合同。
下表列出了 solidity 全局变量及其内置函数:
| 全局变量/函数 | 描述 |
| msg.sender(address)
| msg.sender
是当前与合同调用消息交互的地址。 |
| msg.data(bytes)
| msg.data
是当前与合同完成呼叫交互的地址。数据以字节为单位。 |
| msg.value(unit)
| msg.value
是当前根据合同与消息发送的 Wei 号交互的地址。 |
| msg.sig
| msg.sig
是当前与返回调用数据前四个字节的契约交互的地址。 |
| gasleft() returns (uint256)
| API 检查剩余气体。 |
| tx.origin
| API 来检查事务的发送方。 |
| tx.gasprice
| API 来检查交易的天然气价格。 |
| now
| 获取当前 unix 时间戳。 |
| block.number
| API 来检索当前的块号。 |
| block.difficulty
| API 来检索当前块的难度。 |
| block.blockhash(uint blockNumber) returns (bytes32)
| API 来获取给定块的哈希;结果只返回 256 个最新的块。 |
| block.gasLimit(unit)
| API 来获取当前块气体限制。 |
| block.coinbase ()
| 返回当前块矿工的地址。 |
| keccak256(...);
| Returns (bytes32)计算(紧密打包)参数的以太坊-SHA-3 (Keccak-256)哈希。 |
| sha3(...)
| returns(bytes 32):keccak 256 的别名。 |
| assert(bool condition)
| assert
可用于检查条件。它表示在任何情况下都不应该是假的。此外,assert
使用0xfe
操作码来导致错误情况。 |
| require(bool condition)
| 应该使用require
函数来确保有效条件。当用户输入不适当的内容时,它会返回 false。此外,require()
使用0xfd
操作码来导致错误情况。 |
| revert()
| revert
仍将撤销所有状态更改。 |
| <address>.balance
| 它检查地址在 Wei (uint256)中的余额。 |
| <address>.send(uint256 amount) returns (bool)
| API 将 Wei 的数量发送到 address,如果失败,则返回 false。 |
| <address>.transfer(uint256 amount)
| API 将 Wei 的数量传递到这个地址,当传递失败时抛出错误。 |
| this
| 当前合同,明确可转换为地址。 |
| super
| 继承层次结构中更高一级的协定。 |
| selfdestruct(address recipient)
| self-destruct
会破坏当前的契约,并从以太坊的世界状态中移除与之相关的存储。 |
| suicide(address recipient)
| 自毁的别名。 |
乙醚单位
固体以太可分为卫,桂,Mwei,Gwei,Szabo,芬尼,凯瑟,Mether,Gether 和系绳。以下是转换单位:
- 1 乙醚= 1000 芬尼
- 1 Finney = 1,000 Szabo
- 1 Szabo = 1000 mwe
- 1 Mwei = 1,000 Kwei
- 1 奎= 1000 魏
时间单位
固体时间单位可分为秒、分、小时、天、周和年。以下是转换单位:
- 1 = 1 秒
- 1 分钟= 60 秒
- 1 小时= 60 分钟
- 1 天= 24 小时
- 1 周= 7 天
- 1 年= 365 天
继承、抽象和接口
许多最广泛使用的编程语言(如 C++、Java、Go 和 Python 等)都支持面向对象编程 ( OOP )并支持继承、封装、抽象和多态。继承支持代码重用和可扩展性。Solidity 支持复制代码形式的多重继承,其中包括多态性。即使一个合同从多个其他合同继承而来,也只能在区块链上创建一个合同。
在可靠性方面,继承非常类似于经典的面向对象编程语言。以下是一些例子:
pragma solidity ^0.4.24;
contract Animal {
constructor() public {
}
function name() public returns (string) {
return "Animal";
}
function color() public returns (string);
}
contract Mammal is Animal {
int size;
constructor() public {
}
function name() public returns (string) {
return "Mammal";
}
function run() public pure returns (int) {
return 10;
}
function color() public returns (string);
}
contract Dog is Mammal {
function name() public returns (string) {
return "Dog";
}
function color() public returns (string) {
return "black";
}
}
Dog 继承自Mammal
,其母契约为Animal
。调用Dog.run()
时,会调用其父方法run()
,返回十。当调用名字时,Dog.name()
将覆盖其专利方法,并返回Dog
的输出。
在 solidity 中,没有主体(没有实现)的方法被称为抽象方法。包含抽象方法的协定不能实例化,但可以用作基。
如果一个协定继承自一个抽象协定,那么该协定必须实现抽象父类的所有抽象方法,或者也必须声明为抽象的。
Dog 有一个具体的color()
方法,是一个具体的契约,可以编译,但是父契约——哺乳动物,祖父母契约——动物,还是抽象契约。
solidity 中的接口类似于抽象契约;它们是隐式抽象的,不能有实现。抽象协定可以有实现默认行为的实例方法。接口中有更多的限制,如下所示:
- 无法继承其他协定或接口
- 无法定义构造函数
- 无法定义变量
- 无法定义 struts
- 无法定义枚举
pragma solidity ^0.4.24;
//interface
contract A {
function doSomething() public returns (string);
}
//contract implements interface A
contract B is A {
function doSomething() public returns (string) {
return "Hello";
}
}
在前面的例子中,契约是一个接口,contract B
实现了interface A
,并且有一个具体的doSomething()
方法。
常见智能合同模式
在本节中,我们将讨论智能合约编程语言的一些常见设计和编程模式。
存取限制
访问限制是一种可靠的安全模式。它只允许授权方访问某些功能。由于区块链的公共性质,任何人都可以看到区块链上的所有数据。声明您的协定函数、使用受限访问控制进行声明,并针对对智能协定功能的未授权访问提供安全性,这一点非常重要。
pragma solidity ^0.4.24;
contract Ownable {
address owner;
uint public initTime = now;
constructor() public {
owner = msg.sender;
}
//check if the caller is the owner of the contract
modifier onlyOwner {
require(msg.sender == owner,"Only Owner Allowed." );
_;
}
//change the owner of the contract
//@param _newOwner the address of the new owner of the contract.
function changeOwner(address _newOwner) public onlyOwner {
owner = _newOwner;
}
function getOwner() internal constant returns (address) {
return owner;
}
modifier onlyAfter(uint _time) {
require(now >= _time,"Function called too early.");
_;
}
modifier costs(uint _amount) {
require(msg.value >= _amount,"Not enough Ether provided." );
_;
if (msg.value > _amount)
msg.sender.transfer(msg.value - _amount);
}
}
contract SampleContarct is Ownable {
mapping(bytes32 => uint) myStorage;
constructor() public {
}
function getValue(bytes32 record) constant public returns (uint) {
return myStorage[record];
}
function setValue(bytes32 record, uint value) public onlyOwner {
myStorage[record] = value;
}
function forceOwnerChange(address _newOwner) public payable
onlyOwner onlyAfter(initTime + 2 weeks) costs(50 ether) {
owner =_newOwner;
initTime = now;
}
}
前面的示例显示了应用于协定的访问限制模式。我们首先用onlyOwner
、changeOwner
和onlyAfter
函数修饰符定义一个名为Ownable
的父类。其他协定可以从此协定继承以使用定义的访问限制。SampleContract
继承自Ownable
合同,因此只有所有者才能访问setValue
功能。此外,forceOwnerChange
只能在合同创建时间后两周以 50 以太网成本调用,并且只有所有者有权执行该功能。
状态机
状态机是一种行为设计模式。它允许契约在其内部状态改变时改变其行为。智能协定函数调用通常会将协定状态从一个阶段移动到下一个阶段。状态机的基本操作有两个部分:
- 它遍历一系列状态,其中下一个状态由当前状态和输入条件决定。
- 它根据状态转换提供输出序列。
为了说明这一点,让我们开发一个简单的状态机。我们将以洗碗为例。这个过程通常是擦洗、漂洗、干燥、擦洗、漂洗、干燥。我们将状态机阶段定义为枚举类型。因为这是一个广泛的用例,所以只给出与状态机相关的代码。省略了任何详细动作执行的逻辑,如rinse()
、dry()
等。请参见以下示例:
pragma solidity ^0.4.24;
contract StateMachine {
enum Stages {
INIT,
SCRUB,
RINSE,
DRY,
CLEANUP
}
Stages public stage = Stages.INIT;
modifier atStage(Stages _stage) {
require(stage == _stage);
_;
}
function nextStage() internal {
stage = Stages(uint(stage) + 1);
}
modifier transitionNext() {
_;
nextStage();
}
function scrub() public atStage(Stages.INIT) transitionNext {
// Implement scrub logic here
}
function rinse() public atStage(Stages.SCRUB) transitionNext {
// Implement rinse logic here
}
function dry() public atStage(Stages.SCRUB) transitionNext {
// Implement dry logic here
}
function cleanup() public view atStage(Stages.CLEANUP) {
// Implement dishes cleanup
}
}
我们定义函数修饰符atStage
来检查当前状态是否允许阶段运行函数。此外,transitionNext
修改器将调用内部方法nextStage()
将状态转移到下一阶段。
智能合同安全性
一旦智能合约被部署到以太坊网络上,它就是不可改变的,并且对每个人都是公开的。许多智能合约功能与账户支付相关;因此,在部署到主网络之前,安全性和测试对于合同来说是绝对必要的。以下是安全实践,将帮助您更好地设计和编写完美的以太坊智能合约。
保持合同简单和模块化
尽量让你的智能合同小,简单,模块化。复杂的代码难以阅读、理解和调试,而且容易出错。
尽可能使用编写良好的库工具。
限制局部变量的数量。
将不相关的功能移至其他合同或库。
使用检查-效果-交互模式
在与其他外部合同交互时要非常小心,这应该是您功能的最后一步。它会带来一些意想不到的风险或错误。外部调用可能会执行恶意代码。这些类型的呼叫应该被视为潜在的安全风险,如果可能的话应该避免。
pragma solidity ^0.4.24;
// THIS CONTRACT is INSECURE - DO NOT USE
contract Fund {
mapping(address => uint) userBalances;
function withdrawBalance() public {
//external call
if (msg.sender.call.value(userBalances[msg.sender])())
userBalances[msg.sender] = 0;
}
}
contract Hacker {
Fund f;
uint public count;
event LogWithdrawFallback(uint c, uint balance);
function Attacker(address vulnerable) public {
f = Fund(vulnerable);
}
function attack() public {
f.withdrawBalance();
}
function () public payable {
count++;
emit LogWithdrawFallback(count, address(f).balance);
if (count < 10) {
f.withdrawBalance();
}
}
}
}
线路msg.sender.call.value(userBalances[msg.sender])
是外部呼叫,当withdrawBalance
被呼叫时,它会用address.call.value()
发送以太。黑客可以通过触发 hack fallback 函数攻击基金合同,该函数可以再次调用withdrawBalance
方法。这将允许攻击者多次退款,耗尽帐户中的所有乙醚。
前面的契约漏洞称为可重入性。为了避免这种情况,您可以使用 checks-effects-interactions 模式,如下例所示:
pragma solidity ^0.4.24;
contract Fund {
mapping(address => uint) userBalances;
funct
ion withdrawBalance() public {
uint amt = userBalances[msg.sender];
userBalances[msg.sender] =0;
msg.sender.transfer(amt);
}
}
我们首先需要确定函数的哪一部分涉及到外部调用,uint amt = userBalances[msg.sender]; userBalances[msg.sender] =0;
。
该函数读取userBalances
值,并将其赋给一个局部变量,然后重置userBalances
。这些步骤是为了确保消息发送者只能转移到自己的帐户,但不能对状态变量进行任何更改。在乙醚实际转移到用户之前,用户的余额将减少。如果转账过程中出现任何错误,整个交易将被还原,包括状态变量中余额的减少转账金额。这种方法可以被描述为乐观会计,因为效果在实际发生之前就被记录为完成。
带阻塞气体限制的 DoS
以太坊区块链交易由于区块气体限制只能处理一定量的气体,所以要小心寻找没有固定的有限积分。当许多迭代成本超过 gas 限制时,事务将失败,合同可能会在某个点上停止。在这种情况下,攻击者可能会攻击合同,并操纵天然气。
处理外部呼叫中的错误
正如我们之前讨论的,solidity 有一些底层调用方法:address.call()
、address.callcode()
、address.delegatecall()
和address.send()
。这些方法仅在调用遇到异常时返回 false。因此,处理外部调用中的错误在契约中非常重要,如下面的代码片段所示:
// good
if(!contractAddress.send(100)) {
// handle error
}
contractAddress.send(20);//don't do this
contractAddress.call.value(55)(); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for result
contractAddress.call.value(50)(bytes4(sha3("withdraw()"))); // if withdraw throws an exception, the raw call() will only return false and transaction will NOT be reverted
案例研究——众筹活动
在本节中,我们将为众筹活动用例实现和部署智能契约。
众筹的概念是从大众中为一个项目或企业筹集资金的过程。投资者会收到代表他们所投资的创业公司股份的代币。这个项目设定了一个预先定义的目标和达到目标的最后期限。一旦项目没有达到目标,投资就会得到回报,这降低了投资者的风险。这种去中心化的筹款模式可以替代创业的资金需求,不需要集中的可信平台。只有在基金回报的情况下,投资者才会支付天然气费用。任何项目贡献者都会得到一个令牌,他们可以交易、出售或保留这些令牌。在一定阶段,代币可以用来换取实物作为实物奖励。
定义结构和事件,如下所示:
pragma solidity ^0.4.24;
contract CrowdFunding {
Project public project;
Contribution[] public contributions;
//Campaign Status
enum Status {
Fundraising,
Fail,
Successful
}
event LogProjectInitialized (
address owner,
string name,
string website,
uint minimumToRaise,
uint duration
);
event ProjectSubmitted(address addr, string name, string url, bool initialized);
event LogFundingReceived(address addr, uint amount, uint currentTotal);
event LogProjectPaid(address projectAddr, uint amount, Status status);
event Refund(address _to, uint amount);
event LogErr (address addr, uint amount);
//campaign contributors
struct Contribution {
address addr;
uint amount;
}
//define project
struct Project {
address addr;
string name;
string website;
uint totalRaised;
uint minimumToRaise;
uint currentBalance;
uint deadline;
uint completeAt;
Status status;
}
//initialized project
constructor (address _owner, uint _minimumToRaise, uint _durationProjects,
string _name, string _website) public payable {
uint minimumToRaise = _minimumToRaise * 1 ether; //convert to wei
uint deadlineProjects = now + _durationProjects* 1 seconds;
project = Project(_owner, _name, _website, 0, minimumToRaise, 0, deadlineProjects, 0, Status.Fundraising);
emit LogProjectInitialized(
_owner,
_name,
_website,
_minimumToRaise,
_durationProjects);
}
定义修饰符,如下面的代码所示:
//check if project is at the required stage
modifier atStage(Status _status) {
require(project.status == _status,"Only matched status allowed." );
_;
}
//check if msg.sender is project owner
modifier onlyOwner() {
require(project.addr == msg.sender,"Only Owner Allowed." );
_;
}
//check if project pass the deadline
modifier afterDeadline() {
require(now >= project.deadline);
_;
}
//Wait for 6 hour after campaign completed before allowing contract destruction
modifier atEndOfCampain() {
require(!((project.status == Status.Fail || project.status == Status.Successful) && project.completeAt + 6 hours < now));
_;
}
定义智能合同功能,如下图所示:
function () public payable {
revert();
}
/* The default fallback function is called whenever anyone sends funds to a contract */
function fund() public atStage(Status.Fundraising) payable {
contributions.push(
Contribution({
addr: msg.sender,
amount: msg.value
})
);
project.totalRaised += msg.value;
project.currentBalance = project.totalRaised;
emit LogFundingReceived(msg.sender, msg.value, project.totalRaised);
}
//checks if the goal or time limit has been reached and ends the campaign
function checkGoalReached() public onlyOwner afterDeadline {
require(project.status != Status.Successful && project.status!=Status.Fail);
if (project.totalRaised > project.minimumToRaise){
project.addr.transfer(project.totalRaised);
project.status = Status.Successful;
emit LogProjectPaid(project.addr, project.totalRaised, project.status);
} else {
project.status = Status.Fail;
for (uint i = 0; i < contributions.length; ++i) {
uint amountToRefund = contributions[i].amount;
contributions[i].amount = 0;
if(!contributions[i].addr.send(contributions[i].amount)) {
contributions[i].amount = amountToRefund;
emit LogErr(contributions[i].addr, contributions[i].amount);
revert();
} else{
project.totalRaised -= amountToRefund;
project.currentBalance = project.totalRaised;
emit Refund(contributions[i].addr, contributions[i].amount);
}
}
}
project.completeAt = now;
}
function destroy() public onlyOwner atEndOfCampain {
selfdestruct(msg.sender);
}
}
让我们用 Remix 来测试我们的活动。我们选择 JavaScript VM 选项。
- 通过单击 Deploy 按钮初始化活动,输入以下内容。这将通过调用构造函数启动我们的活动。我们将第一个客户指定为项目所有者。最低募集资金为 30 以太,截止时间为 5 分钟,用于测试目的。将以下输入代码放入 Deploy 按钮旁边的文本框中。以下是构造函数的输入参数:
0xca35b7d915458ef540ade6068dfe2f44e8fa733c, 30, 100, "smartchart",
"smartchart.tech"
以下是此步骤的混音编辑器屏幕截图:
混音编辑器屏幕
- 切换到第二个帐户,在重新混合值输入字段中,输入
20
乙醚,然后点击(后退)按钮。这将把20
乙醚添加到 totalRaised 中。若要检查项目信息,请单击“项目”按钮,您应该会看到现在的总加薪为 20 乙醚。在捐款输入框中输入0
uint,我们可以看到第二个账户的捐款地址,以及 20 乙醚的资金金额:
重新混合值输入字段
- 切换到第三个帐户,在值字段中输入
15
ethers,为项目添加资金。点击(后退),我们可以看到项目总资金提高到 35 乙醚。这时,该项目已经实现了竞选目标:
向项目添加资金
- 切换回项目所有者,这是第一个帐户,并单击 checkGoalReached。我们可以看到交易已经成功执行。在日志中,项目状态更新为“成功”。
LogProjectPaid
被触发。如果我们检查 Remix 帐户 1、2、3,项目所有者帐户现在总共包含大约 135 个醚。我们的活动智能合同在 Remix 中测试成功:
在智能合同中成功测试活动
摘要
在这一章中,我们学习了可靠性编程的基本特征。我们还概述了当前流行的智能合约开发工具。通过探索常见的模式和安全最佳实践,我们学会了如何编写更好的代码来避免契约漏洞。最后,我们编写了一个众筹活动合同,并使用 Remix 来部署和测试我们的示例。
在下一章,我们将为众筹建立一个分散的应用程序(DApp)。