private
: 컨트랙트 내부의 다른 함수들에서만 호출될 수 있음internal
: private과 비슷하지만, 해당 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있음external
: 오직 컨트랙트 외부에서만 호출public
: 내외부 모두에서, 어디서든 호출view
: 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음pure
: 해당 함수가 어떤 데이터도 블록체인에 저장하지 않을 뿐만 아니라, 블록체인으로부터 어떤 데이터도 읽지 않음둘 다 컨트랙트 외부에서 호출됐을 경우, 가스를 전혀 소모하지 않고 다른 함수에 의해 내부 호출되었을 경우에는 가스를 소모한다.
onlyOwner
와 aboveLevel
같은 것!이런 제어자들은 함수 하나에 다음처럼 함께 사용할 수 있다.
function test() external view onlyOwner anotherModifier { /* ... */ }
✨ payable은 이더를 받을 수 있는 특별한 함수 유형!
이를 통해 함수를 실행하기 위해 컨트랙트에 일정 금액을 지불하게 하는 것 같은 구조를 아래와 같이 만들어낼 수 있다.
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.toWei(0.001)})
value
필드를 보면, 자바스크립트 함수 호출에서 이 필드를 통해 ether
를 얼마나 보낼지 결정한다.
참고: 만약 함수가 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;
msg.sender.transfer(msg.value - itemFee);
storage
에 저장하고, 누군가 판매자의 아이템을 구매하면 구매자로부터 받은 요금을 그에게 전달할 수도 있다. seller.transfer(msg.value)
이런 식으로 누구에게도 제어되지 않는 분산 장터를 만들 수 있다!
😂 솔리디티에서는 난수를 안전하게 생성할 수 없다.....!
다음과 같은 방식으로 난수를 만들어낼 수 있다
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
이 예시에서는 now
의 타임스탬프 값, msg.sender
, 증가하는 nonce
(딱 한 번만 사용되는 숫자, 즉 똑같은 입력으로 두 번 이상 동일한 해시 함수를 실행할 수 없게 함)를 받고 있다.
그리고서 keccak
을 사용하여 이 입력들을 임의의 해시 값으로 변환하고, 변환한 해시 값을 uint
로 바꾼 후, % 100
을 써서 마지막 2자리 숫자만 받도록 했네. 이를 통해 0과 99
사이의 완전한 난수를 얻을 수 있다.
이 메소드는 정직하지 않은 노드의 공격에 취약하다.
이더리움 검증 과정
이더리움에서는 컨트랙트의 함수를 실행하면 transaction
으로서 네트워크의 노드 하나
혹은 여러
노드에 실행을 알리게 된다.
그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고, 작업 증명
으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 된다.
그리고 해당 트랜잭션 그룹을 그들의 작업 증명
(PoW)과 함께 _블록_
으로 네트워크에 배포하게 된다.
한 노드가 어떤 PoW를 풀면, 다른 노드들은 그 PoW를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증한다.
유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작한다.
이것이 우리의 난수 함수를 취약하게 만든다.
트랜잭션 조작 가능
동전 던지기 컨트랙트를 사용한다고 가정 - 앞면이 나오면 돈이 두 배가 되고, 뒷면이 나오면 모두 다 잃는 것이다. 앞뒷면을 결정할 때 위에서 본 난수 함수를 사용한다고 가정한다. (random >= 50은 앞면, random < 50은 뒷면이네).
내가 만약 노드를 실행하고 있다면, 나는 오직 나의 노드
에만 트랜잭션을 알리고 이것을 공유하지 않을 수 있다. (하나 또는 여러 노드에 실행을 알리면 되니까)
그 후 내가 이기는지 확인하기 위해 동전 던지기 함수를 실행할 수 있다.
블록체인의 전체 내용은 모든 참여자에게 공개되므로, 이건 풀기 어려운 문제이고 그 해결 방법은 여러가지가 있다.StackOverflow
하나의 방법은 이더리움 블록체인 외부의 난수 함수에 접근할 수 있도록 오라클
을 사용하는 것이다. (내부에서 만들지 말고 외부 난수 함수를 가져온다!)
oracle
: 이더리움 외부에서 데이터를 받아오는 안전한 방법 중 하나
오라클을 사용해서 블록체인 밖에서 안전한 난수를 만들어 온체인에서 사용