智能合约学习 CryptoZombies 僵尸工厂 第三课高级理论

349 阅读6分钟

在前两节课了解了基本语法之后第三节课要介绍一些常见问题的处理办法:智能合约所有权,GAS花费,代码优化,代码安全。 是相当重要的一节课了。

智能合约永固性

智能合约一旦上传不能修改但是我们是可以通过实现方法修改值来改变。

ownableContracts

但是通过set方法修改变量值的方法不能被所有人访问,一般只能被智能合约所有者使用,这时候除了自己实现检查调用者的逻辑之外可以使用## OpenZeppelin库中的Ownable合约。

快速使用方式

contract MyContract is Ownable {
  event LaughManiacally(string laughter);

  //注意! `onlyOwner`上场 :
  function likeABoss() external onlyOwner {
    LaughManiacally("Muahahahaha");
  }
}

注意likeABoss函数上的onlyOwner修饰符。 当你调用likeABoss时,首先执行onlyOwner中的代码, 执行到onlyOwner中的_;语句时,程序再返回并执行likeABoss中的代码。可见,尽管函数修饰符也可以应用到各种场合,但最常见的还是放在函数执行之前添加快速的require检查。

因为给函数添加了修饰符onlyOwner,使得唯有合约的主人(也就是部署者)才能调用它。

Gas

在第1课中,我们提到除了基本版的 uint 外,还有其他变种uint:uint8,uint16,uint32等。通常情况下我们不会考虑使用uint变种,因为无论如何定义uint的大小,Solidity为它保留256位的存储空间。例如,使用uint8而不是uint(uint256)不会为你节省任何 gas。除非,把uint绑定到struct里面。

如果一个struct中有多个uint,则尽可能使用较小的uint,Solidity会将这些 uint打包在一起,从而占用较少的存储空间。例如:

struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}
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放一起了。

时间单位

Solidity使用自己的本地时间单位。变量now将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。

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

下面是一些使用时间单位的实用案例:

**

uint lastUpdated;

// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {
  lastUpdated = now;
}

// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {
  return (now >= (lastUpdated + 5 minutes));
}

有了这些工具,我们可以为僵尸设定”冷静时间“功能。

检查共有函数

你必须仔细地检查所有声明为public和external的函数,一个个排除用户滥用它们的可能,谨防安全漏洞。请记住,如果这些函数没有类似onlyOwner这样的函数修饰符,用户能利用各种可能的参数去调用它们。检查完这个函数,用户就可以直接调用这个它。想要防止漏洞,最简单的方法就是设其可见性为internal。

自定义修改符

带参数的函数修饰符

之前我们已经读过一个简单的函数修饰符了: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把参数传递给它的修饰符的。

8.利用 'View' 函数节省Gas

“view”函数不花“gas”

当玩家从外部调用一个view函数,是不需要支付gas的。这是因为view函数不会真正改变区块链上的任何数据——它们只是读取。因此用view标记一个函数,意味着告诉web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。

稍后我们将介绍如何在自己的节点上设置 web3.js。但现在,你关键是要记住,在所能只读的函数上标记上表示只读的“external view”声明,就能为你的玩家减少在DApp中gas用量。

注意:如果一个view函数在另一个函数的内部被调用,而调用函数与view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为view的函数只有同一个合约的外部调用时才是免费的。

9.存储非常昂贵

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

为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑——比如每次调用一个函数,都需要在memory(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。

在大多数编程语言中,遍历大数据集合都是昂贵的。但是在Solidity中,使用一个标记了external view的函数,遍历比storage要便宜太多,因为view函数不会产生任何花销。

在内存中声明数组

在数组后面加上memory关键字, 表明这个数组是仅仅在内存中创建,不需要写入外部存储,并且在函数调用结束时它就解散了。与在程序结束时把数据保存进storage的做法相比,内存运算可以大大节省gas开销——把这数组放在view里用,完全不用花钱。以下是申明一个内存数组的例子:

**

function getArray() external pure returns(uint[]) {
  // 初始化一个长度为3的内存数组
  uint[] memory values = new uint[](3);
  // 赋值
  values.push(1);
  values.push(2);
  values.push(3);
  // 返回数组
  return values;
}

这个小例子展示了一些语法规则,下一章中,我们将通过一个实际用例,展示它和for循环结合的做法。

注意:内存数组必须用长度参数(在本例中为3)创建。目前不支持 array.push()之类的方法调整数组大小,在未来的版本可能会支持长度修改。