이더리움에서 송금할 때 3가지 방법이 있다. 각각 어떤 차이점이 있는지 알아보자.
받을주소.send(amount);
이더리움 초기에 만든 방법이다. 전송에 성공하면 true를 리턴하고 실패하면 false를 리턴한다. send
의 단점은 에러를 리턴하지 않고, 고정된 gas를 소비한다는 것이다.(2300gas)
그 다음에 나온 애가 call{}()
이다. send
와 다르게 가변적으로 gas를 소비할 수 있다는 장점이 있다. 하지만 단점 또한 명확하다. 첫째는 여전히 true,false로 성공여부를 리턴한다는 점. 둘째는 가변적으로 gas를 소비할 수 있기 때문에 재진입 공격에 취약하다는 점이다. 재진입 공격은 더 다오(The DAO) 해킹으로 유명한 공격이다.
A컨트랙트에서 익명의 B컨트랙트로 송금했을 때 송금이 완료되기 전에 B컨트랙트 내부의 있던 악의적 함수가 A컨트랙트 내부의 송금 함수를 계속해서 호출하게 한다. 결국 마지막에는 A컨트랙트 자금이 모두 없어질 때 까지 호출해서 자금이 탈취되는 공격이다.
받을주소.transfer(amount);
3가지 방법 중 가장 안전하게 송금하는 방법은 transfer()
였다. 고정된 gas를 소비(2300gas)하고, 실패시 에러를 리턴하기 때문이다. 하지만 2019년 이스탄불 하드포크 이후에 가스비가 오르면서 2300gas로는 충분하지 않게 되었다. 제한된 가스가 소용이 없게 되면서 transfer()
도 재진입 공격에 노출되는 위험이 생겨났다.
그럼 어떤 방식으로 송금하는 것이 안전할까?
function 내에서 원하는 기능을 실행하기 전에, 사전에 필수적으로 체크되어야 할 전제조건을 모두 확인한 후 실행이 되도록 하는 패턴을 사용한다면 비교적 안전하게 송금할 수 있다.
function auctionEnd() public {
// 1. Checks require(now >= auctionEnd);
require(!ended);
// 2. Effects
ended = true;
// 3. Interaction
beneficiary.transfer(highestBid);
}
아래 코드는 송금을 먼저 하고나서 sender의 금액을 0으로 만든다. 만약 해커가 fallback 함수로 withdraw()
함수를 계속 호출한다면 sender의 금액을 0으로 만들기 전에 Bank의 잔고가 먼저 0이 될 것이다.
따라서 아래처럼 call
함수가 실행되기 전에 sender의 금액을 0으로 만들어주면 해커가 withdraw()
함수를 계속 호출한다고 해도 이미 sender의 금액은 0이므로 송금이 불가능하다.