컨트랙트의 불변성
- 이더리움 컨트랙트를 배포하고 나면 변하지 않음 (Immutable)
- 컨트랙트로 배포한 최초의 코드는 블록체인에 영구적으로 존재
- 수정하거나 업데이트 할 수 없음
외부 의존성
- 대부분 DApp의 중요한 일부를 수정할 수 있도록 하는 함수를 만들어 두는 것이 합리적
소유 가능한 컨트랙트
컨트랙트를 소유 가능하게 함 → 컨트랙트를 대상으로 특별한 권리를 가지는 소유자가 있음
Ownable Contract
- OpenZeppelin 솔리디티 라이브러리에 있음
생성자( Constructor )
- 컨트랙트와 동일한 이름을 가진 생략 가능한 함수
- 컨트랙트가 실행될 때 딱 한번만 실행됨
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}
onlyOwner 함수 제어자
- 특정 함수에 소유자만 접근 가능하도록 해줌
- 컨트랙트에서 흔히 쓰는 것 중 하나
- 대부분의 솔리디티 DApp들은 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);
function likeABoss() external onlyOwner {
LaughManiacally("Muahahahaha");
}
}
- likeABoss()를 호출하면 onlyOwner()가 먼저 실행
- onlyOwner() 실행 중 _; 를 만나면 likeABoss()로 돌아가 코드 실행
- onlyOwner의 경우에는, 함수에 이 제어자를 추가하면 오직 컨트랙트의 소유자 (내가 배포했다면 나)만이 해당 함수를 호출가능
기본 컨트랙트인 ZombieFactory가 Ownable을 상속하고 있으니, 우리는 onlyOwner 함수 제어자를 ZombieFeeding에서도 사용할 수 있음
ZombieFeeding is ZombieFactory
ZombieFactory is Ownable
함수 제어자 (Function Modifier)
- 다른 함수들에 대한 접근을 제어하기 위해 사용되는 유사 함수
- 보통 함수 실행 전 요구사항 충족 여부를 확인하는데 사용
- 함수 정의부 끝에 해당 함수의 작동 방식을 바꾸도록 제어자의 이름을 붙일 수 있음
gas
- 이더리움 DApp이 사용하는 연료
- 이더리움에서는 사용자들이 만들어진 DApp을 사용할 때마다 가스라고 불리는 화폐를 지불해야함
함수 로직에 따라 필요한 가스가 달라짐
⇒ 각 연산은 소모되는 가스 비용(gas cost)이 있고 그 연산을 수행하는데 소모되는 컴퓨팅 자원의 양이 비용을 결정함
⇒ 함수를 실행하는 것은 실제 돈을 쓰게 하는 것이라 코드 최적화 아주 중요
가스가 필요한 이유
이더리움을 만든 사람들은 누군가가 무한 반복문을 써 네트워크를 방해하거나 자원소모가 큰 연산을 써 네트워크 자원을 모두 사용하지 못하도록 만들길 원함
⇒ 그래서 연산처리에 비용이 들도록 만듦
⇒ 사용자들은 저장공간 뿐만 아니라 연산 사용시간에 따라서도 비용을 지불함
가스를 아끼기 위한 구조체 압축
uint8 등의 하위 타입을 쓰는건 득이 없음
⇒ 솔리디티에선 uint의 크기에 상관없이 256비트의 저장공간을 미리 잡아기때문
⇒ 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 필드가 묶여있어서
uint32 a; uint c; uint32 b;
시간 단위 (Time units)
- 시간을 다룰 수 있는 단위계를 기본적으로 제공
- now 변수를 쓰면 현재의 유닉스 타임 스탬프 값을 얻을 수 있음
+ 유닉스 타임 스탬프 : 1970년 1월 1일부터 지금까지의 초 단위 합
- seconds, minutes, hours, days, weeks, years 같은 시간 단위를 포함하고 있음
+ 이들은 그에 해당하는 길이 만큼의 초 단위 uint 숫자로 변환됨
+ 1분 = 60 / 1시간 = 3600(60*60) / 1일 = 86400(24*60*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 포인터를 전달할 수 있음
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에 데이터를 쓰지 않는게 좋음
⇒ 이를 위해 때때로 비효율적으로 보이는 프로그래밍을 구성하기도 함
대부분의 프로그래밍 언어에서 큰 데이터 집합의 개별 데이터에 모두 접근하는 것은 비용이 비쌈
그러나 솔리디티는 그 접근이 external view 함수라면 storage를 사용하는 것보다 저렴함
메모리에 배열 선언
- 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;
}
for 반복문
- JS 문법과 비슷함
// 짝수로 구성된 배열을 만드는 예시
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;
}
전체 코드
- zombiefactory.sol
pragma solidity ^0.4.19;
import "./ownable.sol";
// Ownable 상속
contract ZombieFactory is Ownable {
// event 선언
event NewZombie(uint zombieId , string name, uint dna);
// dnaModulus라는 uint형 변수를 생성하고 10의 dnaDigits승을 배정
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
// 재사용 대기시간 : 1일이 지나야 다시 사용 가능
uint cooldownTime = 1 days;
// Zombie 라는 구조체 생성
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime; // 재사용 대기시간
}
// Zombie 구조체의 public 배열 생성
Zombie[] public zombies;
// 매핑 생성
// 좀비 id로 좀비를 저장하고 검색
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
// 좀비 생성 private 함수 선언
// 함수 접근 제어자를 private에서 internal로 변경
function _createZombie(string _name, uint _dna) internal {
// 새로운 좀비를 zombies 배열에 추가
// 배열의 첫 원소가 0이라는 인덱스를 갖기 때문에, array.push() - 1은 막 추가된 좀비의 인덱스가 됨
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime))) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
// 새로운 좀비가 배열에 추가되면 이벤트 실행
NewZombie(id, _name, _dna);
}
// uint를 반환하는 view 함수 선언
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
// 좀비 DNA가 16자리 숫자이기만을 원하므로 연산 후 값 반환
return rand % dnaModulus;
}
// public 함수 선언
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
// 좀비 dna 생성
uint randDna = _generateRandomDna(_name);
// 새로운 좀비 배열에 추가
_createZombie(_name, randDna);
}
}
- zombiefeeding.sol
pragma solidity ^0.4.19;
// zombiefactory.sol 불러오기
import "./zombiefactory.sol";
// KittyInterface라는 인터페이스를 정의
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 상속
contract ZombieFeeding is ZombieFactory {
// 크립토키티 컨트랙트 주소의 업데이트가 가능하도록 수정
KittyInterface kittyContract;
// 소유자만 수정할 수 있도록 수정
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
// 1일을 초단위로 바꾼것의 합과 같음
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
// 좀비가 먹이를 먹은 후 충분한 시간이 지났는지 판단해서 알려줌
function _isReady(Zombie storage _zombie) internal view returns(bool) {
return (_zombie.readyTime <= now);
}
// 좀비 dna가 생명체의 dna와 혼합되어 새로운 좀비 생성
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal {
// 주인만 좀비에게 먹이를 줄 수 있음
require(msg.sender == zombieToOwner[_zombieId]);
// 먹이를 먹는 좀비의 dna 얻음
Zombie storage myZombie = zombies[_zombieId];
// 좀비가 먹이 먹은 시간을 고려하도록 수정
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus; // 16자리보다 크면 안됨
uint newDna = (myZombie + _targetDna) / 2; // 새로운 dna
// 키티 유전자를 가진 좀비는 dna 마지막 2자리를 99로 변경
if(keccak256(_species) == keccak256("kitty")) {
newDna = newDna - newDna % 100 + 99;
}
// 좀비 생성 함수 호출
// 인자로 좀비의 이름과 dna 값을 받음
_createZombie("NoName", newDna);
// 좀비의 재사용 대기시간 만듦
_triggerCooldown(myZombie);
}
// 크립토키티 컨트랙트와 상호작용
// 크립토키티 컨트랙트에서 고양이 유전자를 얻어내는 함수를 생성
function feedOnKitty (uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
// dna가 혼합된 새로운 좀비를 생성하는 함수
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
- 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);
_;
}
// 좀비의 레벨이 2 이상이면 이름 수정 가능
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) {
require(zombieToOwner[_zombieId] == msg.sender);
zombies[_zombieId].name = _newName;
}
// 좀비의 레벨이 20이상이면 좀비에게 임의의 dna 줄 수 있음
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(zombieToOwner[_zombieId] == msg.sender);
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.lengthl; i++) {
if(zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
github : https://github.com/dlwltn98/studyCryptoZombies
cryptoZombies: https://cryptozombies.io/ko/course
'Solidity' 카테고리의 다른 글
SolidityCryptoZombies 5 (ERC721 & 크립토 수집품) 정리 (0) | 2023.02.10 |
---|---|
SolidityCryptoZombies 4 (좀비 전투 시스템) 정리 (0) | 2023.02.09 |
CryptoZombies 2 (좀비가 희생물을 공격하다) 정리 (0) | 2023.02.07 |
CryptoZombies 1 (좀비 공장 만들기) 정리 (0) | 2023.02.06 |
vscode + solidity (1) | 2023.02.05 |