[SCH] Unchecked Return

frenchkebab·2023년 5월 20일
0
post-thumbnail

Solidity에서 Low-level call을 하였을 때, return value를 check하지 않을 경우 문제가 생길 수 있는 경우들이 있다.

ERC20 approve과 front running - increaseAllowance(), decreaseAllowance()를 쓰시오 에서 다룬 적이 있는데,
이번 글에서는 문제가 되는 상황들을 한번 살펴보려고 한다.

송금의 3가지 방법

1) tansfer

: transfer를 사용하면, 사실은 가장 직관적이다. 실패할 경우 revert를 시켜주므로 가장 깔끔한 방법이다.
하지만 문제는 전달해주는 gas2300으로 고정되어 있어, receive()함수에서 자유도가 매우 떨어진다.

따라서 사용하지 않는 것이 좋다.

2) send

: transfercall의 중간 단계인데 정말 아무짝에 쓸모 없는 함수인데 왜 있는지 의문일 정도이다.
transfer처럼 gas2300으로 제한되어 있는데, call처럼 실패하였을 경우 revert되지도 않는다.

3) call

: low-level call에서 사용되는데, tx의 실패 여부에 따라 true 혹은 false를 return해준다.

다만 한 가지 주의할 점이 있는데 아래 예시를 보자.

pragma solidity 0.8.18;

contract CallNonExistingAddr {
    function test() external {
        (bool success, ) = address(0).call(abi.encodeWithSignature("what?"));
        require(success); // 통과
    }
}

존재하지 않는 주소로 call을 하였는데, revert가 생기지 않으면 true를 return하게 되니, 주의하여야 한다.

Example1 - receive()가 없는 contract로 송금

receive()함수가 없는 contract에 송금을 할 경우, 트랜잭션이 실패하게 된다.

하지만 여기서 send 혹은 call을 사용하고, return value를 체크해주지 않고 만약 contract에서 자산을 withdraw하는 로직이 없다면, 영원히 ETH가 해당 contract에 묶여버리게 된다.

withdraw로직이 있다고 하더라도, 비즈니스로직이 망가질 수 있다.

Example2 - 실패해도 무시하는 경우

대출을 받을 때 debtToken을 mint하고 차입자에게 돈을 빌려주는 함수가 있다고 해보자.

escrowNFT.call(abi.encodeWithSignature("mint(address,uint256,uint256)", _recipient, amount, matureTime));

이렇게 low-level call로 함수를 호출하고, return value를 체크하지 않을 경우,
mint함수가 최초에만 실행되고 이후에는 revert되게 된다.

따라서, 차입자는 1번만 debtToken을 mint하고 돈을 계속 무한정 가져갈 수 있게 된다.

Example3 - ERC20 토큰

    function swap(address fromToken, address toToken, uint256 amount) external nonReentrant {
        require(isSupported(fromToken, toToken), "one of the tokens (or both) are not supported");
        require(amount > 0, "amount should be bigger then 0");

        // Check liquidity
        uint256 balance = IERC20(toToken).balanceOf(address(this));
        require(balance >= amount, "Not enough liquidity");

        // Transfer
        IERC20(fromToken).transferFrom(msg.sender, address(this), amount);
        IERC20(toToken).transfer(msg.sender, amount); value not checked
    }

이 예시를 한번 보자.

ERC20의 전송에 대한 함수 구현은 토큰마다 제각각이다.

    function transferFrom(address _from, address _to, uint256 _value) public onlyPayloadSize(3 * 32) returns (bool) {
        var _allowance = allowed[_from][msg.sender];

        if (_value > _allowance) {
            return false;
        }

        uint256 fee = (_value.mul(basisPointsRate)).div(10000);
        if (fee > maximumFee) {
            fee = maximumFee;
        }
        if (_allowance < MAX_UINT) {
            allowed[_from][msg.sender] = _allowance.sub(_value);
        }
        uint256 sendAmount = _value.sub(fee);
        balances[_from] = balances[_from].sub(_value);
        balances[_to] = balances[_to].add(sendAmount);
        if (fee > 0) {
            balances[owner] = balances[owner].add(fee);
            Transfer(_from, owner, fee);
        }
        Transfer(_from, _to, sendAmount);
    }

이런 식으로 만약 bool 값만 return을 해주고, 실패 시 revert를 시키지 않는다면?
위의 예시에서 msg.sender의 balance가 0이여서 transferFrom에서 false가 return되어도 계속 token을 받아올 수 있게 된다.

profile
Blockchain Dev Journey

0개의 댓글