ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Ethernaut 풀이] 이더리움을 해킹해보자 - 1.Fallback
    BlockChain/Technology 2021. 8. 11. 01:11

    오늘의 풀어볼 문제는 

    Fallback 입니다.

     

    Fallback은 컨트랙트 함수에 없는 함수를 호출했을 때 실행되거나 

    컨트랙트에 receive 함수가 없는 데 data 없이 호출되었을 때 실행됩니다.

     

    ≪ Ethernaut 풀이 시리즈 ≫

     

    [Ethernaut 풀이] 이더리움을 해킹해보자 - 0.Hello Ethernaut


    1. 목표 확인하기

    • 컨트랙트의 주인이 되어야 한다.
    • 컨트랙트의 잔액이 0이 되어야 한다.

    Get new instance를 눌러 게임을 시작합니다.

     

    콘솔창에 보면 Level address와 Instance address 두 가지 주소가 나옵니다.

    Level - Contract Account (CA)
    -> Level 컨트랙트는 사용자가 요청하면 Instance 컨트랙트를 생성합니다.
    콘솔 명령 : level
    Instance - Contrac tAccount (CA)
    -> Level의 내용를 담고 있는 컨트랙트입니다.
    콘솔 명령 : instance

     

    풀이에 도움이 되는 힌트도 제공됩니다.

    • How to send ether when interacting with an ABI
    • How to send ether outside of the ABI
    • Converting to and from wei/ether units (see help() command)
    • Fallback methods

     

    2. 문제 풀이

     

    아래는 level 1의 instance 배포에 사용된 Solidity 코드입니다. 이 코드를 분석해서 미션을 완수해야 합니다.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.6.0;
    
    import '@openzeppelin/contracts/math/SafeMath.sol';
    
    contract Fallback {
    
      using SafeMath for uint256;
      mapping(address => uint) public contributions;
      address payable public owner;
    
      constructor() public {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
      }
    
      modifier onlyOwner {
            require(
                msg.sender == owner,
                "caller is not the owner"
            );
            _;
        }
    
      function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if(contributions[msg.sender] > contributions[owner]) {
          owner = msg.sender;
        }
      }
    
      function getContribution() public view returns (uint) {
        return contributions[msg.sender];
      }
    
      function withdraw() public onlyOwner {
        owner.transfer(address(this).balance);
      }
    
      receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
      }
    }​

     

    ① 최초의 Owner

    constructor() 

    solidity언어는 객체지향언어(OOP)이기 때문에 객체지향 특징 중 하나인 생성자(Constructor)를 가지고 있습니다. 생성자는 컨트랙트가 처음 배포될 때 호출됩니다. 이 컨트랙트가 배포될 때, owner는 msg.sender(컨트랙트 배포자)가 되며 컨트랙트 배포자의 contributions에 매핑된 값은 1000 ether가 됩니다. 

     

    ② Owner 바꾸기

    1번째 목표인 owner를 나로 바꾸기를 달성할 수 있는 코드를 찾아봅니다. 

    contribute() 함수와 receive() 함수 마지막 줄에 owner = msg.sender가 적혀있습니다. 즉, owner를 바꾸기 위해서는 이 두 함수를 실행시켜야 합니다.

     

    ● function contribute()

      function contribute() public payable {
        require(msg.value < 0.001 ether);
        contributions[msg.sender] += msg.value;
        if(contributions[msg.sender] > contributions[owner]) {
          owner = msg.sender;
        }
      }​

    require은 함수를 실행시킬 수 있는 충분 조건에 해당합니다. contribute() 함수를 호출할 때 msg.value의 값이 0.001 ether보다 작아야 됩니다. 

     

    다음 코드를 통해 contribute() 함수를 호출하고, 이더를 보낼 수 있습니다.

    => await contract.contribute.sendTransaction({value: toWei("0.0001")})

     

    contract는 컨트랙트를 Web3와 ABI를 이용해서 console에서 접근할 수 있게 한 것입니다.

    sendTransaction은 이더를 보내는 함수인데 기본은 wei단위로 설정됩니다. 그래서 toWei를 통해 ether단위로 변경해주었습니다.

     

    require을 만족했으니 다음 줄인 contributions[msg.sender] += msg.value가 실행됩니다. 처음 함수를 호출한 것이라면 contributions 매핑에 msg.sender(호출자의 주소) - msg.value(0.0001 ether)가 저장됩니다. 그리고 if(...)으로 넘어가면 드디어 owner를 바꿀 수 있는 최종 조건입니다. 

     

    contributions[msg.sender]가 contributions[owner]보다 커야 하는데 현재 0.0001 ether > 1000 ether가 되므로 false값이 반환되어 조건을 충족시키지 못합니다. 이 조건을 넘어가려면 contribute() 함수로 0.001 보다 작은 이더를 계속 보내서 1000 ether를 넘겨야 합니다. 최소 백만 번 이상을 실행시켜야 하니 무지하게 오래걸릴 것입니다. 다른 방법을 찾아보겠습니다.

     

    ● receive()

      receive() external payable {
        require(msg.value > 0 && contributions[msg.sender] > 0);
        owner = msg.sender;
      }
    }

     

    receive() 함수를 먼저 알아보겠습니다. 솔리디티에서 fallback 함수와 비슷하며, send나 transfer로 이더를 보낼 때 실행됩니다. 특징은 이렇습니다.

    • receive() external payable { … }
      • function 키워드를 사용하지 않습니다.
      • external payable로 명시됩니다.
      • 매개 변수를 갖지 않고 함수 명은 receive여야 합니다.
      • 빈 calldata를 가지고 컨트랙트가 호출될 때 실행됩니다.

    fallback 함수와 비슷하지만 실제 쓰일 때는 이더를 받는 용도로 receive를 사용하고, 컨트랙트에 없는 함수를 호출할 경우에  fallback 함수를 사용합니다. 

     

    => await sendTransaction({from:player, to:"contract.address", value: toWei("0.0001")})

     

     

    Web3에서 지원하는 sendTransaction의 기본형은 다음과 같습니다. 보내는 주소, 받는 주소, 금액을 입력합니다.

    eth.sendTransaction({from: account address, to: account address, value: web3.toWei(number, "ether")})

     

    컨트랙트로 이더를 전송하는 명령어입니다. 위에서 sendTransaction을 사용할 때는 value만 적었는데, from과 to가 추가되었습니다. player는 현재 메타마스크가 연결된 나의 계정 주소이고, 받는 쪽은 컨트랙트의 주소입니다. 0.0001 ether를 보냈습니다. receive 함수가 호출이 되고 owner를 바꾸기 위해서는 require의 조건을 충족시켜야 합니다.

     

    require의 조건을 보면 msg.value > 0 와 contributions[msg.sender] > 0 두 조건이 있습니다.

    첫 번째 조건은 require 함수를 호출할 때 0.0001 ether를 보냈으므로 자연스럽게 msg.value의 값이 0.0001 ether가 되어 충족됩니다. 

    두 번째 조건은 contributions[msg.sender]의 값이 0보다 커야 합니다. contributions에 매핑값을 넣으려면 contribute 함수를 통해 넣을 수 있었습니다. 위에서 contribute 함수를 호출하며 0.0001 ether를 보냈을 때 contribution[msg.sender]의 값에 0.0001 ether가 저장이 되었으므로 조건이 충족됩니다.

     

    첫 번째와 두 번째 조건이 모두 충족되었으므로 ownermsg.sender인 나로 바뀌게 됩니다.

     

    1번 목표 달성 ! 컨트랙트의 주인이 되어야 한다.

     

     

    ③ 컨트랙트의 잔액 0으로 만들기

    Owner를 바꾸는 과정에서 현재 컨트랙트의 잔액은 0.0002 ether가 쌓이게 되었습니다. 이 잔액을 0으로 만들어야 목표가 완전히 달성됩니다. 컨트랙트의 잔액을 0으로 만들기 위해서 사용가능한 함수를 찾아봅니다. 

     

     function withdraw() public onlyOwner {
        owner.transfer(address(this).balance);
      }

    withdraw() 함수가 있습니다. 아래 transfer는 솔리디티에서 제공하는 함수입니다.

     

    <address>.transfer(uint256 amount):

    주어진 양만큼의 Wei를 Address로 전송합니다. 실패시 에러를 발생시키고 2300 gas를 전달하며 이 값은 변경할 수 없습니다.

     

    transfer를 이용해서 컨트랙트에 있는 잔액을 owner인 나에게 전송할 수 있습니다. 여기서 특이한 점이 있습니다. 함수를 선언하는 줄에서 public 옆에 onlyOwner가 적혀 있습니다. 함수 제어자를 통해 owner만 이 함수를 실행시킬 수 있게 권한을 부여한 것입니다. 이는 modifier(함수제어자)에서 설정할 수 있습니다.

     

    코드의 윗부분으로 가면 modifier가 등장합니다.

     modifier onlyOwner {
            require(
                msg.sender == owner,
                "caller is not the owner"
            );
            _;
        }

    modifier의 사용은 간단합니다. 함수가 실행되기 전에 요구조건을 만족시키는 확인하는 작업을 해줍니다. onlyOwner가 추가된 함수를 호출시 modifier가 실행됩니다. 위의 내용에서는 owner와 현재 msg.sender가 같을 경우 modifier 내부의  _;부분에서 함수로 되돌아가 함수가 실행이 됩니다. 다를 경우는 caller is no th owner라는 메시지가 출력되며 실행이 되지 않습니다.

     

    자 이제 인출을 할 수 있는 조건이 만족되었으니 

     

    => await contract.withdraw()

     

    를 입력하면 컨트랙트의 잔액이 인출되면서 0이 됩니다.

     

    2번 목표 달성 ! 컨트랙트의 잔액이 0이 되어야 한다.

     

    이제 목표가 완전히 달성되었으니 Submit instance 버튼을 눌러 1단계를 완료하면 됩니다. 수고하셨습니다 !

     

    이번에도 요란한 콘솔창에 출력되는 미션 완수 메시지..

     

     

     

    댓글

Designed by Tistory.