codehawks-2023-07-codehawks-escrow-contract-m02

[M-02] Lack of emergency withdraw function when no arbiter is set

보고서

Summary

구매자가 거래를 중단하고 싶은 경우 중재자를 통해야 하는데, 중재자를 설정하지 않은 경우 구매자가 토큰을 회수할 방법이 없다.

Keyword

business logic vul, asset lock

Vulnerability

Escrow 생성자에서는 중재자를 설정할 수 있다. 중재자를 address(0)로 하면 중재자 없이 거래할 수 있으며, 이 경우 중재 기능은 사용할 수 없다.

    constructor(
        uint256 price,
        IERC20 tokenContract,
        address buyer,
        address seller,
        address arbiter,
        uint256 arbiterFee
    ) {
        if (address(tokenContract) == address(0)) revert Escrow__TokenZeroAddress();
        if (buyer == address(0)) revert Escrow__BuyerZeroAddress();
        if (seller == address(0)) revert Escrow__SellerZeroAddress();
        if (arbiterFee >= price) revert Escrow__FeeExceedsPrice(price, arbiterFee);
        if (tokenContract.balanceOf(address(this)) < price) revert Escrow__MustDeployWithTokenBalance();
        ...
        i_arbiter = arbiter;
        ...
    }

이때, 중재자를 설정하지 않은 경우 구매자가 Escrow를 배포할 시 예치한 토큰을 다시 빼낼 방법이 없다. 거래가 성립되지 않은 경우 구매자가 토큰을 다시 돌려받을 수 있는 방법은 중재 요청을 하는 것 뿐이다. 하지만 중재자가 설정되지 않은 경우 initiateDispute는 revert 된다.

    function initiateDispute() external onlyBuyerOrSeller inState(State.Created) {
        if (i_arbiter == address(0)) revert Escrow__DisputeRequiresArbiter();
        s_state = State.Disputed;
        emit Disputed(msg.sender);
    }

initiateDispute를 호출하여 상태를 State.Disputed로 바꿔야만 resolveDispute 함수를 호출하여 토큰을 돌려받을 수 있다. 하지만 이 함수는 중재자만 호출 가능하며, initiateDispute 호출도 실패했으므로 토큰을 돌려받는 것은 불가하다.

function resolveDispute(uint256 buyerAward) external onlyArbiter nonReentrant inState(State.Disputed) {
    /// @audit this function will always revert unless initiateDispute is called by buyer or seller
    uint256 tokenBalance = i_tokenContract.balanceOf(address(this));
    uint256 totalFee = buyerAward + i_arbiterFee; // Reverts on overflow
    if (totalFee > tokenBalance) {
        revert Escrow__TotalFeeExceedsBalance(tokenBalance, totalFee);
    }
 
    s_state = State.Resolved;
    emit Resolved(i_buyer, i_seller);
 
    if (buyerAward > 0) {
        i_tokenContract.safeTransfer(i_buyer, buyerAward);
    }
    if (i_arbiterFee > 0) {
        i_tokenContract.safeTransfer(i_arbiter, i_arbiterFee);
    }
    tokenBalance = i_tokenContract.balanceOf(address(this));
    if (tokenBalance > 0) {
        i_tokenContract.safeTransfer(i_seller, tokenBalance);
    }
}

따라서 중재자가 없다면 토큰을 이동시킬 수 있는 방법은 confirmReceipt를 호출해 거래를 성립하는 것 뿐이다.

Impact

중재자를 설정하지 않은 경우 구매자가 거래를 원하지 않더라도 토큰을 회수할 수 없다.

Mitigation

중재자가 없는 경우 구매자가 토큰을 회수할 수 있는 함수를 추가한다. 단, 즉시 출금할 수는 없도록 지연을 두자.

Memo

자금 흐름, 특히 가능한 시나리오와 그렇지 않은 시나리오에 주의를 기울이자.


tags: bughunting, codehawks, smart contract, solidity, business-logic-vul, asset lock, severity medium