KoreanFoodie's Study
Solidity 튜토리얼 #3 : 심화 문법 본문
크립토 좀비에서 제공하는 튜토리얼을 통해 배우는 Solidity 문법을 정리하고 있습니다!
Lesseon 3 에서는 가스 소모와 코드 효율성 등에 대해 공부할 수 있었다.
기존에 zombiefactory.sol 과 zombiefeeding.sol 로 나누어진 코드에 zombiehelper.sol 과 ownable.sol 을 추가했다.
이번 장을 정리한 내용은 다음과 같다.
1. Immutability of Contracts
이더리움에 컨트랙트를 배포하면, 해당 컨트랙트는 불변(immutable) 이 된다. 따라서, contract 에 문제가 생기면 이를 수정하는 것이 아니라 다른 smart contract 의 주소를 유저에게 제공하는 식으로 패치를 해야 한다.
2. Ownable Contracts
만약 어떤 함수를 사용하는데, 주체가 해당 contract 의 소유자만 사용할 수 있게 하려면 어떻게 코드를 짜야 할까?
간단하게 생각하면 함수 내부에 require 한 줄을 넣는 방법도 있겠지만, modifier 라고 불리는 일종의 half-function 을 사용하면 범용적이며 더 간단하게 구현할 수 있다.
실제로 많은 contract 들이 Ownable 이라는, onlyOwner 라는 modifier(함수 호출 시 contract 소유자만 호출할 수 있도록 함) 를 제공하는 contract 를 상속하기도 한다. 해당 Ownable contract 는 OpenZeppelin Solidity Library 에서 제공하고 있다.
Ownable contract sms 다음과 같은 3 가지 기능을 제공한다.
- 생성자가 owner 를 msg.sender 로 설정
- owner 만 사용할 수 있는 onlyOwner modifier 추가
- 새로운 owner 에게 contract 를 transfer 할 수 있게 해줌
이제 코드를 보자.
/**
* @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;
}
}
여기서 modifier onlyOwner() 을 보면, require 구문과 _; 가 있다. "_;" 는 해당 modifier 가 쓰인 함수에서, modifier 에 정의된 구현을 실행 한 후, 실제 해당 함수를 코드를 동작시키라는 뜻이다. 아래는 해당 modifier 를 적용한 예시이다.
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
3. Gas
이더리움 네트워크를 이용해 연산을 수행하려면 gas 라는 비용을 지불해야 한다. 블록체인 네트워크의 storage 에 데이터를 쓰는 것은 큰 비용을 필요로 하는데, 타입을 적절히 설정해서 이 비용을 줄여볼 수 있다. 예시를 보자.
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);
4. Time Units
"now" 라는 변수는 current unix timestamp of the latest block (since January 1st 1970) 을 리턴한다. 그리고 Solidity 는 이를 변환한 seconds, minutes, hours, days, weeks, years 라는 시간 관련 변수를 제공하고 있다.
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));
}
5. More on Function Modifiers
Function Modifiers 는 매개 변수를 가질 수 있다.
// 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
}
6. callData
원래 함수가 호출될 때 매개변수가 memory 에 저장된다고 했었다. 그런데 external 함수에 있어서는 memory 대신 callData 에 저장된다.
function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
7. Saving Gas With 'View' Functions, because Storage is Expensive!
이전에도 이야기 했지만, 이더리움 네트워크에 값을 쓰거나 변경하는 행위(storage 를 건드리는 행위) 는 매우 큰 gas cost 를 유발한다. 따라서 최대한 gas cost 를 줄일 수 있도록 contract 를 구현해야 한다.
그런데 view 함수는 블록체인 내의 데이터를 수정하지 않으므로, gas cost 가 들지 않는다(web.js 가 local Ethereum node 를 실행). 따라서 사용자가 external view 함수를 사용하도록 구현할 것을 권장한다. 물론 gas 를 지불하는 또 다른 함수 내부에서 호출되는 view 함수는 gas 를 지불한다.
storage 수정 비용이 비싸다 보니, C++ 같은 언어에서는 배열을 리턴하는 함수가 있을 때 기존 데이터를 읽어 참조자로 전달하겠지만, Solidity 에서는 배열을 임시 객체로 새로 생성하는 것이 더 싸게 먹힌다. 다음과 같이 말이다.
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;
}
이제 완성된 코드를 보면서 Wrap-Up 을 하자!
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);
_;
}
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
return result;
}
}
zombiefeeding.sol
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 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);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
zombiefactory.sol
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;
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 {
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);
}
}
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;
}
}
'Tutorials > Solidity' 카테고리의 다른 글
Solidity 튜토리얼 #6 : Web3.js 와 이더리움 연동하기 (5) | 2022.09.23 |
---|---|
Solidity 튜토리얼 #5 : ERC721 과 SafeMath (0) | 2022.09.22 |
Solidity 튜토리얼 #4 : 이더리움 전송 및 payable 함수 (0) | 2022.09.22 |
Solidity 튜토리얼 #2 : 기본 문법 추가 (0) | 2022.09.21 |
Solidity 튜토리얼 #1 : 기본 문법 (0) | 2022.09.20 |