Ethernaut 9. King

독수리박박·2024년 7월 15일
0
post-thumbnail

Ehternaut level9 King

Problem


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {
    address king;
    uint256 public prize;
    address public owner;

    constructor() payable {
        owner = msg.sender;
        king = msg.sender;
        prize = msg.value;
    }

    receive() external payable {
        require(msg.value >= prize || msg.sender == owner);
        payable(king).transfer(msg.value);
        king = msg.sender;
        prize = msg.value;
    }

    function _king() public view returns (address) {
        return king;
    }
}

이 문제는 이 시스템을 붕괴(?) 시키는 것 입니다. 시스템이 더 이상 정상적인 작동을 못하도록 하면 되는 것입니다.

컨트랙트를 살펴보면 생성됨과 동시에 owner, king, prize가 정해지는걸 확인할 수 있습니다. 그리고 receive 함수를 보면 owner이거나 prize보다 높은 ETH를 전송한다면 기존의 king에게 해당 ETH를 전송하고 새로운 king이 될 수 있습니다.

시스템을 붕괴시킨다 == 더 이상 정상적인 작동을 불가능하게 만든다.

어떻게 하면 해당 컨트랙트가 더 이상 정상적으로 작동하지 못하게 할 수 있을까요...

Solution


이 시스템은 누군가가 왕이 되고 새로운 왕이 나타난다면 보상을 받고 물러나도록 되어있습니다.

이게 문제 컨트랙트의 거의 유일한 기능이라 할 수 있습니다.

그렇다면 이 기능이 더 이상 작동하지 못하도록 하면 됩니다.

2가지 방법이 존재합니다.

  • 아무도 나보다 더 높은 prize를 제시하지 못하도록 엄청난 금액을 prize로 제출
  • 누군가 새로운 왕이 되기 위해 prize를 제출할 때 받기를 거부!

첫 번째 방법은 현실성이 없습니다.. 물론 할 수 있긴 하지만 문제의 의도랑은 조금 맞지 않는거 같습니다. 돈자랑 하는 곳이 아니기 때문입니다..!

그렇다면 어떻게 prize를 받는 것을 거부할 수 있을까요.

일단 king이 EOA라면 거부하지 못합니다. EOA는 돈을 받을때는 특정 조건을 걸지 못하기 때문입니다. 프로그래밍이 불가능합니다..!(metamask 기준)

하지만 CA 즉 컨트랙트는 해당 조건을 프로그래밍할 수 있습니다. CA는 receive, fallback 같은 함수로 ETH를 받을 때 특정 조건을 걸거나 구현하지 않는다면 안받을 수 있습니다..!

기존에는 fallback만 존재했지만 현재에는 receive와 fallback으로 나뉘었습니다.
두 함수 모두 external 키워드가 필수입니다.

  • fallback: 현재는 컨트랙트에 존재하지 않는 함수가 호출될 때 실행되고 있습니다. 존재하지 않는 함수에 대한 예외 처리이기 때문에 payable를 사용해도, 안해도 됩니다.(함수에서 ETH를 보낼 때, calldata 존재)
  • receive: payable 키워드를 반드시 사용해야 합니다. 이더를 받을 때 호출되며 call, send, transfer 같은 전송 함수에 의해 실행됩니다. (calldata 없이 순수하게 ETH 전송 시 호출)

그렇다면 공격 컨트랙트를 작성하고 위 함수를 구현하지 않거나 내부에 revert()로 트랜잭션을 거부하는 방법을 사용하면 됩니다..!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Attack {
    constructor(address payable target) payable  { 
       (bool success,) = target.call{value: msg.value}("");
       require(success, "failed");
    }

    receive() external payable {
       revert();
    }
}

contract.prize()를 호출해 prize를 알아내고 해당 가격 이상의 prize를 같이 전송해주면 됩니다.

저는 좀 더 알아보기 쉽게 receive 내부에서 revert하고 있도록 구현했습니다.

이외에도 공격함수를 따로 구현하거나 King instance를 만들거나 여러가지 방법으로 공격 컨트랙트를 작성 가능합니다.

Conclusion


receive, fallback 함수는 컨트랙트의 특성에 따라서 잘 사용해야 합니다. 가스비 제한도 존재하기 때문에 내부에 너무 많은 동작을 집어 넣을 시 컨트랙트가 ETH를 받지 못할 수도 있습니다.

그리고 문제의 컨트랙트에서는 king을 오직 EOA만 가능하게 하던가 아니라면 전송할 때 상대 컨트랙트에 ETH를 받을 수 있는 장치가 있는지 확인하는 safeTransfer 같은 함수를 사용하는 것이 좋아보입니다.

0개의 댓글