ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • EIP-150 : 이더리움의 63/64 Rule
    evm 2024. 2. 22. 21:43

    Author : 최원혁

     

    본 내용은 EIP-150에 대한 내용을 다루고 있습니다.

    Intro


    2016년 7월 20일에 이더리움 비잔티움 하드포크 당시, 업데이트된 이더리움 개선 제안(EIP)으로 EIP-150가 프로토콜에 반영된 적이 있다. 이는 스마트 컨트랙트의 함수 로직 내부에 외부 함수(다른 컨트렉트의 함수) 호출(external call) 시, 연산되는 가스비를 63/64 Rule 규칙을 도입하여 재정의하자는 내용으로, 이더리움 프로토콜 내에 큰 변화를 주는 의미있는 EIP였다. 지금부터 EIP-150와 63/64 Rule 규칙이 어떤것인지 알아보자.

     

     

     

    EIP-150


    이더리움 공식 Github에 나와있는 EIP-150의 제목은 “IO(input/output)이 많은 작업의 가스 비용 변화(Gas cost changes for IO-heavy operations)” 이다. IO하나의 트랜잭션에서 발생하는 외부 함수 호출(input) 및 결과 리턴(output)를 의미한다. 여기서 외부 함수 호출(external call)은 스마트 컨트랙트의 함수 호출을 의미한다. Opcode에서 CALL, STATICCALL, DELEGATECALL 3가지가 외부 함수 호출시 사용된다. 즉, EIP-150은 트랜잭션에 의해 동작하는 EVM 내에서 너무 많은 외부 함수 호출에 의해 발생하는 문제를 해결하기 위해 가스비 계산 공식을 재정의하겠다는 내용이다. 그럼 어떤 문제가 있었기에 EIP-150이 제안되었을까?

     

    이더리움 비잔티움 하드포크(2016) 이전에는 외부 함수 호출에 소비되는 가스비가 매우 낮아서, 스마트 컨트랙트 함수에서 다른 함수를 가스비 부담 없이 여러 번 호출할 수 있었다. 그러나 이더리움은 하나의 트랜잭션에서 반복 호출되는 외부 함수의 스택 깊이(Call Stack Depth)를 1024로 제한했기 때문에, 일정 시점에서 해당 외부 함수 호출은 “stack too deep” 애러와 함께 revert된다. 이런 EVM 프로세스에는 특정 취약점이 있었으며, 이를 활용한 서비스 거부 공격(denial-of-service attacks)이 발생하게 된다.

     

     

     

    Call Depth Attack


    Call Depth Attack이란 아무 external call를 반복적으로 호출하여 Call Stack depth를 1023까지 올리고 *서비스 로직에 포함된 external callCall Stack depth 1024때 호출 시켜 의도적으로 실패하게 만든 후, 나머지 로직을 실행시키는 공격이다. 이는 개발자가 구현한 서비스 로직과 다르게 흘러가도록하는 서비스 거부 공격 패턴이다. 아래에 Call Depth Attack을 설명하는 대표 코드인 경매(Auction) 컨트랙트를 보고 알아보자.

     

    *서비스 로직 : 개발자가 의도한 정상적인 로직

     

    | 서비스 로직 : Contract Auction

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.5;
    
    // DO NOT USE!!!
    contract Auction {
        address public highestBidder;
        uint256 public highestBid;
    
        function bid() external payable {
            if (msg.value < highestBid) revert();
    
            if (highestBidder != address(0)) {
                payable(highestBidder).send(highestBid); // refund previous bidder
            }
    
            highestBidder = msg.sender; // “가장 이더를 많이 보낸 사람” 칭호 획득
            highestBid = msg.value;
        }
    }
    
    contract CallDeplthAttack {
      address auction;
    
      constructor(address _auction) {
        auction = _auction;
      }
    
      /// @param i 1024
      function attack(uint i) public payable {
        if(i>1) {
          this.attack(i-1);
        }
        auction.call{value:msg.value}(abi.encodeWithSignature("bid()"));
      }
    }

     

    Contract Auction은 bid() 함수를 통해 컨트랙트로 가장 많은 이더를 전송하는 사람의 지갑주소와 이더양을 변수 highestBidderhighestBid에 저장하여 “가장 이더를 많이 보낸 사람” 칭호를 얻을 수 있다. 만약 bid() 함수를 호출하는 새로운 사람의 이더양이 더 많은 경우, 이전에 칭호를 받은 사람이 보낸 이더는 다시 환불 받고, 새로운 사람이 “가장 이더를 많이 보낸 사람” 칭호를 얻게 된다.

    여기서 이전 사람의 이더를 “환불”해주는 서비스 로직은 payable(highestBidder).send(highestBid)이다. 스마트 컨트랙트 내에서 이더를 전송하는 transfer(), call(), send()의 Opcode는 CALL이며, 이 또한 외부 함수 호출(external)에 포함된다.

     

     

    | 공격 패턴 : Contract CallDeplthAttack

     

    만약 공격자가 의도적으로 bid() 함수를 Call Stack depth 1023때 호출하면, “환불” 해주는 send()는 Call Stack depth 1024 때 호출되어 “stack too deep” revert 처리 된다. 즉, “환불”이라는 서비스 로직을 Call Depth Attack을 통해 호출하지 못하게 하는 것이다.

    공격자가 실행하는 Contract CallDeplthAttack의 attack 함수를 보자. 함수의 인자로 1024을 넣어주면, attack() 함수는 i > 0 조건이 될때까지 재귀적으로 호출될 것이다. 1023번의 재귀 호출 후, i > 0 조건을 벗어나면 마침내 auction의 bid()를 호출하게 된다. 하지만 bid()의 환불 로직은 Call Stack depth 1024때 호출되어 실패(revert)된다.

     

    그리고 “가장 이더를 많이 보낸 사람” 칭호를 정하는 변수 highestBidder에 공격자의 주소가 저장되는 로직은 정상적으로 실행되며, 칭호의 이전 주인은은 돈도 잃고 칭호도 뺏기게 된다.

    📖 Auction 컨트랙트의 코드는 EIP-150의 63/64 Rule이 적용되어 더 이상 성공적으로 실행되지 않는다.

     

     

     

    해결방안 : 63/64 Rule


    결국 문제는 external call의 가스비가 저렴하여 공격자가 부담없이 의도적으로 Call Stack depth를 1024까지 만들 수 있기 때문에 발생한 것이다. 이를 방지 하기 위해 EIP-150에서는 external call를 사용할 때 마다 현재 사용할 수 있는 gas의 63/64 만큼만 전달할 수 있는 63/64 Rule를 적용한다.

     

    Gas available at Stack depth 0  = Initial gas available * (63/64)^0
    Gas available at Stack depth 1  = Initial gas available * (63/64)^1
    Gas available at Stack depth 2  = Initial gas available * (63/64)^2
    Gas available at Stack depth 3  = Initial gas available * (63/64)^3
    .
    .
    .
    Gas available at Stack depth N  = Initial gas available * (63/64)^N
    

     

    63/64 Rule이 적용되면 아무리 Gas Limit을 높게 잡아도, external call로 전달할 수 있는 가스량은 점진적으로 줄어들기 때문에, Call Stack depth 1024에 도달하기전 가스비 부족으로 트랜잭션 자체가 실패하게 될 것이다.

    https://www.rareskills.io/post/eip-150-and-the-63-64-rule-for-gas

     

    63/64 Rule이 적용된 상황에서 사용 가능한 가스비 3000으로 external call를 반복적으로 호출하는 실험을 했을 때, 위 그래프 처럼 어느 순간 external call가 사용할 수 있는 가스비는 급진적으로 줄어들고, 1024 스택에 도달하기전 거의 0에 가까운 가스비만 남게 된다.

     

    현재 이더리움은 여전히 스택 깊이를 1024로 제한하였지만, 63/64 Rule로 인해 가스비가 부족하여 실제로는 도달할 수 없다.

Designed by Tistory.