继续前面的学习。感兴趣的同学,可以参考HiBlock社区有个Solidity 教程进行学习。
第三章 高级 Solidity 理论
把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。如果你的智能协议有任何漏洞,即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议,然后转移到一个新的修复后的合约上。
[注] 这对程序员来说很艰难,因为程序一般都有有bug,而且有的工作就是来处理bug。这种不容易更新的特性确认对程序员不友好。
对于合约地址的使用,应当使用函数,而不是直接使用合约地址这种“硬编码”。
OpenZeppelin库的Ownable 合约
OpenZeppelin 是主打安保和社区审查的智能合约库,您可以在自己的 DApps中引用。
构造函数:构造函数不是必须的,它与合约同名,构造函数一生中唯一的一次执行,就是在合约最初被创建的时候。
函数修饰符:修饰符跟函数很类似,不过是用来修饰其他已有函数用的, 在其他语句执行前,为它检查下先验条件。
Ownable 合约:
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
// 将其 owner 设置为msg.sender(其部署者)
function Ownable() public {
owner = msg.sender;
}
// 修饰符 onlyOwner,它会限制陌生人的访问,将访问某些函数的权限锁定在 owner 上。
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
// 允许将合约所有权转让给他人。
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
onlyOwner 函数修饰符是这么用的:
contract MyContract is Ownable {
event LaughManiacally(string laughter);
//注意! `onlyOwner`上场 :
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
注: likeABoss 函数上的 onlyOwner 修饰符。 当你调用 likeABoss 时,首先执行 onlyOwner 中的代码, 执行到 onlyOwner 中的 _; 语句时,程序再返回并执行 likeABoss 中的代码。
一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。
省 gas 的招数:结构封装 (Struct packing)
如果一个 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);
[注] 这和C语言使用结构体来节省内存相同。不确定solidity语言是否也有数据对齐,猜测应该是有的。因为有如下的说明:通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。只有在结构体中这种方法才能节省空间。
时间单位
变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。
Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
[问题] 这两个函数的形参为什么要用storage?尤其是_isReady函数,只是读取数据。
带参数的函数修饰符
例子如下:
// 存储用户年龄的映射
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) {
// 其余的程序逻辑
}
“view” 函数不花 “gas”
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
存储非常昂贵
Solidity 使用storage(存储)是相当昂贵的,”写入“操作尤其贵。这是因为,无论是写入还是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长,拷贝份数更多,存储量也就越大。这是需要成本的!
注意:内存数组 必须 用长度参数(在本例中为3)创建。目前不支持 array.push()之类的方法调整数组大小,在未来的版本可能会支持长度修改。
第四章:僵尸作战系统
payable 修饰符
payable 它们是一种可以接收以太的特殊函数。
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}
在这里,msg.value 是一种可以查看向合约发送了多少以太的方法,另外 ether 是一个內建单元。
一些人会从 web3.js 调用这个函数 (从DApp的前端), 像这样 :
// 假设 `OnlineStore` 在以太坊上指向你的合约:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意: 如果一个函数没标记为payable, 而你尝试利用上面的方法发送以太,函数将拒绝你的事务。
提现
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
transfer的调用者,上面的例子是owner,是收款者。this.balance为该合约的所有资金。
生成一些(伪)随机数:
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
总结
这里的支付函数比较重要,这是区别于其他函数的地方。但是在实际的使用中,还是不确定如何实现。还需要查看一下实际的程序中web3是如何调用的。