해당 글은 CrytoZombie를 공부하면서 정리한 내용입니다.
= OpenZeppelin 솔리디티 라이브러리에서 가져온 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 public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @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 {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
onlyOwner는 컨트랙트에서 흔히 쓰는 것 중 하나라, 대부분의 솔리디티 DApp들은 Ownable 컨트랙트를 복사/붙여넣기 하면서 시작한다. 그리고 첫 컨트랙트는 이 컨트랙트를 상속해서 만든다.
ZombieFeeding is ZombieFactory
ZombieFactory is Ownable
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
contract MyContract is Ownable {
event LaughManiacally(string laughter);
// 아래 `onlyOwner`의 사용 방법을 잘 보게:
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
*참고: 사이드체인에서는 반드시 이렇지는 않다. 이더리움 메인넷에서 월드 오브 워크래프트 같은 게임을 직접적으로 돌리는 것은 가스 비용이 엄청나게 높을 것이기 때문에 절대 하면 안된다. 하지만 다른 합의 알고리즘을 가진 사이드체인에서는 가능할 수 있다.
기본적으로는 하위 타입(uint8, uint16, uint32 등)들을 쓰는 것은 아무런 이득이 없다. 왜냐하면 솔리디티에서는 uint의 크기에 상관없이 256비트의 저장 공간을 미리 잡아놓기 때문이다. 예를 들자면, uint(uint256) 대신에 uint8을 쓰는 것은 가스 소모를 줄이는 데에 아무 영향이 없다.
하지만 여기에 예외가 하나 있지. 바로 struct의 안에서 이다.
만약 구조체 안에 여러 개의 uint를 만든다면, 가능한 더 작은 크기의 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);
uint c; uint32 a; uint32 b;
의 가스비 <uint32 a; uint c; uint32 b;
의 가스비솔리디티는 시간을 다룰 수 있는 단위계를 기본적으로 제공한다.
now 변수를 쓰면 현재의 유닉스 타임스탬프(1970년 1월 1일부터 지금까지의 초 단위 합)값을 얻을 수 있다.
예 >유닉스 타임의 값 = 1515527488
*참고: 유닉스 타임은 전통적으로 32비트 숫자로 저장된다. 이는 유닉스 타임스탬프 값이 32비트로 표시가 되지 않을 만큼 커졌을 때 많은 구형 시스템에 문제가 발생할 "Year 2038" 문제를 일으킬 수 있다. 그러니 만약 우리 DApp이 지금부터 20년 이상 운영되길 원한다면, 우리는 64비트 숫자를 써야 할 것이다. 하지만 우리 유저들은 그동안 더 많은 가스를 소모해야 된다. 그러므로 설계를 보고 결정을 해야 한다.
솔리디티는 또한 seconds, minutes, hours, days, weeks, years 같은 시간 단위 또한 포함하고 있다. 이들은 그에 해당하는 길이 만큼의 초 단위 uint 숫자로 변환된다. 즉 1 minutes는 60, 1 hours는 3600(60초 x 60 분), 1 days는 86400(24시간 x 60분 x 60초) 같이 변환된다.
uint lastUpdated;
// `lastUpdated`를 `now`로 설정
function updateTimestamp() public {
lastUpdated = now;
}
// 마지막으로 `updateTimestamp`가 호출된 뒤 5분이 지났으면 `true`를, 5분이 아직 지나지 않았으면 `false`를 반환
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
private 또는 internal 함수에 인수로서 구조체의 storage 포인터를 전달할 수 있다. 예를 들어 함수들 간에 우리의 Zombie 구조체를 주고받을 때 유용하다.
function _doStuff(Zombie storage _zombie) internal {
// _zombie로 할 수 있는 것들을 처리
}
// 사용자의 나이를 저장하기 위한 매핑
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 함수가 동일 컨트랙트 내에 있는, view 함수가 아닌 다른 함수에서 내부적으로 호출될 경우, 여전히 가스를 소모할 것이다. 이것은 다른 함수가 이더리움에 트랜잭션을 생성하고, 이는 모든 개별 노드에서 검증되어야 하기 때문이다. 그러니 view 함수는 외부에서 호출됐을 때에만 무료이다.
솔리디티에서 더 비싼 연산 중 하나는 storage 이다. 그중에서도 쓰기 연산이 비싸다.
이건 데이터의 일부를 쓰거나 바꿀 때마다, 블록체인에 영구적으로 기록되기 때문이다. 수천 개의 노드들이 그들의 하드 드라이브에 그 데이터를 저장해야 하고, 블록체인이 커져가면서 이 데이터의 양 또한 같이 커진다. 그러니 이 연산에는 비용이 든다.
비용을 최소화하기 위해서, 진짜 필요한 경우가 아니면 storage에 데이터를 쓰지 않는 것이 좋다. 이를 위해 때때로는 겉보기에 비효율적으로 보이는 프로그래밍 구성을 할 필요가 있다.(어떤 배열에서 내용을 빠르게 찾기 위해, 단순히 변수에 저장하는 것 대신 함수가 호출될 때마다 배열을 memory에 다시 만드는 것)
대부분의 프로그래밍 언어에서는, 큰 데이터 집합의 개별 데이터에 모두 접근하는 것은 비용이 비싸다. 하지만 솔리디티에서는 그 접근이 external view 함수라면 storage를 사용하는 것보다 더 저렴한 방법하다. view 함수는 사용자들의 가스를 소모하지 않기 때문이다.
Storage에 아무것도 쓰지 않고도 함수 안에 새로운 배열을 만들려면 배열에 memory 키워드를 쓰면 된다. 이 배열은 함수가 끝날 때까지만 존재한다. 이는 storage의 배열을 직접 업데이트하는 것보다 가스 소모 측면에서 훨씬 저렴하다.
= 메모리에 배열을 선언하는 방법
function getArray() external pure returns(uint[]) {
// 메모리에 길이 3의 새로운 배열을 생성한다.
uint[] memory values = new uint[](3);
// 여기에 특정한 값들을 넣는다.
values.push(1);
values.push(2);
values.push(3);
// 해당 배열을 반환한다.
return values;
}
*참고: 메모리 배열은 반드시 길이 인수와 함께 생성되어야 한다. 메모리 배열은 현재로서는 storage 배열처럼 array.push()로 크기가 조절되지는 않는다. 이후 버전의 솔리디티에서는 변경될 수도 있다.
getZombiesByOwner를 구현할 때, 기초적인 구현 방법은 ZombieFactory 컨트랙트에서 소유자의 좀비 군대에 대한 mapping을 만들어 저장하는 것이다.
새로운 좀비를 만들 때마다, 해당 소유자의 좀비 배열에 ownerToZombies[owner].push(zombieId)
를 사용해서 새 좀비를 추가한다.
// mapping
mapping (address => uint[]) public ownerToZombies
//
function getZombiesByOwner(address _owner) external view returns (uint[]) {
return ownerToZombies[_owner];
}
위의 방법의 문제점
만약 나중에 한 좀비를 원래 소유자에서 다른 사람에게 전달하는 함수를 구현하게 된다면 아래의 과정을 필요하다.
3번째 단계는 극단적으로 가스 소모가 많이 든다. 왜냐하면 위치를 바꾼 모든 좀비에 대해 쓰기 연산을 해야 하기 때문이다. 소유자가 20마리의 좀비를 가지고 있고 첫 번째 좀비를 거래한다면, 배열의 순서를 유지하기 위해 우린 19번의 쓰기를 해야 할 것이다.
솔리디티에서 storage에 쓰는 것은 가장 비용이 높은 연산 중 하나이기 때문에, 이 전달 함수에 대한 모든 호출은 가스 측면에서 굉장히 비싸게 된다. 이 함수가 실행될 때마다 다른 양의 가스를 소모된다는건 더 최악이다. 사용자가 자신의 군대에 얼마나 많은 좀비를 가지고 있는지, 또 거래되는 좀비의 인덱스에 따라 달라지된다. 즉 사용자들은 거래에 가스를 얼마나 쓰게 될지 알 수 없게 된다.
*참고: 물론, 빈 자리를 채우기 위해 마지막 좀비를 움직인 다음, 배열의 길이를 하나 줄여도 된다. 하지만 그렇게 하면 교환이 일어날 때마다 좀비 군대의 순서가 바뀌게 된다.
view 함수는 외부에서 호출될 때 가스를 사용하지 않기 때문에, 우린 getZombiesByOwner 함수에서 for 반복문을 사용해서 좀비 배열의 모든 요소에 접근한 후 특정 사용자의 좀비들로 구성된 배열을 만들 수 있다. 그러고 나면 transfer 함수는 훨씬 비용을 적게 쓰게 된다.
= 솔리디티 for문 사용
function getEvens() pure external returns(uint[]) {
uint[] memory evens = new uint[](5);
// 새로운 배열의 인덱스를 추적하는 변수
uint counter = 0;
// for 반복문에서 1부터 10까지 반복함
for (uint i = 1; i <= 10; i++) {
// `i`가 짝수라면...
if (i % 2 == 0) {
// 배열에 i를 추가함
evens[counter] = i;
// `evens`의 다음 빈 인덱스 값으로 counter를 증가시킴
counter++;
}
}
return evens;
= 전체 소스 (zombiehelper.sol)
pragma solidity ^0.4.19;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function changeName(uint _zombieId, string _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[]) {
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;
}
}