# How did this contract got hacked?

**URL:** <https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573>\
**Category:** Security\
**Created:** [September 7, 2024, 3:14pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573 "2024-09-07T15:14:39Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![pmk](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/pmk/32/2097_2.png) [@pmk](https://forum.openzeppelin.com/u/pmk)\
**Post date:** [September 7, 2024, 3:14pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/1 "2024-09-07T15:14:39Z")

</div>

Hi,

This contract got hacked, and I don't see how it happened, here is the contract, and one of the hacker transactions:

```auto
// SPDX-License-Identifier: Propietary
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract CastleVampiresGame is Ownable(0x2867d29563BB9c011AA82E2718146Ba1a7070955), ReentrancyGuard {

    IERC20 public tokenAddress; 
    
    mapping(address => bool) public wonLottery;  
    mapping(address => uint8) public tierReward;  

    mapping(address => bool) public isInGame;
    mapping(address => uint256) public ticketExpiration;   
    
    uint256 public tier3Amount = 0.0175 ether;
    uint256 public tier2Amount = 0.015 ether;
    uint256 public tier1Amount = 0.01 ether;
    uint256 public ticketCost = 0.01 ether;
    uint256 public tokensNeededToPlay = 1 ether;

    //The numbers that will decide what tier rewards will be given
    uint8 public oddsToWin = 7;
    uint8 public tier1Limit = 60;
    uint8 public tier2Limit = 94;

    event Claimed(address player ,uint amount);
    event DiceResult(address player ,bool result);
    event TierWon(address player ,uint8 tier);
    event Dice(uint256 diceNum);
    event Tier(uint8 tierNum);
    event GameEntered(address player); 

    constructor(address _tokenAddress) {
        tokenAddress = IERC20(_tokenAddress);       
    }

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    function EnterGame() external payable nonReentrant{
        require(msg.value == ticketCost, "Incorrect amount");
        require(getPlayerTokenBalance() >= tokensNeededToPlay, "More tokens needed in wallet to play");        
        isInGame[msg.sender] = true;
        ticketExpiration[msg.sender] = block.timestamp + 6 minutes;
        emit GameEntered(msg.sender);       
    } 

    //claim amount to send in wei
    function claim() public nonReentrant {        
        require (address(this).balance >= 0 , "Not enough funds");
        require (wonLottery[msg.sender], "Can only claim if won the dice lottey");                      
               
        wonLottery[msg.sender] = false;

        address payable _to = payable(msg.sender);

        if (tierReward[msg.sender] == 1) {            
            bool sent = _to.send(tier1Amount);
            require(sent, "Failed to send Ether");
            emit Claimed(msg.sender,tier1Amount);            
        } else if (tierReward[msg.sender] == 2) {             
            bool sent = _to.send(tier2Amount);
            require(sent, "Failed to send Ether");
            emit Claimed(msg.sender,tier2Amount);          
        } else if (tierReward[msg.sender] == 3) {            
            bool sent = _to.send(tier3Amount);
            require(sent, "Failed to send Ether");
            emit Claimed(msg.sender,tier3Amount);          
        }    

        tierReward[msg.sender] = 0;        
    }    

    function getNumberWin() private returns (uint8){       
       bytes32 numberR = keccak256(abi.encodePacked(msg.sender, block.prevrandao, block.timestamp));
       uint256 convertedNumber = uint256(numberR);
       uint8 lastDigit = uint8(convertedNumber % 10);      
       emit Dice(convertedNumber);
       return (lastDigit);
    }

    function getNumberTier() private view returns (uint8) {         
        uint numberR = block.prevrandao + block.timestamp / 2;
        // Get the last two digits directly
        uint8 lastTwoDigits = uint8(numberR % 100);
        return lastTwoDigits;
    }

    function throwDice() public {
    require (!wonLottery[msg.sender], "You already won, go and claim your prize");    
    require(isInGame[msg.sender] == true, "Not in game");        
    require(block.timestamp < ticketExpiration[msg.sender] , "Ticket expired"); 
       if (getNumberWin() < oddsToWin){
            wonLottery[msg.sender] = true;
            diceTier();
            emit DiceResult(msg.sender, true);          
        } else {  
            emit DiceResult(msg.sender, false);
        } 
    isInGame[msg.sender] = false;
    }

    function diceTier() private returns (string memory){    

        uint8 result = getNumberTier();
        emit Tier(result);
        if (result <= tier1Limit) {
            emit TierWon(msg.sender, 1);
            tierReward[msg.sender] = 1;
            return "Tier 1";
        } else if (result > tier1Limit && result <= tier2Limit) {
            emit TierWon(msg.sender, 2);
            tierReward[msg.sender] = 2;
            return "Tier 2";
        } else if (result > tier2Limit) {
            emit TierWon(msg.sender, 3);
            tierReward[msg.sender] = 3;
            return "Tier 3";
        } else {
            emit TierWon(msg.sender, 1);
            return "Tier1";
        }
    }

    // new amount to claim in wei units
    function updateTier3Amount(uint256 newAmount) external onlyOwner {
       tier3Amount = newAmount;
    }

    function updateTier2Amount(uint256 newAmount) external onlyOwner {
       tier2Amount = newAmount;
    }

    function updateTier1Amount(uint256 newAmount) external onlyOwner {
       tier1Amount = newAmount;
    }

    //Change the difficulty of the tiers
    function updateTier1Limit(uint8 newLimit) external onlyOwner {
       tier1Limit = newLimit;
    }

    function updateTier2Limit(uint8 newLimit) external onlyOwner {
       tier2Limit = newLimit;
    }

    function updateOddsToWin(uint8 newOdds) external onlyOwner {
       oddsToWin = newOdds;
    }

    //Change the amount of tokens needed to be on the wallet to play, in wei units
    function updateAmountOfTokensToPlay(uint256 newLimit) external onlyOwner {
       tokensNeededToPlay = newLimit;
    }  

    function getPlayerTokenBalance() public view returns (uint256){
       return tokenAddress.balanceOf(address(msg.sender));
    }

    function updateERC20TokenAddress (address _tokenAddress) external onlyOwner {
        tokenAddress = IERC20(_tokenAddress);
    }    

    //Withdraw Eth
    function withdraw() external onlyOwner {        
        payable(msg.sender).transfer(address(this).balance);        
    }

}

```

> **[Ethereum Transaction Hash (Txhash) Details | Etherscan](https://etherscan.io/tx/0x4fcc52d3ce4e909bda851d5aa3b9c671f70c8ba5a2270a22df7fc963921d46e4)**
>
> Ethereum (ETH) detailed transaction info for txhash 0x4fcc52d3ce4e909bda851d5aa3b9c671f70c8ba5a2270a22df7fc963921d46e4. The transaction status, block confirmation, gas fee, Ether (ETH), and token transfer are shown.

---

<div class="post-metadata">

**Author:** ![pmk](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/pmk/32/2097_2.png) [@pmk](https://forum.openzeppelin.com/u/pmk)\
**Post date:** [September 9, 2024, 3:22pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/3 "2024-09-09T15:22:47Z")

</div>

In specific, all the ETH balance got drained.

---

<div class="post-metadata">

**Author:** ![Cainuriel](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/cainuriel/32/5410_2.png) [@Cainuriel](https://forum.openzeppelin.com/u/Cainuriel)\
**Post date:** [September 13, 2024, 3:28pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/4 "2024-09-13T15:28:40Z")

</div>

Have you totally ruled out access to or theft of the project's private keys that could drain the game?

---

<div class="post-metadata">

**Author:** ![Cainuriel](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/cainuriel/32/5410_2.png) [@Cainuriel](https://forum.openzeppelin.com/u/Cainuriel)\
**Post date:** [September 13, 2024, 3:39pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/5 "2024-09-13T15:39:06Z")

</div>

The main problem with your contract, and possible cause of the hack, is the use of onChain randomness.

In the early days of the Ethereum network the first thing that was invented was gambling. From that moment on, the techniques of guessing by miners of those numbers that cannot give randomness due to the deterministic nature of the network were developed.

This means that any malicious miner who detects your contract knows how to guess that number.

You have to use an oracle.

Other than that:

1. Controlled reentrancy, but the use of send() is insecure.
2. Lack of ERC20 token balance control during the whole game.
3. Lack of proper verification of contract funds before making payments.

My opinion is that the potential for manipulation by a miner is remarkable. I believe that was the cause.

---

<div class="post-metadata">

**Author:** ![pmk](https://sea2.discourse-cdn.com/flex022/user_avatar/forum.openzeppelin.com/pmk/32/2097_2.png) [@pmk](https://forum.openzeppelin.com/u/pmk)\
**Post date:** [September 15, 2024, 4:50pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/6 "2024-09-15T16:50:05Z")

</div>

Thanks for the breakdown.

How would a contract that needs a random number would do it then?, without having to pay Chainlink $20 per request?

---

<div class="post-metadata">

**Author:** ![lemon](https://avatars.discourse-cdn.com/v4/letter/l/d2c977/32.png) [@lemon](https://forum.openzeppelin.com/u/lemon)\
**Post date:** [January 16, 2025, 11:47pm UTC](https://forum.openzeppelin.com/t/how-did-this-contract-got-hacked/41573/7 "2025-01-16T23:47:00Z")

</div>

You're asking about the risk of predicting the random number generated by the `getNumberWin()` function. This is crucial for the fairness and security of the game.

```auto
function getNumberWin() public view returns (uint8){       
    bytes32 numberR = keccak256(abi.encodePacked(msg.sender, block.prevrandao, block.timestamp));
    uint256 convertedNumber = uint256(numberR);
    uint8 lastDigit = uint8(convertedNumber % 10);      
    return (lastDigit);
}

```

**Risk Analysis:**

This function uses `keccak256(abi.encodePacked(msg.sender, block.prevrandao, block.timestamp))` to generate a "random" number. This method is _not secure_ on the blockchain and is vulnerable to prediction. The main reasons are:

1. **Predictability of `block.prevrandao`:** `block.prevrandao` is the random number from the previous block, provided by the miner. While theoretically miners would need significant hash power to manipulate this value, in practice, especially on smaller or less decentralized networks, miners _could_ influence `block.prevrandao` by controlling a portion of the hash power. This would allow them to influence the generated random number.
2. **Limited Manipulability of `block.timestamp`:** `block.timestamp` is the timestamp of the current block, also set by the miner. Although the Ethereum protocol imposes limits on how much the timestamp can be adjusted, miners still have some control over it. This provides a window for predicting or manipulating the random number.
3. **`msg.sender` is Known:** `msg.sender` is the address of the user calling the function. This is public information known to everyone.

**Combining these factors, the random number generated by `keccak256(abi.encodePacked(msg.sender, block.prevrandao, block.timestamp))` is _not truly random_ and is susceptible to prediction or manipulation.**

**Level of Risk:**

- **On Ethereum Mainnet:** Due to the highly distributed hash power of the Ethereum mainnet, it's very difficult for a single miner or pool to control `block.prevrandao`. Therefore, directly manipulating the random number is more challenging. However, a non-zero risk still exists, especially in unforeseen circumstances like a sudden consolidation of hash power.
- **On Testnets or Private Networks:** On testnets or private networks, where hash power is more concentrated, controlling `block.prevrandao` is much easier. The risk of random number prediction or manipulation is _very high_.
- **For Malicious Actors:** If an attacker controls a significant portion of the network's hash power or can discover patterns in `block.prevrandao`, they could exploit this information to predict the output of `getNumberWin()` and cheat in the game.
