[Solidity] Selfdestruct & Forcefully send Ether

임형석·2023년 10월 5일
0

Solidity


selfdestruct

selfdestruct 는 전역함수로, 어떤 컨트랙트에서도 사용할 수 있다.

말그대로 스스로 붕괴시키다 라는 의미인데, 이 전역함수를 사용하면 사용하는 컨트랙트에 있는 이더를 지정하는 주소로 이전시킨 후, 컨트랙트를 비활성화 시킨다.

테스트넷에 아래와 같이 테스트 컨트랙트를 배포하고 직접 사용해보았다.

contract test_selfdestruct{
    uint a = 1;
    
    receive() external payable { }

    function getA() public view returns(uint) {
        return a;
    }

    function plusA() public {
        a += 1;
    }

    function sendEther() public payable {
        payable(address(this)).transfer(msg.value);
    }

    function selfDestruct(address _address) public {
        address payable addr = payable(address(_address));
        selfdestruct(addr);
    }
}

plusA 함수로 상태변수 a 를 2로 만들고, 0.001 Eth 를 컨트랙트에 보낸 후, selfdestruct 를 해보았다.

모든 이더가 지정한 주소로 전송되었고, 상태변수가 0으로 초기화되어 있다.

이 상태에서 다시한번 plusA, sendEther 함수를 사용해보았는데 트랜잭션이 정상적으로 처리는 되었지만, 컨트랙트의 상태변수가 먹통이 되어 0으로 변함이 없었다.

또한 selfdestruct 함수를 다시 사용해보았고 트랜잭션도 정상적으로 처리되었다. 하지만, 컨트랙트 내부의 이더는 그대로. 상태변수 역시 0으로 변함이 없었다.

말그대로 죽은 컨트랙트가 된 것이다.


리믹스에서 컨트랙트를 작성하다보면 아래와 같이 경고문구가 뜬다.

"selfdestruct" 연산의 사용을 비추천하고 있으며 이에 대한 변경 사항을 예고하고 있다.

그리고 이것을 이용해서 컨트랙트에 대한 공격을 진행할 수 있다.


Ether 강제로 보내기

위에서 설명한 selfdestruct 를 이용해서 강제로 이더를 보낼 수 있다.

그리고 경우에 따라 치명적인 공격이 될 수도 있다.

컨트랙트 코드로 아래의 예시를 보면..

contract EtherGame {
    uint public targetAmount = 7 ether;
    address public winner;

    function deposit() public payable {
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }

    function claimReward() public {
        require(msg.sender == winner, "Not winner");

        (bool sent, ) = msg.sender.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }
}

이더게임의 참여를 원하는 참가자는 1이더를 컨트랙트에 보낼 수 있다.

컨트랙트는 7번째 이더를 받는 순간, 보낸 사람을 당첨자로 선정.

당첨자는 claimReward 함수를 이용하여 7이더를 받아갈 수 있다.


위의 경우에 치명적인 공격이 될 수 있다.

예를 들어, 누군가가 selfdestruct 를 이용해서 EtherGame 컨트랙트에 강제로 이더를 전송해서 balance 를 7이더 이상으로 만들어 버린다면?

아래의 Attack 컨트랙트 코드를 예시로 본다면..

contract Attack {
    EtherGame etherGame;

    constructor(EtherGame _etherGame) {
        etherGame = EtherGame(_etherGame);
    }

    function attack() public payable {
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

attack 함수에 포함된 selfdestruct 로 7이더 이상을 EtherGame 컨트랙트로 보내버리면?

require(balance <= targetAmount, "Game is over"); 부분에서 balance 가 targetAmount 인 7이더보다 많아지기에 게임의 참여를 원하는 사람의 입금이 불가능 할 것이다.

정확히 7이더가 모이면 당첨자를 추첨하는데, selfdestruct 로 이더를 받을 경우 우승자를 추첨할 수도 없고, 7이더 이상 balance 가 남아버려 새로운 참가자도 받을 수 없다.

이 컨트랙트는 사실상 죽은 컨트랙트가 되어버렸다. 심지어 남아있는 이더를 회수할 수도 없다.

아래 사진과 같이 7이더가 입금되었음에도 우승자가 추첨되어 있지 않다.


selfdestruct 공격 방어

이전에는 uint balance = address(this).balance; 로 처리되어 있어 현재 컨트랙트가 가지고 있는 이더 갯수를 그대로 인식했다.

하지만 balance 라는 상태변수를 새로 선언한 후, balance += msg.value; 로 고쳐준다면 deposit 함수를 통해 입금한 이더만을 계산할 것이다.

아래와 같이 변경하면 된다.

contract EtherGame {
    uint public targetAmount = 7 ether;
    address public winner;
    // balance 상태변수 선언
    uint balance;

    function deposit() public payable {
        require(msg.value == 1 ether, "You can only send 1 Ether");

        // deposit 을 통해 입금한 이더만을 계산
        balance += msg.value;
        require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }

    function claimReward() public {
        require(msg.sender == winner, "Not winner");

        (bool sent, ) = msg.sender.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }
}

0개의 댓글