【编程】solidity打僵尸笔记

文章目录

  • 基础语法
    • 数据类型
    • 函数
      • Gas费用相关
      • 可见性
      • 交互
      • 代码复用
      • 修饰符
      • 其他功能函数
  • 智能合约特点
    • 永固性Immunity
    • GAS
    • 修饰符payable
    • 随机数
  • 合约案例
    • OpenZeppelin库
    • ERC721
    • SafeMath

本文知识点来自于西蒙斯直播和cryptozombies.io。

基础语法

数据类型

整数:uint, uint8, uint16, uint32, …, uint256,非负整数。

地址:address

结构体:struct,类似于python的dict、class的某些应用场景。

数据:固定长度和非固定长度

映射:mapping

constract ZombieFactory {
    uint zombieDna = 20;
    struct Zombie {
        uint8 height;
        string name;
        address addressId;
    }
    Zombie[] public zombies;
    mapping(uint => address) public zombieToOwner;
}

函数

Gas费用相关

view意味着只能读取不能更改数据,用户调用其时不需要支付gas,pure表明这个函数不访问应用里的数据。

function multiply(uint _x, uint _y) public pure returns (uint) {
    return _x * _y;
}

function getMyFavoriteNumber() public view returns (uint256) {
    return favoriteNumber;
}

在 Solidity 中,有两个地方可以存储变量 —— storagememory

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

使用storage(存储)是相当昂贵的,”写入“操作尤其贵。这是因为,无论是写入还是更改一段数据,这都将永久性地写入区块链,意味着需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长、拷贝份数更多、存储量也就越大。

可见性

Solidity 定义的函数的属性默认为public,意味着任何一方 (或其它合约) 都可以调用合约里的函数,这样的合约易于受到攻击,因此最好只有当需要外部世界调用它时才将它设置为public

uint[] numbers;

function _addToArray(uint _number) private {
  numbers.push(_number);
}

Solidity 还使用了另外两个描述函数可见性的修饰符:internal(内部) 和 external(外部)。internalprivate 类似,如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。externalpublic 类似,只不过这些函数只能在合约之外调用,它们不能被合约内的其他函数调用。

contract Sandwich {
  uint private sandwichesEaten = 0;

  function eat() internal {
    sandwichesEaten++;
  }
}

contract BLT is Sandwich {
  uint private baconSandwichesEaten = 0;

  function eatWithBacon() public returns (string) {
    baconSandwichesEaten++;
    // 因为eat() 是internal 的,所以我们能在这里调用
    eat();
  }
}

交互

与外界交互最主要的路径就是链上交互,有一些全局变量是可以被所有函数调用的,比如msg.sender,指的是当前调用者的address,该方法具有以太坊区块链的安全保障,除非窃取以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。

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

代码复用

import "./xxx.sol";是一个调用的方法。

继承Inheritance也是一个方法,假设已经有contract Doge了,那么使用contract BabyDoge is Doge即可继承Doge中定义的公共函数。

修饰符

修饰符跟函数很类似,不过是用来修饰其他已有函数用的, 在其他语句执行前,为它检查下先验条件。它不能像函数那样被直接调用,只能被添加到函数定义的末尾。

如OpenZeppelin库的modifier 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) {
  // 其余的程序逻辑
}

此外,常见的修饰符有可见性修饰符:private, public, internal, external;状态修饰符:view, pure;支付修饰符:payable。这些修饰符可以同时作用于一个函数定义上。

其他功能函数

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行,在调用一个函数之前,用其验证前置条件是非常有必要的:

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

智能合约特点

永固性Immunity

把智能合约上传到以太坊上后就变得不可更改,这种特性意味着上传的代码永远不能被调整或更新。这是以太坊安全性的一个重要原因,但如果智能合约有漏洞,即便发现了也没有办法,只能让用户放弃这个智能合约并转到新的修复后的合约。

因此,正常不能硬编码,而要采用函数,以便DApp的关键部分可以以参数形式修改。

GAS

在以太坊上,用户每次执行DApp都要支付一定的gas费用,一个DApp操作收取多少gas取决于功能逻辑的复杂程度,比如存储数据就比做个加法计算贵得多。因为当你运行一个程序的时候,网络上的每一个节点都在进行相同的运算以验证它的输出,为了防止恶意用户堵塞网络,以太坊的创建者采用付费成本的方式来抑制这一行为。也因此solidity更强调优化,精巧优化的代码会为用户节省更多的gas费。

修饰符payable

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

当我们在调用一个普通网站服务器上的API函数的时候,是无法用函数传送美元、比特币的。但是在以太坊中, 因为以太币、数据 (transaction payload), 以及合约代码都存在于以太坊,于是可以在调用函数的同时并付钱给合约。这就允许出现很多有趣的逻辑, 比如向一个合约要求支付钱来运行一个函数。

contract OnlineStore {
  function buySomething() external payable {
    // 检查以确定0.001以太发送出去来运行函数:
    require(msg.value == 0.001 ether);
    // 如果为真,一些用来向函数调用者发送数字内容的逻辑
    transferThing(msg.sender);
  }
}

随机数

Solidity 中最好的随机数生成器是 keccak256 哈希函数。这个方法首先拿到 now 的时间戳、 msg.sender、 以及一个自增数 nonce (一个仅会被使用一次的数,这样我们就不会对相同的输入值调用一次以上哈希函数了),然后利用 keccak 把输入的值转变为一个哈希值, 再将哈希值转换为 uint, 然后利用 % 100 来取最后两位, 就生成了一个0到100之间随机数了。

// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

这个方法很容易被不诚实的节点攻击,当调用合约时会广播给节点们,网络上的节点会收集很多事务,并试着成为第一个解决POW问题的节点,一旦一个节点解决了一个POW,其他节点就会停止解决,并验证其它节点的事务列表是有效的,然后接受这个区块再去进行下一个区块的操作。

假设我们有一个硬币正反合约,正面赢双倍钱,反面输掉所有的钱。假如它使用上面的方法来决定是正面还是反面 (random >= 50 算正面, random < 50 算反面)。如果我正运行一个节点,我可以只对我自己的节点发布一个事务,并不分享它。如果我输了,就不把这个事务包含进下一个区块中去,此时可以一直运行这个方法,直到我赢得了并解决了下一个区块,然后获利。

解决方法中,有一个方法是利用oracle(预言机)来访问以太坊区块链之外的随机数函数。

合约案例

OpenZeppelin库

OpenZeppelin 是主打安保和社区审查的智能合约库,其中Ownable合约被经常用来指定合约所有权。

/**
 * @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;
  }
}
  • 构造函数:function Ownable()是一个 _ constructor_ (构造函数),构造函数不是必须的,它与合约同名,构造函数一生中唯一的一次执行,就是在合约最初被创建的时候。
  • 函数修饰符:modifier onlyOwner()。 修饰符跟函数很类似,不过是用来修饰其他已有函数用的, 在其他语句执行前,为它检查下先验条件。 在这个例子中,就可以写个修饰符 onlyOwner 检查下调用者,确保只有合约的主人才能运行本函数。
  • _;当执行transferOwnership函数时,会首先执行 onlyOwner 中的代码, 执行到 onlyOwner 中的 _; 语句时,程序再返回并执行 transferOwnership 中的代码。

所以Ownable 合约基本都会这么干:

  1. 合约创建,构造函数先行,将其 owner 设置为msg.sender(其部署者)
  2. 为它加上一个修饰符 onlyOwner,它会限制陌生人的访问,将访问某些函数的权限锁定在 owner 上。
  3. 允许将合约所有权转让给他人。

ERC721

即NFT,ERC721 代币是不能互换的,因为每个代币都被认为是唯一且不可分割的,只能以整个单位交易它们,并且每个单位都有唯一的ID。

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

balanceOf这个函数只需要一个传入 address 参数,然后返回这个 address 拥有多少代币。

ownerOf这个函数需要传入一个代币 ID 作为参数 (我们的情况就是一个僵尸 ID),然后返回该代币拥有者的 address

ERC721 规范有两种不同的方法来转移代币:

  1. 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId
  2. 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。

transfertakeOwnership 都将包含相同的转移逻辑,只是以相反的顺序。 (一种情况是代币的发送者调用函数;另一种情况是代币的接收者调用它)。

案例:

contract ZombieOwnership is ZombieAttack, ERC721 {

  mapping (uint => address) zombieApprovals;

  function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerZombieCount[_owner];
  }

  function ownerOf(uint256 _tokenId) public view returns (address _owner) {
    return zombieToOwner[_tokenId];
  }

  function _transfer(address _from, address _to, uint256 _tokenId) private {
    ownerZombieCount[_to]++;
    ownerZombieCount[_from]--;
    zombieToOwner[_tokenId] = _to;
    Transfer(_from, _to, _tokenId);
  }

  function transfer(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    _transfer(msg.sender, _to, _tokenId);
  }

  function approve(address _to, uint256 _tokenId) public onlyOwnerOf(_tokenId) {
    zombieApprovals[_tokenId] = _to;
    Approval(msg.sender, _to, _tokenId);
  }

  function takeOwnership(uint256 _tokenId) public {
    require(zombieApprovals[_tokenId] == msg.sender);
    address owner = ownerOf(_tokenId);
    _transfer(owner, msg.sender, _tokenId);
  }
}

SafeMath

智能合约存在一个主要的安全特性:防止溢出(overflow)和下溢(underflow)。

假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255)。那对于255来说,+1则等于0了,即给二进制 11111111 加1, 它将被重置为 00000000,就像钟表从 23:59 走向 00:00。下溢也类似,如果从一个等于 0uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

为了防止这些情况,OpenZeppelin 建立了一个叫做 SafeMath 的库。

使用 SafeMath 库的时候,要在合约前声明 using SafeMath for uint256 这样的语法。 SafeMath 库有四个方法 — addsubmul, 以及 div

using SafeMath for uint256;
using SafeMath for uint;

uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10

uint test = 2;
test = test.mul(3); // test 等于 6 了

你可能感兴趣的:(编程,以太坊,区块链,数字货币)