目录
合约的不可变性 Immutability
Solidity看起来和java很相似,但是还有很多不一样的地方。其中最突出的一点就是合约的不可变性。一旦在链上部署,合约代码就不可更改,换句话说,合约就是法律。如果有更改的需求,则需要重新部署更换合约地址了。
外部依赖 External dependencies
前篇,我们将CryptoKitties合约地址硬编码到DApp中。但是,如果CryptoKitties合约出现bug,有人摧毁了所有的小猫,会发生什么?
这不太可能,但如果真的发生了,这将使我们的DApp完全无用——我们的DApp将指向一个不再返回任何kitty的硬编码地址。我们的僵尸将无法以小猫为食,我们也无法修改合约来修复它。
出于这个原因,拥有允许您更新DApp关键部分的功能通常是有意义的。
例如,我们不应该将CryptoKitties合约地址硬编码到我们的DApp中,我们可能应该有一个setKittyContractAddress函数,在CryptoKitty合约出现问题时,该函数允许我们在将来更改该地址。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
// 1. Remove this:
// 2. Change this to just a declaration:
KittyInterface kittyContract;
// 3. Add setKittyContractAddress method here
function setKittyContractAddress(address _address) external {
kittyContract = KittyInterface(_address);
}
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
受掌管的合约 Ownable Contracts
前面学的合约不安全,需要有一个合约掌管者owner专门控制合约,这才相对更安全。
OpenZeppelin's Ownable contract
OpenZeppelin是一个安全且经过社区审核的智能合约库,您可以在自己的DApp中使用。强烈建议查看他们的网站以进一步学习!
下面是 取自OpenZeppelin Solidity图书馆的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 private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns(address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @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 {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
构造函数:constructor()是一个构造函数,它是一个可选的特殊函数,与合约同名。它只会在合约首次创建时执行一次。
函数修饰符:修饰符onlyOwner()。修饰符是一种半函数,用于修改其他函数,通常用于在执行之前检查某些需求。在这种情况下,只能使用owner来限制访问,因此只有合约的所有者才能运行此功能。
因此,Ownable Contracts基本上包括以下内容:
1.创建合约时,其构造函数将所有者设置为msg.sender(部署它的人)
2.它添加了一个onlyOwner修饰符,可以将对某些函数的访问限制为只有所有者
3.它允许您将合约转让给新所有者
运用
设置
pragma solidity >=0.5.0 <0.6.0;
// 1. Import here
import "./ownable.sol";
// 2. Inherit here:
contract ZombieFactory is Ownable{
onlyOwner 函数修饰符
我们的合约ZombieFactory继承自Ownable,我们也可以在ZombieFeeding中使用onlyOwner函数修饰符。
// Modify this function:
function setKittyContractAddress(address _address) external onlyOwner{
kittyContract = KittyInterface(_address);
}
燃料 Gas
在Solidity中,执行一个函数时都必须付费。执行一个函数需要多少Gas取决于该函数的逻辑有多复杂。每个操作的Gas成本大致取决于执行该操作需要多少计算资源(例如,写入存储比添加两个整数要昂贵得多)。功能的总燃气成本是其所有单独操作的燃气成本之和。
因为运行函数会为用户带来成本,所以代码优化在以太坊中比在其他编程语言中更重要。
节省Gas
使用uint8而不是uint(uint256)不会节省任何Gas,因为Solidity保留256位存储空间。
但有一个例外:内部结构。
如果在一个结构中有多个uint,则在可能的情况下使用较小大小的uint将允许Solidity将这些变量打包在一起以占用较少的存储空间。例如:
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// `mini` will cost less gas than `normal` because of struct packing
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
出于这个原因,在结构中,尽可能用小的整数子类型。
您还需要将相同的数据类型聚集在一起(即将它们放在结构中彼此相邻的位置),以便Solidity可以最小化所需的存储空间。例如,具有字段uint c的结构;uint32 a;uint32 b;将比具有字段uint32 a的结构消耗更少的气体;uint c;uint32 b;
运用
struct Zombie {
string name;
uint dna;
// Add new data here
uint32 level;
uint32 readyTime;
}
时间单位 Time Units
为了跟踪僵尸需要等待多长时间才能再次攻击,我们可以使用Solidity的时间单位。
我们需要僵尸有随着时间推移而升级的等级,以及有进食或攻击后的冷却期cooldown period。
Solidity提供了一些处理时间的原生单位。
该变量现在将返回最新块的当前unix时间戳(自1970年1月1日以来经过的秒数)。
注意:Unix时间传统上以32位数字存储。这将导致“2038年”问题,届时32位unix时间戳将溢出并破坏许多遗留系统。因此,如果我们希望我们的DApp在20年后继续运行,我们可以使用64位数字,但同时我们的用户将不得不花费更多的精力来使用我们的DApp。
示例:
uint lastUpdated;
// Set `lastUpdated` to `now`
function updateTimestamp() public {
lastUpdated = now;
}
// Will return `true` if 5 minutes have passed since `updateTimestamp` was
// called, `false` if 5 minutes have not passed
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
运用
给DApp添加一个冷却时间,让僵尸在攻击或进食后必须等待1天才能再次攻击。
pragma solidity >=0.5.0 <0.6.0;
import "./ownable.sol";
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
// 1. Define `cooldownTime` here
uint cooldownTime = 1 days;
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
// 2. Update the following line:
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
僵尸的冷却时间
1.对一个僵尸触发冷却的函数
2.查询僵尸冷却时间是否结束的函数
// 1. Define `_triggerCooldown` function here
function _triggerCooldown(Zombie storage _zombie) internal{
_zombie.readyTime = uint32(now + cooldownTime);
}
// 2. Define `_isReady` function here
function _isReady(Zombie storage _zombie) internal view returns(bool){
return (_zombie.readyTime <= now);
}
公共函数与安全性
我们不希望任何人可以喂食他们想要的dna给僵尸,所以要把feed函数public修饰符改为internal。
如果冷却时间未到,停止后续逻辑。如果冷却时间结束,执行dna融合操作后要重新让原僵尸进入冷却。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
// 1. Make this function internal
function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal{
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
// 2. Add a check for `_isReady` here
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
// 3. Call `_triggerCooldown`
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
更多有关函数修饰符
为了后续给僵尸升级后添加特殊功能,我们需要先学习更多一些有关函数修饰符的内容。
带参数的函数修饰符
之前我们看了onlyOwner的简单示例。但是函数修饰符也可以接受参数。
在ZombieHelper中,创建名为aboveLevel的修饰符。它需要两个参数,_level(一个uint)和_zombieId(也是一个uit)。
修饰符体应确保僵尸[_zombieId].level大于或等于_level。
记住让修饰符的最后一行用_;调用函数的其余部分。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
// Start here
modifier aboveLevel(uint _level,uint _zombieId){
require(zombies[_zombieId].level >= _level);
_;
}
}
自定义修饰符的运用
1. 对于僵尸级别2和更高,用户可以更改其名称。
2. 对于20级及更高级别的僵尸,用户将能够为其提供自定义DNA。
创建一个名为changeName的函数。两个参数:_zombieId(一个uint)和_newName(一个数据位置设置为calldata的字符串),并将其设置为external。它应该具有aboveLevel修饰符,并且应该为_level参数传入2。(不要忘记传递_zombieId)。
注意:calldata在某种程度上类似于内存,但它只适用于外部函数。
在这个函数中,首先我们需要使用require语句来判断msg.sender等于zombieToOwner[_zombieId]。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// Start here
function changeName(uint _zombieId,string calldata _newName) external aboveLevel(2, _zombieId){
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId,uint _newDna) external aboveLevel(20, _zombieId){
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
}
用“view” Functions来节省Gas
当用户从外部调用视图view函数时,不需要花费任何Gas。
我们需要一个getZombiesByOwner函数来获取一个用户所有的僵尸,这是一个只读的函数。
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
// Create your function here
function getZombiesByOwner(address _owner) external view returns(uint[] memory){
}
}
存储storage是很昂贵的
Solidity中最昂贵的操作之一是使用存储,尤其是写入。编程时要注意避免低效率的编程逻辑,比如:每次调用函数时都在内存中重建一个数组,而不是简单地将该数组保存在变量中以便快速查找。
在大多数编程语言中,在大型数据集上循环的开销很大。但在Solidity中,如果使用外部视图功能,这比使用存储要便宜得多,因为视图view功能不会花费用户任何Gas。
在内存memory中声明数组
您可以在数组中使用memory关键字在函数中创建一个新数组,而无需向存储storage中写入任何内容。数组只会存在直到函数调用结束,如果是外部调用的视图函数,那么这比在无存储环境中更新数组要便宜得多。
示例:
function getArray() external pure returns(uint[] memory) {
// Instantiate a new array in memory with a length of 3
uint[] memory values = new uint[](3);
// Put some values to it
values[0] = 1;
values[1] = 2;
values[2] = 3;
return values;
}
运用
在getZombiesByOwner函数中,我们希望返回一个uint[]数组,其中包含特定用户拥有的所有僵尸。
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
// Start here
uint[] memory result = new uint[](ownerZombieCount[_owner]);
return result;
}
For循环应用场景
有时需要使用for循环在函数中构建数组的内容,而不是简单地将该数组保存到存储中。
为什么?
对于我们的getZombiesByOwner函数,一个简单的实现是在ZombieFactory合约中存储所有者到僵尸们的映射:
mapping (address => uint[]) public ownerToZombies
然后,每当我们创建一个新的僵尸时,我们只需使用ownerToZombies[owner].push(zombieId)将其添加到所有者的僵尸数组中。getZombiesByOwner会是一个非常简单的函数:
function getZombiesByOwner(address _owner) external view returns (uint[] memory) {
return ownerToZombies[_owner];
}
但是用这种方法存在一些问题:
这种方法很简单,很有吸引力。但是,让我们看看如果我们稍后添加一个函数,将僵尸从一个所有者转移transfer到另一个所有者,会发生什么情况?
该传递transfer函数需要:
step1.将僵尸push新所有者的僵尸数组,
step2.将僵尸从旧所有者的僵尸数组中remove,
step3.将旧主人数组中的每一个僵尸都向上移动一个位置,以填充空隙hole,然后将数组长度减少1。
step3将会很费gas,因为我们必须为每个改变位置的僵尸进行写入操作。如果一个拥有者有20个僵尸并交易掉第一个僵尸,我们将不得不执行19次写入操作来维持阵列的顺序。
注意:当然,我们可以只移动数组中的最后一个僵尸来填充空隙,并将数组长度减少一。但是我们每次交易都会改变僵尸军队的顺序。
由于视图函数在外部调用时不会耗费大量的gas,因此我们可以简单地使用getZombiesByOwner中的for循环来迭代整个僵尸数组,并构建属于该特定所有者的僵尸数组。那么我们的传递函数会便宜得多,因为我们不需要重新排序存储中的任何数组,而且这种方法总体上更便宜。
简而言之,利用view函数配合for循环获取数组(省了gas),相当于从外部重新编排好一个数组再执行写入操作。这样避免了在内部频繁执行写入的操作。
运用
类似于JavaScript。
让我们通过编写一个for循环来完成getZombiesByOwner函数,该循环遍历DApp中的所有僵尸,比较它们的所有者以判断是否匹配,并在return之前将它们push到结果数组。
定义一个计数器赋值为0,for循环中迭代僵尸->所有者的映射来判断该僵尸是否为目标所有人,如果真则记录当前映射索引到结果集中。结果集用单独的计数器作为索引迭代赋值。
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
// Start here
uint counter = 0;
for(uint i=0;i < zombies.length;i++){
if(zombieToOwner[i] == _owner){
result[counter] = i;
counter++;
}
}
return result;
}
回顾一下
我们添加了一种更新CryptoKitties合约的方法
我们已经学会了只使用Owner来保护核心功能
我们了解了Gas和Gas优化
我们为僵尸增加了等级和冷却
我们现在有了当僵尸达到一定等级后更新其名称和DNA的功能
加油,第四篇继续!