Beginner FriendlyGameFi
100 EXP
View results
Submission Details
Severity: medium
Valid

Predictable Randomness via Timestamp Allows Manipulation

Predictable Randomness via Timestamp Allows Manipulation

Description

Describe the normal behavior:
The get_random_slice() function assigns a pseudo-random reward between 100 and 500 APT to users during registration, using the current blockchain timestamp.

Explain the specific issue or problem:
Because the function relies solely on timestamp::now_microseconds(), the output is predictable and deterministic within each block, allowing attackers to manipulate registration timing and consistently extract maximum-value rewards.

#[randomness]
entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
// Timestamp-based randomness is deterministic per block and manipulable
@> let time = timestamp::now_microseconds();
let random_val = time % 401;
let random_amount = 100 + random_val; // 100–500 APT
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}

Risk

Likelihood:

  • The vulnerability is likely to occur whenever multiple users are registered in the same block, since timestamp::now_microseconds() returns the same value within that block, resulting in identical "random" rewards.

  • Attackers can brute-force registration timing by scripting wallet interactions and repeatedly calling the register_pizza_lover() function to find high-reward timestamps and farm multiple accounts in that window.

Impact:

  • The attacker can manipulate the airdrop to consistently claim high-value rewards, extracting disproportionate value and undermining fairness.

  • Over time, this can lead to centralised token distribution, financial loss for the project, and reputational damage due to perceived manipulation or unfairness.

Proof of Concept

This vulnerability was demonstrated using the following unit test, which shows that reward values are entirely determined by the current timestamp, resulting in identical outcomes for multiple users registered within the same logical time window:

This test registers two separate users in quick succession and retrieves the reward amount assigned to each. Since both registrations occur within the same microsecond timestamp window - the randomness function (get_random_slice()) returns the same value to both users. The final assertion confirms this determinism.
By extension, an attacker could simulate this pattern off-chain, identify when a high-value reward (e.g., 500 APT) would be granted, and then register multiple wallets during that same timestamp window to extract maximum value - fully bypassing the intended randomness of the airdrop.

#[test(deployer = @pizza_drop, user1 = @0xabc, user2 = @0xdef, framework = @0x1)]
fun test_timestamp_based_randomness(deployer: &signer, user1: &signer, user2: &signer, framework: &signer) acquires State, ModuleData {
use aptos_framework::timestamp;
timestamp::set_time_has_started_for_testing(framework);
// Register two users back-to-back
register_pizza_lover(deployer, signer::address_of(user1));
register_pizza_lover(deployer, signer::address_of(user2));
// Fetch assigned amounts
let amt1 = get_claimed_amount(signer::address_of(user1));
let amt2 = get_claimed_amount(signer::address_of(user2));
// Assert identical slice amounts
assert!(amt1 == amt2, 999);
}

Recommended Mitigation

Below are two recommended mitigation strategies, shown in pseudocode with inline comments.

- // Vulnerable randomness source
- let time = timestamp::now_microseconds();
- let random_val = time % 401;
- let random_amount = 100 + random_val;
+ // Mitigation Option 1: Commit-Reveal scheme (pseudo-code)
+ // Phase 1: User submits commitment = hash(address + nonce)
+ // Phase 2: User reveals (address, nonce) and random amount is derived from hash
+ let commitment = sha3_256(address + nonce);
+ let random_val = commitment % 401;
+ let random_amount = 100 + random_val;
+ // Mitigation Option 2: Use a VRF oracle (preferred)
+ // Integrate with Chainlink VRF or other trusted randomness oracle
+ // Fetch randomness from VRF before assigning airdrop value
+ let random_val = VRF::get_random(); // placeholder call
+ let random_amount = 100 + (random_val % 401);
Updates

Appeal created

bube Lead Judge 9 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Predictable randomness

The `get_random_slice` function should only be called by the owner via the `register_pizza_lover` function. Also, the `owner` is trusted and will not choose a specific time for a new user to register. Therefore, I disagree with the claim of most reports in this group that an attacker can manipulate the random number of pizza slices. But I agree with the root cause of the reports in this group, that the random distribution is not completely random.

Support

FAQs

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