이더리움 DApp은 다른 프로그래밍 언어와 비슷하지만, 일반적인 어플리케이션과는 다른 면도 있습니다.
이더리움 컨트랙트들은 한번 배포하면 변경할 수 없습니다. 나중에 수정하거나 업데이트가 절대 불가능하고 영원히 블록체인에 남게 됨. 이로 인해 솔리디티를 기반으로 짜는 것은 상당한 리스크가 있습니다. 만약 contract code에 결함이 있다고 하더라도 나중에 패치할 수 있는 방법이 없고, 유저들에게 다른 contract adrress를 사용해달라고 부탁해야 합니다.
하지만 다르게 생각하면, 코드가 곧 법임을 의미하기도 합니다. 만약 스마트 컨트랙트로 작성된 코드가 있다면, 항상 그 코드가 동일하게 작동할 것임을 보장할 수 있습니다.
lesson 2에서 CryptoKittiy의 contract address를 하드코딩했습니다. 하지만 CryptoKitty의 contract가 나중에 버그가 발생하거나 공격받으면 수정이 불가능합니다. 따라서 DApp에 대해서 수정할 수 있는 구멍을 만들어두는 것이 중요합니다.
DApp에 있는 주소를 하드코딩하는 대신에, 어떠한 문제가 생겼을 때 교체할 수 있도록 주소를 설정해두면 버그에 대응할 수 있습니다.
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
// 외부에서 address를 주입할 수 있도록 함.
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");
}
}
이때 이전 챕터처럼 작성하면 보안문제가 발생할 수 있습니다. setKittyContractAdrress가 external일 때 누구나 해당 함수를 부를 수 있으므로 악성 contract로 교체해버릴 수 있습니다.
예를 들어서 KittyInterface에 맞게 만들었지만 악성인 contract를 만들었다고 가정해봅시다.
contract MaliciousKitty {
function getKitty(uint256 _id) external returns (
bool, bool, bool, bool, bool, bool, bool, bool, bool, uint256
) {
// 얻기 매우 희귀한 DNA를 하드코딩해서 무조건 얻을 수 있도록 한다.
uint very_expensive_dna = 1234...;
return (false, false, false, false, false, false, false, false, false, very_expensive_dna);
}
}
이 MalciousKitty를 선언해두고 호출하면 원래는 얻기 어렵도록 설정된 DNA도 너무 쉽게 얻어버릴 수 있습니다. 또한 배포 후에는 원상복구가 불가능하므로 획득한 희귀 DNA를 없앨 수도 없습니다.
이를 방지하기 위해서는 ZombieFeeding을 배포한 사람만 setKittyContractAddress를 실행할 수 있도록 제한해야 합니다.
OpenZepplin이라는 솔리디티 라이브러리를 사용하면 Ownable Contract를 쉽게 구현할 수 있습니다. OpenZepplin은 DApps에서 보안을 위한 다양한 기능을 지원하니 공식문서를 확인하는 것을 권장합니다.
목표는 해당 컨트랙트의 owner가 누구인지 관리하고, owner만 기능을 쓸 수 있게 제한하는 것이다.
contract Ownable {
address private _owner;
// event가 발생하면 블록체인에 로그가 남음.
// 이때 indexed를 사용하면 주소를 사용하여 필터링을 검색할 수 있으므로
// ownership이 언제 누구에게 넘어갔는지 추적할 수 있음.
event OwnershipTransffered(
address indexed previousOwner,
address indexed newOwner
);
constructor() internal {
// 첫 배포자가 owner가 됨.
_owner = msg.sender;
// 첫 배포자가 등록되면 로그에 기록을 남김
// address(0)은 이전 owner가 없었던, 최초 생성임을 알리는 표현
emit OwnershipTransferred(address(0), _owner);
}
modifier onlyOwner() {
// isOwner를 통과했을 때만 주어진 함수를 수행할 수 있음.
reuire(isOwner());
_; // 실제 함수의 본문이 들어가는 부분. decorator와 비슷한 원리
}
function owner() public view returns(address) {
return _owner;
}
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
// owner 권한을 포기하는 것. 아무도 owner가 아님.
// 그럼 누구도 이 contract을 수정할 수 없게 됨.
function renounceOwnership() public onlyOwner{
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
// owner를 이전하는 것
// 왜 굳이 분리했을까?
// 상속받은 컨트랙트에서 ownership을 이전하는 로직을 재사용할 수 있도록 하기 위함.
// 상속 이후에는 internal 함수만 overriding해도 됨.
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransffered(_owner, newOwner);
_owner = newOwner;
}
}
owner는 이 컨트랙트를 배포한 트랜잭션의 배포자 주소를 의미합니다. 만약 이 주소를 가지지 않은 사람은 setKittyContract를 호출할 수 없습니다.
잘 와닿지 않으면 아래 사례를 봅시다.
정상적으로 작동한다고 하면
만약 공격하게 된다면
만약 개발자가 업데이트를 해야 한다고 하면, setKittyContractAddress에 교체할 새 주소만 넣으면 됩니다. 다른 사람들은 교체할 권한이 없으므로 안전하게 업데이트할 수 있습니다.
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);
}
}
modifier는 데코레이터와 유사합니다. 다른 function에서 붙여서 사용하면 주어진 함수 정의를 변형할 수 있습니다. _; 부분에서 정의한 function이 들어가고, 그 이외의 공통으로 사용되는 로직을 modifier에서 정의할 수 있습니다.
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
);
}
// ZombieFactory가 Ownable이므로
// ZombieFeeding에서도 Ownable의 public, internal method 들을 사용할 수 있음.
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는 DApps을 움직일 수 있도록 하는 연료입니다. Gas는 DApp 상에서의 화폐이고, 어떤 일을 할 때 보상으로 주어집니다. 만약 이더를 사용하여 가스를 구입하면 DApp 안에서의 다양한 function들을 수행할 수 있습니다.
이때 얼마나 많은 gas가 드는지는 function이 얼마나 복잡하냐에 따라 다릅니다. 컴퓨팅 리소스가 많이 들수록 가스비가 올라간다고 생각하면 됩니다. 이때 function이 다른 function들을 호출할 때에는 사용하는 모든 function의 가스비를 합한 금액이 최종적인 가스비가 됩니다.
따라서 코드를 최대한 최적화해야 가스비를 줄일 수 있습니다.
가스비는 탈중앙을 구현하기 위해서는 필수적입니다. 중앙서버였다면 하나의 서버에서만 검증하면 되지만, 이더리움에서는 네트워크에 있는 모든 개별 노드들이 동일 함수에 대한 output을 검증해야 합니다.
이때 무한루프 코드가 발생하거나 너무 많은 연산이 올라가게되면 네트워크 자원이 해당 contract에 독접되는 문제가 생길 수 있습니다. 따라서 연산하는만큼 가스비를 지불하게 하여 위와 같은 독점 문제를 방지하고자 하였습니다.
물론 다른 블록체인에서는 완전히 맞는 말이 아닐 수 있습니다. 구현하기 나름!
가스비를 줄이기 위해서는 struct 안에서 자료형을 잘 활용하면 좋습니다.
일반적으로 uint8, uint16, uint32 등을 개별적으로 선언할 때에는 가스비 차이가 없습니다. uint8이 uint보다 더 적은 메모리를 가지지만 가스비는 차이가 없습니다.
하지만 struct 안에서는 uint8이 uint보다 더 적은 메모리를 가집니다. 따라서 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에서만 가스비가 절약이 될까?
EVM의 스토리지 슬롯 때문입니다.
EVM에서는 스토리지(개별 변수가 저장되는 메모리)가 256비트 단위로 관리됩니다. 이때 value type는 무조건 하나의 슬롯을 사용하기 때문에 자료형을 다르게 선언하더라도 동일한 리소스를 차지하므로 가스비에 차이가 없습니다.
단, struct의 자료형은 여러개의 슬롯을 연속으로 배치해서 사용합니다. 이때 만약 주어진 자료형이 슬롯을 다 채우지 않았다면 같은 슬롯에 다른 값을 같이 집어넣을 수있습니다. 이런 작업을 packing 이라고 합니다.
struct MiniMe {
uint32 a; // 32비트
uint32 b; // 32비트 → a와 같은 슬롯에 packing!
uint c; // 256비트 → 새 슬롯
}
// → 총 2 슬롯
이때 struct 이외에도 packing을 사용할 수 있는 reference type에 대해서는 자료형으로 가스비를 절약할 수도 있습니다. (다 되는 건 아님)
fixed-size array는 packing이 적용되지만 dynamic array나 mappind은 packing되지 않습니다.
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: 쿨다운 시간과 같습니다. 좀비가 항상 공격을 할 수 있는 게 아니라 한번 때러고 나서나 Kitty를 하나 먹고 나서 쿨다운 시간동안은 공격을 할 수 없어야 합니다. 한번에 1000번씩 때릴 수 있다고 하면 게임이 너무 쉬워지니 걸어두는 제약 조건이라고 생각하면 됩니다.
솔리디티에서 지원하는 시간 계산 메서드를 알아봅시다.
now : 가장 최신 블록의 UNIX Timestamp을 알려줍니다. 이때 현재 시각은 정수 값으로 나옵니다.
UNIX Timestamp는 기본적으로 32bit로 저장되어 있는데, 이렇게 되면 오버플로우로 2038년밖에 표현할 수 없습니다. 따라서 64비트로 표현해두는 것이 더 적절합니다.
솔리디티에서는 seconds, minutes, hours, days, weeks, years와 같이 다양한 시간 자료형을 사용할수도 있습니다. 모두 uint 값으로 제공되니 참고하세요.
uint lastUpdated;
function updateTimeStamp() public {
lastUpdated = now;
}
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);
}
}
이제 feedAndMultiply 함수에도 쿨다운을 적용해야 합니다.
이때 좀비의 readyTime을 확인할 수 있는 헬퍼 함수를 사용하면 더 편하게 관리할 수 있습니다.
struct 또한 arugument로 보낼 수 있는데, 이때 struct는 reference type이므로 storage 키워드를 반드시 사용해줘야 합니다.
function _doStuff(Zombie storage _zombie) internal {
// do stuff with _zombie
}
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");
}
}
이제 feedAndMultiply에서도 쿨다운 로직을 적용해봅시다.
이전에 feedAndMultiply를 public으로 설정해 두었습니다. 다만 public과 external 함수는 누구나 호출할 수있기 때문에 이를 오남용할 수도 있습니다.
Ownable에서는 수정을 막기는 했지만, 현재는 호출이나 넘기는 데이터를 관리하는 작업이 필요합니다.
예를 들어서, 게임에서 토끼를 실제로 잡는 행위를 했을 때 feedAndMultiply가 호출될 수 있어야 하는데, 현재처럼 contract에 public으로 선언되어 있으면 게임에서 토끼를 잡는 행위가 없어도 feedAndMultiply를 수행할 수 있을 것입니다.
따라서 모든 함수들을 internal로 관리해서 무분별하게 함수가 호출되는 것을 막을 수있습니다.
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");
}
}
추가적으로 좀비 게임을 즐기기 위해 필요한 헬퍼 메서드를 정의해봅시다.
function modifier는 argument들도 받을 수 있습니다.
mapping (uint => uint) public age;
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
function driveCar(uint _userId) public olderThan(16, _userId) {
// something...
}
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);
_;
}
}
이제 각 레벨에 따라 가능한 스킬을 설정해봅시다.
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;
}
}
전체 zombie가 어떻게 되는지 불러올 수 있는 함수도 필요합니다. 이 함수는 단순 읽기만 수행하기 때문에 view 로 선언할 수 있습니다. view 함수는 유저가 호출했을 때 가스비가 나오지 않습니다. 블록체인에 아무것도 변화를 주지 않고 읽기만 하기 때문입니다. 따라서 트랜잭션도 따로 생성되지 않습니다.
하지만 view가 아닌 함수에 의해 호출되는 경우에는 가스비가 청구됩니다. 기본적으로 트랜잭션 안에서 수행되는 함수에 대해서는 모두 가스비가 청구된다고 생각하면 됩니다. 따라서 view externally 함수만 확정적으로 가스비가 청구되지 않는다고 보장할 수 있습니다.
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가 있습니다. storage 키워드를 사용하면 모든 블록에 기록이 되어야 하므로 많은 가스비가 들 수밖에 없습니다.
이를 줄이기 위해서는 정말 필요한 경우가 아니라면 storage 키워드를 사용하지 않는 것이 필요합니다. 다만 프로그래밍 로직 상으로는 비효율적으로 보일 수는 있습니다. (함수를 호출할 때마다 memory를 새로 만든다던가)
일반적인 프로그래밍 언어에서는 큰 데이터세트에 대해 반복문을 도는 것이 매우 비싼 연산이지만, 솔리디티에서는 storage를 쓰는 것보다 훨씬 쉬운 방법입니다. 차라리 external view를 사용하면 가스비 없이 수행할 수 있으니까요.
솔리디티에서는 memory keyword를 사용해서 일시적으로 사용할 배열을 선언할 수 있습니다. 모든 블록체인에 기록되는 것이 아니기 때문에 가스비가 훨씬 저렵합니다.
function getArray() external pure returns(uint[] memory) {
// 나중에 for loop를 배우면 더 편하게 쓸 수 있음.
uint[] memory values = new uint[](3);
values[0] = 1;
values[1] = 2;
values[2] = 3;
return values;
}
이 사례에서는 zombie-owner map말고 owner-zombie map을 하나 더 storage로 만드는 것 보다 차라리 전레 zombie-owner map을 순회하면서 계산하는 것이 가스비 면에서 이득임을 설명하고 있습니다.
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;
}
}
for 루프를 사용해서 반복문을 사용해봅시다.
zombieToOwner maps 말고 ownerToZombie map을 저장하는 방식을 사용하면 함수 구현은 훨씬 더 간단하게 처리할 수 있을 것입니다. 하지만 가스비가 어마어마하게 많이 사용될 수 있습니다.
이때 소유자가 좀비 20개 중에서 하나를 거래한다고 하면, 슬록 재배열을 위해서 19번의 쓰기 작업을 해야 합니다. 이와 함께 storage write 작업은 매우 많은 비용이 사용되기 때문에 가스 비용이 극도로 비싸질 수 있습니다. 이와 함께 사용자가 보유한 좀비가 몇 개인지 모르기 때문에 사용자가 얼마만큼의 가스를 보내야 할지 알 수도 없습니다.
따라서 별도의 map을 두지 않고 for 루프를 통해 zombieToOwner map을 순회하는 방식을 선택하였습니다. 이 방식을 사용하면 storage의 배열을 재정렬할 필요가 없고 더 저렴합니다.
for loop는 아래와 같이 사용할 수 있습니다.
function getEvens() pure external returns(uint[] memory) {
uint[] memory evens = new int[](5);
uint counter = 0;
for (uint i = 1; i <= 10; i++) {
if (i % 2 == 0) {
evens[counter] = i;
counter ++;
}
}
return evens;
}
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;
}
}
❓근데 그냥 push쓰면 안되나요?
memory 배열은 push를 사용할 수 없음. 선언 시점에 크기가 고정이 되어야 하기 때문