Solidity语言-----够用级别

Posted 云灬沙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity语言-----够用级别相关的知识,希望对你有一定的参考价值。

Solidity微教程

学习solidity说实话到目前没有特别好的教程,但是CryptoZombies,通过游戏来学习绝对算一个比较好的方式,对于入门和编写基本的合约绝对够用
本文是对Zombies的知识的提炼,可以与Zombies配套使用

0.简介

Solidity是一种静态语言,在编程中需要指定每个变量的类型。语法与javascripts比较接近

1.版本声明

关键字: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中的^和版本号紧靠着

2.创建合约

关键字:contract(类似于主函数)
contract ZombieFactory {//关键字contract,ZombieFatory是合约名
    
}

3.状态变量

状态变量会永久被保存在合约中,会写入区块链
变量类型意义及范围
uintuint256
booltrue/false
int8 to int2568位到256位的带符号整型数。int256与int相同。
uint8 to uint2568位到256位的无符号整型。uint256和uint是一样的。
uint dnaDigits = 16;

4.数学运算

加法x+y
减法x-y
乘法x*y
除法x/y
取余x%y
乘方x**y(x的y次方)

5.结构体

关键字:struct
struct Person{//结构体声明,与C基本一致,但是没有分号
	uint age;
	string name;
}

6.数组

支持静态数组和动态数组
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"));

7.函数

关键字:function
习惯上函数里的变量是以_开头的以区分全局变量
function eatHamburgers(string _name, uint _amount) {
}

1)函数属性

public 意味着任何一方或其他合约都可以调用合约里面的函数,默认为pulic
private 只有同一个合约中的其他函数才可以调用这个函数,建议使用
ps: 私有函数的名字用_开始

private 意味着它只能被合约内部调用; internal 就像 private 但是也能被继承的合约调用; external 只能从合约外部调用;最后 public 可以在任何地方调用,不管是内部还是外部。

function _addToArray(uint _number) private {

}

2)函数返回值

关键字:returns
ps:可以一次性返回多个值,与C++等有所区别
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();
}

3)函数修饰符

关键字:view,pure
view只能读取数据但是不能更改数据
function sayHello() public view returns (string) {
pure表明这个函数甚至都不访问应用里的数据,返回值完全取决于它的输入参数,可以理解为一个闭环
function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}

4)函数可见性

关键值:internalexternal

internalprivate 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“internal”函数。

externalpublic 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。

8.Keccak256

SHA3版本,将一个字符串转变为一个256位的16进制数

9.类型转换

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;
    }

10.事件

关键字: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()这个函数本身返回的是一个索引值?

11.映射

关键字:mapping
mapping(address => uint) public accountBalance;将用户的余额保存在一个uint类型的变量中

映射本质上是一个存储和查找数据所用的键值对,第一个参数是键,第二个参数是值,左边对应的就是右边的

12.地址

关键字:address

13.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相当于一个转换函数

14.require

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行

15.继承

关键字: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";
  }
}

16.引入

关键字:import
import "./someothercontract.sol";
using ... for ...
using SafeMath for uint256;//使用SafeMath替换uint256

17.存储位置

关键字:storagememory

存储在内存(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;
    // ...如果你想把副本的改动保存回区块链存储
  }
}

18.接口interface

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 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`变量做些什么
  }
}

19.if语句

与其他语言没有区别

20.函数修饰符

关键字: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 

21.时间

变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)

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

注意:必须使用 uint32(...) 进行强制类型转换,因为 now 返回类型 uint256。所以我们需要明确将它转换成一个 uint32 类型的变量。

22.将结构体作为参数传入

由于结构体的存储指针可以以参数的方式传递给一个 privateinternal 的函数,因此结构体可以在多个函数之间相互传递。

遵循这样的语法:

function _doStuff(Zombie storage _zombie) internal {
  // do stuff with _zombie
}

这样我们可以将某僵尸的引用直接传递给一个函数,而不用是通过参数传入僵尸ID后,函数再依据ID去查找。

23.payable函数

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

24.withdraw函数

提现函数

25.sender.transfer()

发送者.transfer(以太值)

26.用 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 算反面)。

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

27.代币

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;
}

28.SafeMath

使用 SafeMath 库的时候,我们将使用 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — addsubmul, 以及 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 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。

29.代码注释

  • 单行注释//
  • 多行注释/**/
  • natspec格式
    • /// @title
    • /// @ author
    • /// @param x
    • /// @return z (x*y)
    • ///@dev 对函数的描述

XX.编写合约的注意事项

1)外部依赖关系

前面写的合约中用到了CryptoKitties的合约地址作为硬编码,如果加密猫消失了,那么加密僵尸也会受到影响,所以不能采用硬编码的方式,而是采用函数的形式

比如,不再一开始就把猎物的地址写入代码,而是写个函数,运行时再设定猎物的地址,这样就可以随时去锁定新的猎物,不用担心小猫的消失。作为输入参数,而不是写进合约里,合约是无法更改的。

2)_OpenZeppelin_Solidity库的Ownable函数

OpenZeppelin 是主打安保和社区审查的智能合约库,可以在自己的 DApps中引用。
/**
 * @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;
  }
}

3)节省Gas的技巧

  • 结构封装

    • 通常不会考虑使用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(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。

4)想要防止漏洞,最简单的方法就是将函数可见性设为 internal

5)优秀的游戏都需要一些随机元素

以上是关于Solidity语言-----够用级别的主要内容,如果未能解决你的问题,请参考以下文章

智能合约从入门到精通:Solidity汇编语言

区块链Solidity智能合约语言学习笔记

智能合约语言 Solidity 教程系列5 - 数组介绍

第一行代码:以太坊-使用Solidity语言开发和测试智能合约

智能合约从入门到精通:Solidity Assembly

区块链特辑——solidity语言基础