본문 바로가기

블록체인/Solidity

[Solidity] 스마트 컨트랙트에서 재진입 공격과 address.call()과 address.transfer()의 차이

반응형

Reentrancy Attack을 공부하면서 의문이 생겼다.

fallback function은 ether가 Contract에 송금되면 정말 무조건 실행되는 것인지..

실제로 코드를 짜서 재진입 상황을 재현해보려고 했는데.

gas 비용이 부족하다고 재진입이 안되는 것이었다....안심스러우면서도 대략난감한 상황....


Reentracy Attack이란?


재귀적인 방법을 사용해서 Contract에서 보유한 모든 이더를 출금하는 방법이다.


해커가 계약을 작성해서 다른 계약으로부터 송금받는 코드를 작성한다.

그리고 해커가 정의한 fallback function에서 다시 해커가 작성한 계약에 이더를 송금하는 function(앞서 호출한 함수와 동일한 함수)을 호출하는 방법으로  퍼브릭 블록체인의 경우 Contract의 코드가 모두 공개되어 있으므로 코드의 허점을 사용해서 돈을 다 털어비리는 방법이다.


필자가 부족하여 설명이 어려우므로 아래 글을 보면 이해하기 쉬울 것 같다..


Reentrancy Attack 관련 글


pragma solidity ^0.5.0;

contract A {
B b;
address wallet;
event EventA(string indexed name);
constructor (B _b) public payable{
b = B(_b);
}
function () external payable {
sendToB();
emit EventA('excuted A fallback function');
}
function sendToB( ) public returns (uint) {
b.get();
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}

contract B {
event EventB(string indexed name);
constructor () public payable{
}
function get() public {
msg.sender.transfer(1 ether);
//msg.sender.call.value(1 ether)("")
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
function () external payable {
emit EventB('excuted B fallback function');
}
}


Contract B의 get() 메소드를 보면 msg.sender.transfer()를 사용하고 있다.

아래의 순서대로 실습을 해보자


1. 두 컨트랙트를 RemixIDE를 이용해서 배포한다.

2. B에 fallback function을 이용해 적정량의 ether를 송금해준다.

3. A에 fallback function을 이용해 적정량의 ether를 송금해준다.


그러면 아래와 같은 결과를 받을 수 있다.


transact to A.(fallback) errored: VM error: revert. revert The transaction has been reverted to the initial state. Note: The constructor should be payable if you send value. Debug the transaction to get more information.



트랜잭션에서 좀 더 정확한 상태를 조회하니 fail을 리턴했다.

결과적으로 설명하자면 실행할 gas가 부족해서 fail을 리턴한 것이다.

address.send()나 transfer()는 사용할 수 있는 gas의 양이 제한되어 있다. (2300gas)


contract B에서 transfer를 실행했을 때 상대의 fallback function에서 기본적인 log을 출력할 수 있는 2300gas만 사용할 수 있고

그 이상의 gas가 소모되는 작업은 fail을 return한다. (자세한 사항은 글 최하단 글 상자 참고)


fallback function에서 제한된 gas 이상을 소모하는 작업을 실행하려면 address.call.value()()를 사용하여야한다.

사실인지 확인해보기 위해 contract B의 get() 메소드에서 msg.sender.transfer(1 ether)를 msg.sender.call.value()()로 바꿔서 실행해보자.



성공적으로 실행된다. 

재귀적으로 함수가 호출 되었는지 로그도 확인해보자.



메소드를 한번 실행했는데. EventA가 여러번 발생했다. 통장잔고도 확인해보자.

contract B의 잔고가 텅텅비었다.(의도한 텅장...)


참고하면 좋은 글들이다.


1. 솔리디티 공식 문서


In the worst case, the fallback function can only rely on 2300 gas being available (for example when send or transfer is used), leaving little room to perform other operations except basic logging. The following operations will consume more gas than the 2300 gas stipend:

      • Writing to storage
      • Creating a contract
      • Calling an external function which consumes a large amount of gas
      • Sending Ether




반응형