CryptoZombies学习笔记——Lesson3
第三课就开始深入讲解solidity编程技巧了。
chapter1: 智能合约的不变性。
合约一旦部署到以太坊后,就不可更改了,所以从一方面来说,智能合约代码的安全性是如此重要,因为一旦发现你的代码里存在漏洞,那么你只能放弃这个合约,告诉你的用户去另外一个新地址调用修复过的新合约。那么另一方面,如果你想调用某一个智能合约,尽管放心,它永远不会失效或者给出意料之外的结果。
所以当我们调用了一个不再更新的合约地址时,该怎么办呢?solidity有一个解决办法,调用接口时不会直接声明合约的地址,而是用一个设置地址的函数代替,方便我们在未来修改地址。
原调用方式:
address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d;
KittyInterface kittyContract = KittyInterface(ckAddress);
新调用方式:
KittyInterface kittyContract; function setKittyContractAddress(address _address) external{ kittyContract = KittyInterface(_address); }
chapter2:Ownable Contracts
上述代码中,setKittyContractAddress是external的,意味着任何一个外部用户都可以更改调用地址,所以存在安全漏洞,所以定义了一个合约的Ownable,即拥有合约者有调用该函数的特权。推荐了OpenZeppilin库,是一个主打安全和社区审查的智能合约库,Ownable合约即来自其中。可以去官网学习更多。
这章里又涉及了几个新的概念:
构造函数:函数名同合约名,在合约开始时自动调用,相当于一个初始化函数。
函数修饰符:modifier functionName();在函数执行前检查约束条件。下一章节会详述更多细节。
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; } }
使用时把代码复制到Ownable.sol然后继承即可。
chapter3:函数修饰符
继承可连续继承,modifier不可和function一样直接调用,调用方法见上文onlyOwner例。调用函数时先执行修饰符函数,当执行到modifier里的_;时,跳出继续执行原函数。
**当然这也带来一个安全隐患,开发者可以给智能合约留下一个只有自己可以操作的后门。导致以太坊上Dapp并不完全是绝对的去中心化的,所以一定要阅读并理解代码,防止恶意植入后门。作为开发者,既留下修改bug的权限,又能让用户完全放心,是一个微妙的平衡,也许以后会有新技术出现完全杜绝这种情况发生。
chapter4:gas
相当于执行函数时需要的能源。执行一次语句消耗多少gas取决于语句的复杂度,即需要多少计算资源去完成这段语句,比如写入storage比两个整数相加消耗的gas多得多。所以,Dapp的代码优化相当重要。
为什么要使用gas?这段写的特别好,摘抄下来:
以太坊相当于一个巨大、缓慢、又极度安全的计算机,每当有一个函数运行时,以太坊中每一个节点都会运行该函数以验证结果。以此保证以太坊的去中心化、数据不可篡改和抗监视。为了防止有人通过死循环阻塞网络,或者非常密集的运算大量占用网络资源,以太坊让这些交易需要付出代价,
但是侧链不一定需要这样,所以一些极为复杂的应用可以应用在使用不同共识算法的侧链上。
用更小的存储单位,比如uint换成uint32不会减少gas消耗,但有一个例外:在struct里使用,solidity会打包存储消耗更少的storage,从而消耗较少gas。同时将相同类型变量聚在一起,比如uint a,uint 32 b, uint 32 c会比uint32 b, uint a, uint32 c消耗的存储空间小。
chapter5:time units
变量now返回值是当前最后区块的时间戳(从1970.1.1到现在的秒数),是一个256位uint。Unix时间变量基本都是存储在32位整数变量里的,所以如果想要你的dapp运行时间超过这个存储范围,需要改为64或更高位存储。
solidity还提供了例如seconds,minutes,hours,days,weeks等时间常量,都是换算成秒的整数。
chapter6:结构体可以以存储指针形式作为参数传入internal或private函数。遵循这种语法:
function _doStuff(Zombie storage _zombie) internal { // do stuff with _zombie }
chapter7:Public函数和安全
检视public且没有加modifier的函数,看是否存在被其他用户滥用所导致的安全漏洞。
chapter8:更多关于函数修饰符
modifier作为函数修饰符时也可以传递参数,格式和函数调用一样。
// A mapping to store a user's age: mapping (uint => uint) public age; // Modifier that requires this user to be older than a certain age: modifier olderThan(uint _age, uint _userId) { require(age[_userId] >= _age); _; } // Must be older than 16 to drive a car (in the US, at least). // We can call the `olderThan` modifier with arguments like so: function driveCar(uint _userId) public olderThan(16, _userId) { // Some function logic }
chapter9:利用modifier完成一些新功能
chapter10:利用view减少gas消耗
view修饰的函数调用时不消耗gas,因为调用view前缀函数并不会对区块链造成任何更改,一个注意点,只有当外部调用时才不消耗gas,被内部非view函数调用还是会消耗gas。
chapter11:storage存储是昂贵的
尤其是写入。可以用memory数组代替,在当前版本里,memory数组必须在定义时声明数组大小。
chapter12:for循环
在需要修改storage的时候,有时候for循环遍历view函数并储存可以减少消耗gas。语法类似js。