智能合约教程

https://segmentfault.com/a/1190000015295148

流程

合约代码编写(Solidity)-> 合约编译(solc)-> 合约部署(web3)

开发语言及工具:

  • 区块链节点:ganache-cli
  • 基础环境:node
  • 合约开发语言:Solidity
  • 合约编译器:solc
  • 合约访问库:web3.js

基础环境安装

  • 1、安装 node.js
  • 2、安装 ganache-cli

ganache-cli

sudo npm install -g ganache-cli

ganache 默认会自动创建 10 个账户,每个账户有 100 个以太币(ETH:Ether)。 可以把账户视为银行账户,以太币就是以太坊生态系统中的货币。

面输出的最后一句话,描述了节点仿真器的监听地址和端口为localhost:8545,在使用 web3.js 时,需要传入这个地址来告诉web3js库应当连接到哪一个节点。

合约设计

合约中的属性用来声明合约的状态,而合约中的方法则提供修改状态的访问接口。

重点:

  • 合约状态是持久化到区块链上的,因此对合约状态的修改需要消耗以太币。
  • 只有在合约部署到区块链的时候,才会调用构造函数,并且只调用一次。
  • 与 web 世界里每次部署代码都会覆盖旧代码不同,在区块链上部署的合约是不可改变的,也就是说,如果你更新 合约并再次部署,旧的合约仍然会在区块链上存在,并且合约的状态数据也依然存在。新的部署将会创建合约的一 个新的实例。

状态变量和整数

Solidity

显著特点:

  1. 后缀.sol
  2. 强类型语言
  3. 语法和javascript类似

函数类型

view: 读取区块链上的数据,但是不修改区块链上面的数据

pure:不修改也不读取区块链上面的数据

一笔事务的控制台信息

地址类型

合约代码编写(Solidity)-> 合约编译(solc)-> 合约部署(web3)

开发语言及工具:

  • 区块链节点:ganache-cli
  • 基础环境:node
  • 合约开发语言:Solidity
  • 合约编译器:solc
  • 合约访问库:web3.js

版本指令

标明 Solidity 编译器的版本. 以避免将来新的编译器可能破坏你的代码。

pragma solidity ^0.4.19;

^0.4.20 0.4.x

~0.4.20 0.x.x

EVM数据存储

  • storage:状态变量(全局变量)会存储在这里。永久性存储数据(因为会把数据写入区块链当中),

  • stack:函数中的本地变量(局部变量)默认会存储在这里,暂时性存储数据。

  • memory:暂时性存储数据,实参(函数中实际传递的参数)默认会存储在这里。

tips:storage和memory都需要消耗gas,但是storage更贵。

结构体作为函数参数,函数必须是internal类型

数组

// 固定长度为2的静态数组:

uint[2] fixedArray;

// 固定长度为5string类型的静态数组:

string[5] stringArray;

// 动态数组,长度不固定,可以动态添加元素:

uint[] dynamicArray;

公共数组

你可以定义 public 数组, Solidity 会自动创建 getter 方法. 语法如下:

Person[] public people;

其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。

定义函数

function eatHamburgers(string _name, uint _amount) {

}

注:: 习惯上函数里的变量都是以(_)开头 (但不是硬性规定) 以区别全局变量。我们整个教程都会沿用这个习惯。

私有 / 公共函数

Solidity 定义的函数的属性默认为公共。 这就意味着任何一方 (或其它合约) 都可以调用你合约里的函数。

显然,不是什么时候都需要这样,而且这样的合约易于受到攻击。

uint[] numbers;

function _addToArray(uint _number) private {

numbers.push(_number);

}

返回值

string greeting = "What's up dog";

function sayHello() public returns (string) {

return greeting;

}

函数的修饰符****view,returns

把函数定义为 view, 意味着它只能读取数据不能更改数据:

function sayHello() public view returns (string) {}

Solidity 还支持 pure 函数, 表明这个函数甚至都不访问应用里的数据,例如:

function _multiply(uint a, uint b) private pure returns (uint) {

return a * b;

}

Keccak256 和 类型转换

Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。

注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。

事件

事件 是合约和区块链通讯的一种机制。你的前端应用监听某些事件,并做出反应。

// 这里建立事件

event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {

uint result = _x + _y;

//触发事件,通知app

IntegersAdded(_x, _y, result);

return result;

}

YourContract.IntegersAdded(function(error, result) {

// 干些事

}

映射(****Mapping)和地址(Address)

Addresses(地址)

以太坊区块链由 account (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 以太 (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。

每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:

0x0cE446255506E92DF41614C46F1d6df9Cc969183

Mapping(映射)

//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:

mapping (address => uint) public accountBalance;

//或者可以用来通过userId 存储/查找的用户名

mapping (uint => string) userIdToName;

msg.sender

在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address。

注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender 总是存在的。

注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender 总是存在的。

以下是使用**** msg.sender 来更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {

// 更新我们的 favoriteNumber 映射来将 _myNumber存储在 msg.sender名下

favoriteNumber[msg.sender] = _myNumber;

// 存储数据至映射的方法和将数据存储在数组相似

}

function whatIsMyNumber() public view returns (uint) {

// 拿到存储在调用者地址名下的值

// 若调用者还没调用 setMyNumber,则值为 0

return favoriteNumber[msg.sender];

}

Require

function sayHiToVitalik(string _name) public returns (string) {

// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序

// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较

// 两字符串的 keccak256 哈希值来进行判断)

require(keccak256(_name) == keccak256("Vitalik"));

// 如果返回 true, 运行如下语句

return "Hi!";

}

继承 Inheritance

有个让**** Solidity 的代码易于管理的功能,就是合约 inheritance (继承):

contract Doge {

function catchphrase() public returns (string) {

return "So Wow CryptoDoge";

}

}

contract BabyDoge is Doge {

function anotherCatchphrase() public returns (string) {

return "Such Moon BabyDoge";

}

}

由于 BabyDoge 是从 Doge 那里 inherits (继承)过来的。 这意味着当你编译和部署了 BabyDoge,它将可以访问 catchphrase() 和 anotherCatchphrase()和其他我们在 Doge 中定义的其他公共函数。

引入****Import

Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import 语句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

Storage与Memory

在 Solidity 中,有两个地方可以存储变量 —— storage 或 memory。

Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。

internal 和 external

internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的内部函数。(嘿,这听起来正是我们想要的那样!)。

external 与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 external 和 public。

Ownable

下面是一个 Ownable 合约的例子: 来自 OpenZeppelin Solidity 库的 Ownable 合约。 OpenZeppelin 是主打安保和社区审查的智能合约库,您可以在自己的 DApps中引用。等把这一课学完,您不要催我们发布下一课,最好利用这个时间把 OpenZeppelin 的网站看看,保管您会学到很多东西!

  • 构造函数:function Ownable()是一个 constructor (构造函数),构造函数不是必须的,它与合约同名,构造函数一生中唯一的一次执行,就是在合约最初被创建的时候。
  • 函数修饰符:modifier onlyOwner()。 修饰符跟函数很类似,不过是用来修饰其他已有函数用的, 在其他语句执行前,为它检查下先验条件。 在这个例子中,我们就可以写个修饰符 onlyOwner 检查下调用者,确保只有合约的主人才能运行本函数。我们下一章中会详细讲述修饰符,以及那个奇怪的_;。
  • indexed 关键字:别担心,我们还用不到它。

    • @title Ownable*
    • @dev The Ownable contract has an owner address, and provides basic authorization control*
    • functions, this simplifies the implementation of "user permissions".*
  • /

contract Ownable {

address public owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**

** @dev The Ownable constructor sets the original owner of the contract to the sender*

** account.*

*/

function Ownable() public {

owner = msg.sender;

}

/**

** @dev Throws if called by any account other than the owner.*

*/

modifier onlyOwner() {

require(msg.sender == owner);

_;

}

/**

** @dev Allows the current owner to transfer control of the contract to a newOwner.*

** @param newOwner The address to transfer ownership to.*

*/

function transferOwnership(address newOwner) public onlyOwner {

require(newOwner != address(0));

OwnershipTransferred(owner, newOwner);

owner = newOwner;

}

}

Gas-驱动以太坊DApps的能源

在 Solidity 中,你的用户想要每次执行你的 DApp 都需要支付一定的 gasgas 可以用以太币购买,因此,用户每次跑 DApp 都得花费以太币。

一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。

由于运行你的程序需要花费用户的真金白银,在以太坊中代码的编程语言,比其他任何编程语言都更强调优化。同样的功能,使用笨拙的代码开发的程序,比起经过精巧优化的代码来,运行花费更高,这显然会给成千上万的用户带来大量不必要的开销。

为何要****gas来驱动?

以太坊就像一个巨大、缓慢、但非常安全的电脑。当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算,以验证它的输出 —— 这就是所谓的”去中心化“ 由于数以千计的节点同时在验证着每个功能的运行,这可以确保它的数据不会被被监控,或者被刻意修改。

可能会有用户用无限循环堵塞网络,抑或用密集运算来占用大量的网络资源,为了防止这种事情的发生,以太坊的创建者为以太坊上的资源制定了价格,想要在以太坊上运算或者存储,你需要先付费。

注意:如果你使用侧链,倒是不一定需要付费,比如咱们在 Loom Network 上构建的 CryptoZombies 就免费。你不会想要在以太坊主网上玩儿“魔兽世界”吧? - 所需要的 gas 可能会买到你破产。但是你可以找个算法理念不同的侧链来玩它。我们将在以后的课程中咱们会讨论到,什么样的 DApp 应该部署在太坊主链上,什么又最好放在侧链。

时间单位

readyTime 稍微复杂点。我们希望增加一个“冷却周期”,表示僵尸在两次猎食或攻击之之间必须等待的时间。如果没有它,僵尸每天可能会攻击和繁殖1,000次,这样游戏就太简单了。

Solidity 使用自己的本地时间单位。

变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。我写这句话时 unix 时间是 1515527488。

注意:Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!

Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。所以 1分钟 就是 60,1小时是 3600(60秒×60分钟),1天是86400(24小时×60分钟×60秒),以此类推

函数修饰符

  • 1、我们有决定函数何时和被谁调用的可见性修饰符: private 意味着它只能被合约内部调用; internal 就像 private 但是也能被继承的合约调用; external 只能从合约外部调用;最后 public 可以在任何地方调用,不管是内部还是外部。
  • 2、我们也有状态修饰符, 告诉我们函数如何和区块链交互: view 告诉我们运行这个函数不会更改和保存任何数据; pure 告诉我们这个函数不但不会往区块链写数据,它甚至不从区块链读取数据。这两种在被从合约外部调用的时候都不花费任何gas(但是它们在被内部其他函数调用的时候将会耗费gas)。
  • 3、然后我们有了自定义的 modifiers,例如在第三课学习的: onlyOwner 和 aboveLevel。 对于这些修饰符我们可以自定义其对函数的约束逻辑。

payable修饰符

payable 方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种可以接收以太的特殊函数。

先放一下。当你在调用一个普通网站服务器上的API函数的时候,你无法用你的函数传送美元——你也不能传送比特币。

但是在以太坊中, 因为钱 (以太), 数据 (事务负载), 以及合约代码本身都存在于以太坊。你可以在同时调用函数 并付钱给另外一个合约。

这就允许出现很多有趣的逻辑, 比如向一个合约要求支付一定的钱来运行一个函数。

示例

contract OnlineStore {

function buySomething() external payable {

// 检查以确定0.001以太发送出去来运行函数:

require(msg.value == 0.001 ether);

// 如果为真,一些用来向函数调用者发送数字内容的逻辑

transferThing(msg.sender);

}

}

提现

在上一节,我们学习了如何向合约发送以太,那么在发送之后会发生什么呢?

在你发送以太之后,它将被存储进以合约的以太坊账户中, 并冻结在哪里 —— 除非你添加一个函数来从合约中把以太提现。

你可以写一个函数来从合约中提现以太,类似这样:

contract GetPaid is Ownable {

function withdraw() external onlyOwner {

owner.transfer(this.balance);

}

}

随机数

优秀的游戏都需要一些随机元素,那么我们在 Solidity 里如何生成随机数呢?

真正的答案是你不能,或者最起码,你无法安全地做到这一点。

我们来看看为什么

用** keccak256 **来制造随机数
Solidity 中最好的随机数生成器是 keccak256 哈希函数.

我们可以这样来生成一些随机数

// 生成一个0到100的随机数:

uint randNonce = 0;

uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;

randNonce++;

uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

这个方法首先拿到 now 的时间戳、 msg.sender、 以及一个自增数 nonce (一个仅会被使用一次的数,这样我们就不会对相同的输入值调用一次以上哈希函数了)。

然后利用 keccak 把输入的值转变为一个哈希值, 再将哈希值转换为 uint, 然后利用 % 100 来取最后两位, 就生成了一个0到100之间随机数了。

这个方法很容易被不诚实的节点攻击
在以太坊上, 当你在和一个合约上调用函数的时候, 你会把它广播给一个节点或者在网络上的 transaction 节点们。 网络上的节点将收集很多事务, 试着成为第一个解决计算密集型数学问题的人,作为“工作证明”,然后将“工作证明”(Proof of Work, PoW)和事务一起作为一个 block 发布在网络上。

一旦一个节点解决了一个PoW, 其他节点就会停止尝试解决这个 PoW, 并验证其他节点的事务列表是有效的,然后接受这个节点转而尝试解决下一个节点。

这就让我们的随机数函数变得可利用了

我们假设我们有一个硬币翻转合约——正面你赢双倍钱,反面你输掉所有的钱。假如它使用上面的方法来决定是正面还是反面 (random >= 50 算正面, random < 50 算反面)。

如果我正运行一个节点,我可以 只对我自己的节点 发布一个事务,且不分享它。 我可以运行硬币翻转方法来偷窥我的输赢 — 如果我输了,我就不把这个事务包含进我要解决的下一个区块中去。我可以一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,然后获利。

所以我们该如何在以太坊上安全地生成随机数呢?

因为区块链的全部内容对所有参与者来说是透明的, 这就让这个问题变得很难,它的解决方法不在本课程讨论范围,你可以阅读 这个 StackOverflow 上的讨论 来获得一些主意。 一个方法是利用 oracle 来访问以太坊区块链之外的随机数函数。

ERC20 ERC721标准(七)

预防溢出

僵尸转移给0 地址(这被称作 “烧币”, 基本上就是把代币转移到一个谁也没有私钥的地址,让这个代币永远也无法恢复)

假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255).

来看看下面的代码。最后 number 将会是什么值?

uint8 number = 255;

number++;

在这个例子中,我们导致了溢出 — 虽然我们加了1, 但是 number 出乎意料地等于 0了。 (如果你给二进制 11111111 加1, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00)。

下溢(underflow)也类似,如果你从一个等于 0 的 uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

使用 SafeMath

为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的 库(library),默认情况下可以防止这些问题。

不过在我们使用之前…… 什么叫做库?

一个库是 Solidity 中一种特殊的合约。其中一个有用的功能是给原始数据类型增加一些方法。

比如,使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — add, sub, mul, 以及 div。现在我们可以这样来让 uint256 调用这些方法:

using SafeMath for uint256;

uint256 a = 5;

uint256 b = a.add(3); // 5 + 3 = 8

uint256 c = a.mul(2); // 5 * 2 = 10

assertrequire区别

assert 和 require 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gasassert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 require,assert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。

Solidity 社区所使用的一个标准是使用一种被称作 natspec 的格式,看起来像这样:

/// @title 一个简单的基础运算合约

/// @author H4XF13LD MORRIS

/// @notice ÏÖÔÚ£¬Õâ¸öºÏÔ¼Ö»Ìí¼ÓÒ»¸ö³Ë·¨

你可能感兴趣的:(智能合约教程)