크립토좀비로 솔리디티 공부하기 링크
지금까지 꽤 많은 함수 제어자(function modifier)
를 다뤘다. 한번 빠르게 복습해보자.
우린 함수가 언제, 어디서 호출될 수 있는지 제어하는 접근 제어자(visibility modifier)를 알게 되었다 :
private
은 컨트랙트 내부의 다른 함수들에서만 호출될 수 있음을 의미한다. internal
은 private
과 비슷하지만, 해당 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있다. external
은 오직 컨트랙트 외부에서만 호출될 수 있다. 마지막으로 public
은 내외부 모두에서, 어디서든 호출될 수 있다.
상태 제어자(state modifier)
는 블록체인과 상호작용하는 방법에 대해 알려준다. view
는 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음을 알려준다. pure
는 해당 함수가 어떤 데이터들도 블록체인에 저장하지 않을 뿐만 아니라, 블록체인으로부터 어떤 데이터도 읽지 않음을 알려준다. 이들 모두 컨트랙트 외부에서 불렸을 때 가스를 전혀 소모하지 않는다. (하지만 다른 함수에 의해 내부적으로 호출됐을 경우에는 가스를 소모한다.)
사용자 정의 제어자
에 대해서도 배웠다. 예를 들면 onlyOwner
와 aboveLevel
같은 것이다. 이런 제어자를 사용해서 함수에 이 제어자들이 어떻게 영향을 줄지를 결정하는 논리를 구성할 수 있다.
이런 제어자들은 함수 하나에 다음처럼 함께 사용할 수 있다 :
function test() external view onlyOwner anotherModifier { /* ... */}
이번 챕터에서 함수 제어자를 하나 더 배울 것이다.
payable
제어자payable
함수는 솔리디티와 이더리움을 아주 멋지게 만드는 것 중 하나이다. 이는 이더를 받을 수 있는 특별한 함수 유형이다.
예를 들자면, 일반적인 웹 서버에서 API함수를 실행할 때 함수 호출을 통해서 US달러나 비트코인을 보낼 수 없다.
하지만 이더리움에서는 돈(이더), 데이터(transaction payload), 그리고 컨트랙트 코드 자체가 모두 이더리움 위에 존재하기 때문에 함수를 실행하는 동시에 컨트랙트에 돈을 지불하는 것이 가능하다.
이를 통해 함수를 실행하기 위해 컨트랙트에 일정 금액을 지불하게 하는 것과 같은 구성을 만들어 낼 수 있다.
예시 :
contract OnlineStore {
function buySomething() external payable {
// 함수 실행에 0.001이더가 보내졌는지 확인 :
require(msg.value == 0.001 ether);
// 보내졌다면, 함수를 호출한 자에게 디지털 아이템을 전달하기 위한 내용 구성 :
transferThing(msg.sender);
}
}
여기서, msg.value
는 컨트랙트로 이더가 얼마나 보내졌느지 확인하는 방법이고, ether
는 기본적으로 포함된 단위이다.
여기서 일어난는 일은 누군가 web3.js(DApp의 자바스크립트 프론트앤드)에서 다음과 같이 함수를 실행할 때 발생한다 :
// `OnlineStore`는 나의 이더리움 상의 컨트랙트를 가리킨다고 가정한다 :
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWie(0.001)})
value
필드를 주목해보자. 자바스크립트 함수 호출에서 이 필드를 통해 ether
를 얼마나 보낼지 결정한다. (여기서는 0.001) 트랜잭션을 봉투로 생각하고 함수 호출에 전달하는 매개 변수를 내가 써넣은 편지의 내용이라고 생각한다면, value
는 봉투 안에 현금을 넣는 것과 같다. 편지와 돈 모두 수령인에게 전달된다.
참고 : 만약 함수가
payable
로 표시되지 않았는데 위에서 본 것처럼 이더를 보내려 한다면, 함수에서 나의 트랜잭션을 거부할 것이다.
컨트랙트로 이더를 보내면, 해당 컨트랙트의 이더리움 계좌에 이더가 저장되고 거기에 갇히게 된다. 컨트랙트로부터 이더를 인출하는 함수를 만들지 않는다면 말이다.
다음과 같이 컨트랙트에서 이더를 인출하는 함수를 작성할 수 있다 :
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
우리가 Ownable
컨트랙트를 import했다고 가정하고 owner
와 onlyOwner
를 사용하고 있다는 것을 참고하자.
transfer
함수를 사용하면 이더를 특정 주소로 전달할 수 있다. 그리고 this.balance
는 컨트랙트에 저장돼있는 전체 잔액을 반환한다. 그러니 100명의 사용자가 우리의 컨트랙트에 1이더를 지불했다면, this.balance
는 100이더가 될 것이다.
transfer
함수를 쓰면 특정한 이더리움 주소에 돈을 보낼 수 있다. 예를 들어, 만약 누군가 한 아이템에 대해 초과 지불했다면, 이더를 msg.sender
로 되돌려주는 함수를 만들 수도 있다 :
uint itemFee = 0.001 ether;
mas.sender.transfer(msg.value - itemFee);
혹은 구매자와 판매자가 존재하는 컨트랙트에서, 판매자의 주소를 storage에 저장하고, 누군가 판매자의 아이템을 구매하면 구매자로부터 받은 요금을 그에게 전달할 수도 있다 : seller.transfer(msg.value)
.
이런 것들이 이더리움 프로그래밍을 아주 멋지게 만들어주는 예시들이다. 이것처럼 누구에게도 제어되지 않는 분산 장터들을 만들 수도 있다.
솔리디티에서 난수를 만들기에 가장 좋은 방법은 keccak256
해시 함수를 쓰는 것이다.
다음과 같은 방식으로 난수를 만들어낼 수 있다 :
// Generate a random number between 1 and 100 :
uint randNonce = 0;
utin random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccack256(now, msg.sender, randNonce)) % 100;
이 예시는 now
의 타임스탬프 값, msg.sender
, 증가하는 nonce
(딱 한 번만 사용되는 숫자, 즉 똑같은 입력으로 두 번 이상 동일한 해시 함수를 실행할 수 없게 함)를 받고 있다.
그리고서 keccak
을 사용하여 이 입력들을 임의의 해시 값으로 변환하고, 해시 값을 uint
로 바꾼 후, % 100
을 써서 마지막 2자리 숫자만 받도록 했다. 이를 통해 0과 99 사이의 완전한 난수를 얻을 수 있다.
이 메소드는 정직하지 않은 노드의 공격에 취약하다
이더리움에서는 컨트랙트의 함수를 실행하면 트랜잭션(transaction)
으로서 네트워크의 노드 하나 혹은 여러 노드에 실행을 알리게 된다. 그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고, "작업 증명"으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 된다. 그리고서 해당 트랜잭션 그룹을 그들의 작업 증명(PoW)
과 함게 블록
으로 네트워크에 배포하게 된다.
한 노드가 어떤 PoW
를 풀면, 다른 노드들은 그 PoW
를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증한다. 유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작한다.
이것이 우리의 난수 함수를 취약하게 만든다.
예를 들어 동전 던지기 컨트랙트를 사용한다고 해보자. 앞면이 나오면 돈이 두 배가 되고, 뒷면이 나오면 모두 다 잃는다. 앞뒷면을 결정할 때 위에서 본 난수 함수를 사용한다고 가정해보자. (random >= 50
은 앞면, random < 50
은 뒷면)
내가 만약 노드를 실행하고 있다면, 나는 오직 나의 노드에만
트랜잭션을 알리고 이것을 공유하지 않을 수 있다. 그 후 내가 이기는지 확인하기 위해 동전 던지기 함수를 실행할 수 있다. 그리고 만약 내가 진다면, 내가 풀고 있는 다음 블록에 해당 트랜잭션을 포함하지 않는 것을 선택한다. 나는 이것을 내가 결국 동전 던지기에서 이기고 다음 블록을 풀 때까지 무한대로 반복할 수 있고, 이득을 볼 수 있다.
그럼 이더리움에서는 어떻게 난수를 안전하게 만들어낼 수 있을까?
블록체인의 전체 내용은 모든 참여자에게 공개되므로, 이건 풀기 어려운 문제이고 그 해답은 이 튜토리얼을 벗어나는 내용이다. 하나의 방법은 이더리움 블록체인 외부의 난수 함수에 접근할 수 있도록 오라클
을 사용하는 것이다.
물론, 네트워크 상의 수만 개의 이더리움 노드들이 다음 블록을 풀기 위해 경쟁하고 있으니, 내가 다음 블록을 풀 확률은 매우 낮을 것이다. 위에서 말한 부당한 방법을 쓰는 것은 많은 시간과 연산 자원을 필요로 할 것이다. 하지만 그만큼 보상이 충분히 크다면, 공격할 만한 가치가 있을 것이다.
그러니 이런 난수 생성은 이더리움 상에서 안전하지는 않지만, 실제로는 난수 함수가 즉시 큰 돈이 되지 않는 한, 게임의 사용자들은 게임을 공격할 만한 충분한 자원을 들이지 않을 것이다.
이 튜토리얼에서는 시연 목적으로 간단한 게임을 만들고 있고 바로 돈이 되는 게 없기 때문에, 우린 구현하기 간단한 난수 생성기를 사용하는 것으로 타협할 것이다. 이게 완전히 안전하지는 않다는 걸 알긴 하지만 말이다.
향후 레슨에서는 , oracle
(이더리움 외부에서 데이터를 받아오는 안전한 방법 중 하나)을 사용해서 블록체인 밖에서 안전한 난수를 만드는 방법을 다룰 수도 있다.
보안에 문제가 되지 않기 위해서 누구든지 attack
함수를 실행하는 사용자는 공격에 사용하는 좀비를 실제로 소유하고 있다는 것을 확실하게 해야할 것이다.
해결하는 방법은 다음과 같다 :
require(msg.sender == zombieToOwner[_zombieId]);
이렇게 동일한 내용을 여러번 사용하고 있으니, 코드를 정리하고 반복을 피할 수 있도록 이 내용을 이것만의 modifier
롤 옮기는 것이 좋다.