学习solidity说实话到目前没有特别好的教程,但是CryptoZombies,通过游戏来学习绝对算一个比较好的方式,对于入门和编写基本的合约绝对够用
本文是对Zombies的知识的提炼,可以与Zombies配套使用
pragma solidity
pragma solidity ^0.4.0;
表明源代码使用Solidity版本0.4.0写的,并且使用0.4.0以上版本运行也没问题(最高到0.5.0,但是不包含0.5.0)
pragma solidity >=0.4.0;
编译器版本号大于等于0.4.0版本都可以,例如0.6.X都是可以的
ps:^0.4.0中的^和版本号紧靠着
contract
(类似于主函数)contract ZombieFactory {//关键字contract,ZombieFatory是合约名
}
变量类型 | 意义及范围 |
---|---|
uint | uint256 |
bool | true/false |
int8 to int256 | 8位到256位的带符号整型数。int256与int相同。 |
uint8 to uint256 | 8位到256位的无符号整型。uint256和uint是一样的。 |
uint dnaDigits = 16;
加法 | x+y |
---|---|
减法 | x-y |
乘法 | x*y |
除法 | x/y |
取余 | x%y |
乘方 | x**y(x的y次方) |
struct
struct Person{//结构体声明
uint age;
string name;
}
uint[2] Array1;//固定长度为2
uint[] dynamicArray2;//动态数组,可以动态添加元素
Person[] public people;//Person类型的数组,命名为people
//可以定义publics数组,Solidity会自动创建getter方法
尾部
,类似于C++push
Person satoshi =Person(172,"Satoshi");
people.push(satoshi);
或者
people.push(Person(172,"satoshi"));
function
_
开头的以区分全局变量function eatHamburgers(string _name, uint _amount) {
}
public
意味着任何一方或其他合约都可以调用合约里面的函数,默认为pulicprivate
只有同一个合约中的其他函数才可以调用这个函数,建议使用_
开始private
意味着它只能被合约内部调用; internal
就像 private
但是也能被继承的合约调用; external
只能从合约外部调用;最后 public
可以在任何地方调用,不管是内部还是外部。
function _addToArray(uint _number) private {
}
returns
string greeting = "What's up dog";
uint id= 165;
function sayHello() public returns (string,uint) {
return greeting,id;
}
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// 这样来做批量赋值:
(a, b, c) = multipleReturns();
}
view
,pure
view
只能读取数据但是不能更改数据function sayHello() public view returns (string) {
pure
表明这个函数甚至都不访问应用里的数据,返回值完全取决于它的输入参数,可以理解为一个闭环function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
internal
和external
internal
和 private
类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“internal”函数。
external
与public
类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
a * b
返回类型是 uint
, 但是当我们尝试用 uint8
类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8
, 就可以了,编译器也不会出错。
小的范围可以直接转换到大的范围,但是大的范围转到小的可能会出现问题,必须强转
function _generateRandomDna(string _str) private view returns (uint) {
uint rand =uint(keccak256(_str));
return rand%dnaDigits;
}
event
事件是合约与区块链进行通讯的一种机制,前端监听事件,后端做出反应
event IntergersAdded(uint x,uint y,uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
emit IntegersAdded(_x, _y, result);
return result;
}
你的 app 前端可以监听这个事件。JavaScript 实现如下:
YourContract.IntegersAdded(function(error, result) {
// do something
})
array.push()
返回数组的长度类型是uint
- 因为数组的第一个元素的索引是 0, array.push() - 1
将是我们加入的僵尸的索引。 zombies.push() - 1
就是 id
,数据类型是 uint
。
**这里有点不太理解,
uint id= zombies.push(Zombie(_name,_dna))-1;
是zombies.push()这个函数本身返回的是一个索引值?
mapping
mapping(address => uint) public accountBalance;
将用户的余额保存在一个uint类型的变量中映射本质上是一个存储和查找数据所用的键值对,第一个参数是键,第二个参数是值,左边对应的就是右边的
address
msg.sender
全局变量,可以被所有函数调用,指的是当前调用智能合约的address
在Solidity中,功能执行始终都是需要从外部调用者开始的,一个合约部署到区块链上不会做任何事情,除非有人去调用其中的功能,所以
msg.sender
总是存在的
使用
msg.sender
很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。
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];
}
**重点:**使用[]
来读取值,[]里面是键,传进去的值是值,直接调用favoriteNumber[msg.sender]
出来的是值,mapping相当于一个转换函数
require
require
使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行is
可以多继承,只需要用逗号隔开就好
contract A is B,C{
}
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
import
import "./someothercontract.sol";
using ... for ...
using SafeMath for uint256;//使用SafeMath替换uint256
storage
和memory
存储在内存(memory)还是存储在存储介质中(storage)中
Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。
在数组后面加上 memory
关键字, 表明这个数组是仅仅在内存中创建,不需要写入外部存储,并且在函数调用结束时它就解散了
有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 结构体 和 **数组 ** 时:
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 看上去很直接,不过 Solidity 将会给出警告
// 告诉你应该明确在这里定义 `storage` 或者 `memory`。
// 所以你应该明确定义 `storage`:
Sandwich storage mySandwich = sandwiches[_index];
// ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
// 在存储里,另外...
mySandwich.status = "Eaten!";
// ...这将永久把 `sandwiches[_index]` 变为区块链上的存储
// 如果你只想要一个副本,可以使用`memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
// 另外
anotherSandwich.status = "Eaten!";
// ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
// 不过你可以这样做:
sandwiches[_index + 1] = anotherSandwich;
// ...如果你想把副本的改动保存回区块链存储
}
}
如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。
没有用{}或者()来定义函数主体,而是跟了一个分号
interface NumberInterface {
function getNum(address _myAddress) public view returns (uint);//注意分号必须加
}
接口与抽象合约相似,但是没有任何可执行的函数,还有一些其他的限制
调用接口
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ^ 这是FavoriteNumber合约在以太坊上的地址
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// 现在变量 `numberContract` 指向另一个合约对象
function someFunction() public {
// 现在我们可以调用在那个合约中声明的 `getNum`函数:
uint num = numberContract.getNum(msg.sender);
// ...在这儿使用 `num`变量做些什么
}
}
modifier
,_;
不能像函数那样被直接调用,只能被添加到函数定义的末尾,用以改变函数的行为。
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
contract MyContract is Ownable {
event LaughManiacally(string laughter);
//注意! `onlyOwner`上场 :
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
注意 likeABoss
函数上的 onlyOwner
修饰符。 当你调用 likeABoss
时,首先执行 onlyOwner
中的代码, 执行到 onlyOwner
中的 _;
语句时,程序再返回并执行 likeABoss
中的代码。
尽管函数修饰符也可以应用到各种场合,但最常见的还是放在函数执行之前添加快速的 require
检查。
注意,直接将修饰符放到后面就可以,不需要加其他的
之前我们已经读过一个简单的函数修饰符了:onlyOwner
。函数修饰符也可以带参数。例如:
// 存储用户年龄的映射
mapping (uint => uint) public age;
// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {
// 其余的程序逻辑
}
看到了吧, olderThan
修饰符可以像函数一样接收参数,是“宿主”函数 driveCar
把参数传递给它的修饰符的。
修饰符可以同时作用于一个函数定义上:
function test() external view onlyOwner anotherModifier
变量 now
将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)
Solidity 包含秒(seconds)
,分钟(minutes)
,小时(hours)
,天(days)
,周(weeks)
和 年(years)
等时间单位。它们都会转换成对应的秒数放入 uint
中。所以 1分钟
就是 60
,1小时
是 3600
(60秒×60分钟),1天
是86400
(24小时×60分钟×60秒),以此类推。
注意:必须使用
uint32(...)
进行强制类型转换,因为now
返回类型uint256
。所以我们需要明确将它转换成一个uint32
类型的变量。
由于结构体的存储指针可以以参数的方式传递给一个 private
或 internal
的函数,因此结构体可以在多个函数之间相互传递。
遵循这样的语法:
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
这样我们可以将某僵尸的引用直接传递给一个函数,而不用是通过参数传入僵尸ID后,函数再依据ID去查找。
payable
方法是让 Solidity 和以太坊变得如此酷的一部分 —— 它们是一种可以接收以太的特殊函数。属于modifier
提现函数
发送者.transfer(以太值)
keccak256
来制造随机数。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
算反面)。
如果我正运行一个节点,我可以 只对我自己的节点 发布一个事务,且不分享它。 我可以运行硬币翻转方法来偷窥我的输赢 — 如果我输了,我就不把这个事务包含进我要解决的下一个区块中去。我可以一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,然后获利。
ERC20
ERC721
ERC721标准
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}
使用 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
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
首先我们有了 library 关键字 — 库和 合约很相似,但是又有一些不同。 就我们的目的而言,库允许我们使用 using 关键字,它可以自动把库的所有方法添加给一个数据类型:
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了
注意 mul 和 add 其实都需要两个参数。 在我们声明了 using SafeMath for uint 后,我们用来调用这些方法的 uint 就自动被作为第一个参数传递进去了(在此例中就是 test)
我们来看看 add 的源代码看 SafeMath 做了什么:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
基本上 add 只是像 + 一样对两个 uint 相加, 但是它用一个 assert 语句来确保结果大于 a。这样就防止了溢出。
assert 和 require 相似,若结果为否它就会抛出错误。 assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。所以大部分情况下,你写代码的时候会比较喜欢 require,assert 只在代码可能出现严重错误的时候使用,比如 uint 溢出。
所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。
//
/**/
前面写的合约中用到了CryptoKitties的合约地址作为硬编码,如果加密猫消失了,那么加密僵尸也会受到影响,所以不能采用硬编码的方式,而是采用函数的形式
比如,不再一开始就把猎物的地址写入代码,而是写个函数,运行时再设定猎物的地址,这样就可以随时去锁定新的猎物,不用担心小猫的消失。作为输入参数,而不是写进合约里,合约是无法更改的。
/**
* @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;
}
}
结构封装
通常不会考虑使用uint,会使用uint8而不是uint256来节省gas
如果一个 struct
中有多个 uint
,则尽可能使用较小的 uint
, Solidity 会将这些 uint
打包在一起,从而占用较少的存储空间。例如:
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
当 uint
定义在一个 struct
中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct
:
uint c; uint32 a; uint32 b;` 和 `uint32 a; uint c; uint32 b;
前者比后者需要的gas更少,因为前者把uint32
放一起了。
使用view
当玩家从外部调用一个view
函数,是不需要支付一分 gas 的。
这是因为 view
函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view
标记一个函数,意味着告诉 web3.js
,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
在所能只读的函数上标记上表示“只读”的“external view
声明,就能为你的玩家减少在 DApp 中 gas 用量
注意:如果一个
view
函数在另一个函数的内部被调用,而调用函数与view
函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为view
的函数只有在外部调用时才是免费的。
在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view
的函数,遍历比 storage
要便宜太多,因为 view
函数不会产生任何花销。
不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑 - 比如每次调用一个函数,都需要在 memory
(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。
internal
。