CryptoZombie: Advanced Solidity Concepts
try it yourself
pragma solidity ^0.4.25;
import "./ownable.sol";
// ownable 상속
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
// 쿨 타임 추가
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 _name, uint _dna) internal {
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 _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
pragma solidity ^0.4.25;
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 feedAndMultiply(uint _zombieId, uint _targetDna, string _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");
}
}
pragma solidity ^0.4.25;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
//좀비가 특정 레벨이 되야 함수를 실행할 수 있도록 제한을 건다
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 좀비의 이름을 바꾸는 함수, 레벨 2 이상, 반드시 좀비 주인이어야 함
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
// 좀비의 유전자를 바꾸는 함수, 레벨 20 이상, 반드시 좀비 주인이어야 함
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[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
Once a contract is deployed to Ethereum, it cannot be changed. It is permanent. This makes external dependencies risky. As with our code, what if kitty contract has a fault? Therefore it would make sense for us to have some ability to change codes. In this case setKittyContractAddress
function would be helpful.
KittyInterface kittyContract;
function setKittyContractAddress(address _address) external {
kittyContract = KittyInterface(_address);
}
Previous example has a huge security flaw. External means anybody can access it. That's why ownable contracts
exist. It allows the owner(me) privileges. It can be accessed via OpenZeppelin library.
Also mentioned in this chapter are keywords constructor()
and indexed
. Latter will be explained later.
So the Ownable contract basically does the following:
1. When a contract is created, its constructor sets the owner to msg.sender (the person who deployed it)
2. It adds an onlyOwner modifier, which can restrict access to certain functions to only the owner
3. It allows you to transfer the contract to a new owner
_; is needed at the end of modifier. It means the rest of the function goes here!
ownable.sol:
pragma solidity >=0.5.0 <0.6.0;
/**
* @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;
}
}
onlyOwner is such a common requirement for contracts that most Solidity DApps start with a copy/paste of this Ownable contract, and then their first contract inherits from it.
Now add the onlyOwner modifier to the method.
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
Gas
: Fuel needed to run functions in solidity. Users can buy gas with Ether. The price varies depending on the complexity and the amount of computing power required of the logic. It is needed to verify the execution among many computers in the decentralized Ethereum.
uint8, uint16, uint32, etc.
: They didn't really matter because Solidity reserves 256 bits anyway. But in structs, the size is reflected. Exploit this fact to pack variables and reduce storage(=> less gas required!). To maximize the benefit, cluster smaller ones together(recall system programming lecture).
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
}
uint32 is grouped together to save memory space.
readyTime
represents cool time zombies have until they can feed again, which is 1 day.
Time Units
are native units that Solidity provides to deal with time. now
returns the current unix timestamp in uint256. seconds, minutes, hours, days, weeks and years
each represents the respective time their names indicate.
Passing structs
as arguments: instead of memory
, use storage
. This is possible in internal or private functions.
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
function _isReady(Zombie storage _zombie) internal view returns(bool){
return (_zombie.readyTime <= now);
}
Always check public and external functions and think of ways the user might exploit it. Unless it is necessary try swtching it to private or internal.
We've looked at function modifiers like onlyOwner. Let's take a look at how modifiers can also take in arguments.
We will make a new file, zombiehelper.sol.
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);
_;
}
}
- For zombies level 2 and higher, users will be able to change their name.
- For zombies level 20 and higher, users will be able to give them custom DNA.
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 don't cost any gas when they're called externally by a user(it could cost gas if called internally by another function. Same goes for pure).
It only reads from existing blockchain and no transaction is needed to be recorded. Therefore no gas is needed!
We will make a function that the user can use to view their entire zombie army.
One of the more expensive operations in Solidity is using storage — particularly writes. This is because every time you write or change a piece of data, it’s written permanently to the blockchain.
Using storage
should be avoided unless absolutely necessary. This can lead to seemingly ineffiecient codes, but considering gas is real money, this is the norm for Solidity. Try using memory
more.
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
// fill
return result;
}
Sometimes you'll want to use a for loop to build the contents of an array in a function rather than simply saving that array to storage.
For our getZombiesByOwner
function, naive approach would be the following.
In ZombieFactory:
mapping (address => uint[]) public ownerToZombies
Back to getZombiesByOwner
:
function getZombiesByOwner(address _owner) external view returns (uint[] memory) {
return ownerToZombies[_owner];
}
This may seem fine for now, but using storage will cause price issues later. For example, we might implement a function where the zombie owner changes. Implementing with a system like above will waste tons of gas!
Since view functions don't cost gas when called externally, we can simply use a for-loop in getZombiesByOwner to iterate the entire zombies array and build an array of the zombies that belong to this specific owner.
Cheeper logic is counter intuitive in Solidity's case!
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for(uint i = 0; i < zombies.length; i++){
if(zombieToOwner[i] == _owner){
result[counter] = i;
counter++;
}
}
return result;
}