본문 바로가기

자주나오는패턴

[Common Vulnerabilities] DoS with Failed Call

컨트랙트를 외부에서 호출하는 것은 실수로 또는 고의로 실패할 수 있고 이로 인해 컨트랙트에 DoS의 조건이 생길 수 있다. 이러한 실패로 인한 피해를 최소화하려면 각 외부 호출을 호출 수신자가 시작할 수 있는 자체 트랜잭션으로 분리하는 것이 좋다. 이는 특히 결제와 관련이 있으며, 사용자가 자동으로 자금을 푸시하는 것보다 자금을 인출하는 것이 더 좋다(이는 또한 가스 한도 문제가 발생할 가능성을 줄인다)

 

그것을 방지하기 위해서는 

1. 하나의 트랜잭션에 여러 가지 call을 피한다. 특히 반복되는 call을 실행하는 것을.

2. 항상 외부 호출이 실패할 수 있음을 가정해야 한다.

3. call이 실패를 다루는 컨트랙트 로직을 실행해야 한다. 

 

// bad
contract auction {
    address highestBidder;
    uint highestBid;

    function bid() payable {
        require(msg.value >= highestBid);

        if (highestBidder != address(0)) {
            (bool success, ) = highestBidder.call.value(highestBid)("");
            require(success); // if this call consistently fails, no one else can bid
        }

       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

여기서 bid함수에서 call을 하는데 실패하면 아무도 bid를 하지 못하니까. bid랑 돈을 받는 함수랑 따로 분리를 하는 게 맞다.

 

// good
contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;

    function bid() payable external {
        require(msg.value >= highestBid);

        if (highestBidder != address(0)) {
            refunds[highestBidder] += highestBid; // record the refund that this user can claim
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        (bool success, ) = msg.sender.call.value(refund)("");
        require(success);
    }
}

 

[ 참고 및 추가자료 ]

1. https://swcregistry.io/docs/SWC-113/
2. https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#favor-pull-over-push-for-external-calls