前兩天,透過建立了 ZombieFactory 與 ZombieFeeding
講解關於基礎 Contract 的發佈與其他 Contract 互動。
今天將會透過 Lession 3: Advanced Solidity 更深入去理解一些細節
到目前為止, solidity 語法類似於 javascript 語言。
但是 Contract 與一般的應用程式具有很大的不同。
其中一個不同就是發佈到鏈上的 Contract 程式碼無法更改。
一旦發佈上去,就永遠被紀錄在鏈上。
因此,程式碼必須很小心寫,否則一旦有錯,基本上無法修補。
只能重新發新的 Contract 來進版。
如同上述所說,
之前 CryptoKitty 的 Contract address 被 hard-coded 在 ZombieFeeding 這樣一來。
一旦 CryptoKitty Contract 被下架,只能 ZombieFeeding 的功能將無法作用。
所以這時,就不能使用 hard-coded 的方式,而需要開放一個 function setKittyContractAddress 來開放 Contract address 做修改。
所以 ZombieFeeding 的生命周期就依賴於 CryptoKitty 這個外部 Contract 。
更新如下
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 {
// 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");
}
}
可以注意到上面 setKittyContractAddress function 是 external 代表任何其他 Contract 都可以呼叫這個 function 來對 Contract Address 做修改。
並不是一個很安全的作法。
比較好的作法是只允許自己的 Contract 去修改 address 。
要這樣做就比需使用 Ownable 這個概念。
在 solidity , Ownable 代表可以限定一個 owner 所具有的權限。
以下是一個 OpenZeppelin Solidity Library 所實作 Ownable Contract 。 OpenZeppelin 是一個用來做安全性審查以及社群審查的 Smart Contract Library 。
/**
* @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;
}
}
以下是一些新的概念
規結下來, Ownable contract 做到以下事情:
如下:
pragma solidity >=0.5.0 <0.6.0;
// 1. Import here
import "./ownable.sol";
// 2. Inherit here:
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna)) - 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);
}
}
目前已知到繼承關係如下
ZombieFactory is Ownable
ZombieFeeding is ZombieFactory
所以 ZombieFeeding 也可以使用 Ownable 內部的 public/external 的資源。
這邊就可以直接新增 onlyOwner 到 setKittyContractAddress 後面。
更新如下
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;
// Modify this function:
function setKittyContractAddress(address _address) external onlyOwner {
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");
}
}
Gas 是執行 Smart Contract 所需要消耗的燃料。
這是一種避免 Smart Contract 因為不當操作導致過度消耗 EVM 資源的機制。
在 solidity , 每個使用者想要執行 Smart Contract 都必須要使用一個代幣叫作 gas 。
使用者需要使用 Ether 來購買代幣 gas , 也就是說執行 Smart Contract 是需要花費 Ether。
要花費多少代幣 gas 來執行 Smart Contract 跟 Smart Contract 執行內容的複雜度成正相關。
每一個單一運算具有一個代幣消耗值 gas cost 是根據多少運算資源來執行該步驟而定。
舉例來說: 寫一個要寫入 storage 的邏輯會貴於只運算加兩個數值的邏輯
整個功能的代幣消耗 gas cost 是每一個運算代幣消耗的總和。
Ethereum 可以想像是一個巨大,緩慢,但安全的電腦。
當你執行一個功能,每個在共識網路的單一節點需要去執行相同功能來驗證結果。
Ethereum 的創作者不希望 EVM 因為執行一個無窮回圈,導致整個鏈上的節點都癱瘓,
所以設計出根據使用量來付費的機制。
因為 gas 機制,所以在撰寫 Contract 時
會儘可能減少消耗運算資源
在 solidity ,使用子型別一般來說是沒有太多好處。
對於 uint 來說, 不論使用 uint8, uint16, uint32。 solidity 預設會保留 256 bit 來做儲存。
然後如果是在 struct 內,就會有影響
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);
在 struct 中, solidity 會選擇以儘可能小的方式做資料打包。
並且要儘量讓同型別的變數放在一起,舉例來說:
struct Smaller {
uint c; uint32 a; uint32 b;
}
struct Larger {
uint32 a; uint c; uint32 b;
}
同型別放在一起打包會有可以有比較多壓縮空間。
更新如下:
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;
struct Zombie {
string name;
uint dna;
// Add new data here
uint32 level;
uint32 readyTime;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string memory _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna)) - 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);
}
}
level 這個屬性是為了之後要做戰鬥系統所設計的。
readyTime 屬性是為了讓 feed zombie 速度不要過快的冷卻時間。
為了讓 feed zombie 不要過度執行,
因此需要使用時間單位來紀錄剩下多久時間一個 zombie 可以繼續 feed。
Solidity 預設提供 now 變數來取得最後區塊當下的 unix time(timestamp)以秒為單位。
其他還有 seconds, minutes, hours, days, weeks, years 等等。
這些都會被轉變回 uint 並且以秒為單位做呈獻。
範例如下:
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));
}
更新如下
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);
}
}
為了完成 coolDown 功能
需要再修改 feedAndMultiply 使得
可以傳遞 storage 指標當作一個參數到 private 或 internal 的函數
範例如下:
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
透過這種方式就不需要傳遞一個 id 再去察看對應的 zombie 物件
具體修改步驟如下:
更新 ZombieFeeding 如下
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);
}
// 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);
}
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");
}
}
把 cooldown 機制放入 feedAndMultiply function 內。
首先要思考 public 與 external 的 function 是否可能造成誤用
除了 onlyOwner 可以限定 owner 才能執行外。
為了不讓 feedAndMultiply function 被任意呼叫
因此需要做一些讀取權限的修改。
具體作法如下:
更新 ZombieFeeding 如下
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");
}
}
為了讓 Zombie 具有更多功能
所以要把新功能加入到 zombiehelper.sol 內
並且在 zombiefeeding.sol 內引用
前面學到 onlyOwner 這個 modifier。
然而 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
}
olderThan 這個 modifier 需要帶入兩個參數 age 與 userId
接下來將會利用 level 這個屬性來建立 modifier 來做一些限制
新增如下
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);
_;
}
}
使用 aboveLevel modifier 來建立一個功能
定義 Zombie 有兩個獎勵準則來讓使用者升級 Zombie:
具體作法如下:
更新如下
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;
}
}
新增一個 function 叫作 getZombiesByOwner
這個 function 需要從鏈上讀取資料,所以宣告成 view function。
當 view function 被外部使用者呼叫時,不會花費任何 gas
因為 view function 並不會修改任何鏈上的資料,只會讀取資料。因此,web3.js 看到 view function 時只需要去讀取 local Ethereum 節點,而不需要做任何交易到鏈上。
因此,我們可以透過把一些只讀取資料的功能設定為 external view 來節省 gas 。
更新 ZombieHelper contract 如下
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 時,gas 花費會很昂貴特別是寫入資料時。
為了讓 gas 花費降低儘可能不要使用 storage 來處理資料。
大部份程式語言中,查詢大量資料集的消耗是很昂貴的。
但在 solidity,當在 external view function 查詢大量資料卻比使用 storage 變數便宜,因為 view 只做查詢。
以下使用 memory 變數回傳 array 的範例
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;
}
更新如下
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;
}
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
// Start here
uint[] memory result = new uint[](ownerZombieCount[_owner]);
return result;
}
}
語法如下:
function getEvens() pure external returns(uint[] memory) {
uint[] memory evens = new uint[](5);
// Keep track of the index in the new array:
uint counter = 0;
// Iterate 1 through 10 with a for loop:
for (uint i = 1; i <= 10; i++) {
// If `i` is even...
if (i % 2 == 0) {
// Add it to our array
evens[counter] = i;
// Increment counter to the next empty index in `evens`:
counter++;
}
}
return evens;
}
更新 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;
}
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;
}
}
到此 zombie level 的邏輯就設定完了
下個章節將繼續討倫 Battle System 的互動性。