Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Smart contract/Ethernaut 문제풀이

Ethernaut 문제풀이 #9 - King

방피터 2022. 2. 28. 16:35

Ethernaut 9번째 문제 King! 왕입니다.

이번 문제는 영어가 어렵네요. 이번 컨트랙트는 간단한 게임입니다. 왕이 되려면 기존의 상금보다 많은 돈을 입금해야 합니다. 그러면 그 돈은 기존 왕에게 보내지고 새로운 왕이 선출됩니다. 

인스턴스를 제출할 때 이 레벨은 왕 권한을 다시 돌려받습니다. 오너라서 그런가? 암튼 self proclamation을 막으면 이 레벨을 이길 수 있다고 하..는군요.. 무슨 소리야!! 문제 이해를 못하겠네 큰일입니다.

 

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract King {

  address payable king; //왕
  uint public prize; //상
  address payable public owner;//오너

  constructor() public payable {//생성자
    owner = msg.sender;  // 오너 = 왕
    king = msg.sender;
    prize = msg.value; // 생성할 때 보낸 돈.
  }

  receive() external payable { //receive 함수 abi를 사용하지 않고 돈을 보낼 경우 호출됩니다.
    require(msg.value >= prize || msg.sender == owner);// 오너이거나 보낸 돈이 prize보다 커야 합니다.
    king.transfer(msg.value);//기존의 왕에게 그 돈을 보내고
    king = msg.sender;//돈을 보낸 사람이 새로운 왕이 됩니다.
    prize = msg.value;//새로운 값으로 prize를 업데이트 합니다.
  }

  function _king() public view returns (address payable) {
    return king;//왕 주소를 리턴
  }
}

 

인스턴스를 생성하고 일단 상금이 얼마인지 봅시다. (인스턴스 생성할 때 1이더를 뜯어갑니다.)

 

 

음 보기가 힘드네요. wei를 eth단위로 바꾸죠.

web3.utils.fromWei(await contract.prize())

 

 

저에게 뜯어간 1이더가 첫 상금입니다 아하하 -_-;;

 

 

현재는 왕과 오너가 같습니다. (사실 어떻게 풀지 몰라서 이것 저것 눌러보는거에요 ㅎㅎ)

 

Solidity 문서를 읽던 stack overflow에 가던 시간을 들여서 해결책을 찾아보는 게 중요합니다. 한 하루 이틀 넘게 고민해도 안풀리면 그 때 문제 풀이를 참고하세요. 바로 보면 의미가 없습니다.

 

저는 메뉴얼 변태라 Solidity 문서 사이트에서 이리 저리 찾다보니 아래와 같은 항목을 발견했어요. 이번에 나온 레벨과 코드가 거의 똑같습니다.

https://solidity-kr.readthedocs.io/ko/latest/common-patterns.html#withdrawal-pattern

 

자주 쓰이는 패턴 — Solidity 0.5.10 documentation

컨트랙트에서의 출금 Effect 이후 기금송금에 있어 가장 권장되는 방법은 출금 패턴을 사용하는 것입니다. Effect의 결과로 Ether를 송금하는 가장 직관적인 방법은 직접 transfer 를 호출하는 것이겠

solidity-kr.readthedocs.io

 

일단 해결책은 찾은 거 같은데 제 머리속에서 나온게 아니라 찝찝합니다. ㅠㅠ

 

해법은 다른 컨트랙트를 만들어 그 컨트랙트가 왕이 되게 하는 거에요. 그런데 그 상태로 제출하면 오너가 0을 보내면서 다시 왕이 되려고 할텐데요. 이 때 king.transfer(msg.value)가 실행되면서 컨트렉트에게 0원을 보내려고 할 거에요. 하지만 만약에 이 컨트랙트가 돈을 받을 수 없는 상태면 어떤 일이 벌어질까요? receive() 함수의 실행이 실패하겠죠? 그럼 오너가 다시 왕이 될 수가 없어요. 설명이 어렵나요?

 

그림과 함께 순서대로 다시 설명드릴게요. myContract를 만들어요. 그리고 왕이 되기 위해서 2이더를 King 컨트랙트에 보냅니다.

 

 

그럼 King 컨트랙트는 1.1 이더를 기존 왕(오너)에게 보냅니다. 그리고 Prize는 2 이더가 되고 왕은 myContract가 됩니다.

 

 

이 상태로 컨트랙트를 제출하면 오너는 다시 왕이 되려고 0이더를 전송하면서 receive()를 실행시킬거에요.

 

 

그러면 receive() 함수가 수행되면서 0이더를 myContract에 보내려고 하는데 myContract 함수는 이더를 받지 못하도록 해둡니다.

 

 

그러면 결국 receive() 함수 자체가 실패하면서 오너는 왕이 될 수가 없겠죠?

 

그럼 리믹스에서 코드를 만들어 봅시다. 간단하겠네요.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract myContract {
  address public owner;
 
  constructor () public {
      owner = msg.sender;
  } 
  //받은 돈 그대로 전달
  function sendEther(address payable _target) public payable{
    (bool sent, bytes memory data) = _target.call{value: msg.value}("");
    require(sent, "Failed to send Ether");
  }
  //fallback 실행시 revert()로 거래 취소
  fallback() external {
      revert();
  }
}

위에 작성된 코드처럼 이더를 원하는 주소로 보내는 함수를 만들고 돈을 받을 수 없도록 fallback 함수에서 revert()를 실행합니다. myContract가 왕이 되면 King 컨트랙트의 receive() 함수는 언제나 실패할거에요.

 

자 그럼 테스트 해봅시다. 배포한 후 sendEther를 실행시킬거에요. 먼저 Value에 1.1이더를 넣어 주셔야 합니다. wei 단위로 넣으시려면 1100000000000000000 = 1.1 Ether입니다. Ether로 설정해도 되는데 소수점을 나타낼 수가 없어서 이렇게 했습니다. 귀찮으신 분들은 그냥 2 이더를 보내시면 됩니다. 그리고 sendEther 옆에 King 컨트랙트의 주소를 넣어 주셔야겠죠? sendEther 버튼을 눌러 거래를 실행시켜 보겠습니다.

 

거래가 완료되고 나서 콘솔에 owner와 king을 확인해보면 달라진 것을 볼 수 있습니다. owner는 그대로 owner이지만 king은 myContract의 주소로 바뀌었습니다.

 

 

이제 이 상태로 제출하면 owner는 다시 receive() 함수를 콜해서 왕이 되려고 할거에요. 그런데 myContract의 fallback이 이더 수신을 거부하기 때문에 receive() 함수 전체가 동작을 실패할 거에요. 그러면 owner는 다시 왕이 될 수가 없죠!

 

이제 인스턴스를 제출하면 성공했다는 메세지를 보실 수 있을거에요.

 

 

다시 한번 정리해보자면 상태 변경과 이더 출금이 한 함수내에서 이루어지면 악의적으로 출금을 방해하는 다른 컨트랙트에 의해 해당 함수가 영원히 실패하도록 만들어질 수 있습니다. 따라서 각 주체가 수동적으로 자신의 잔고를 출금해 나갈 수 있도록 출금 패턴을 사용해야 합니다.

 

이번 레벨의 교훈!

 

이더를 바로 전송하지 말고 출금 패턴을 사용하자!

 

레벨 10에서 만납시다.

안녕~!

반응형