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

Predictable Rewards Through Timestamp Manipulation

Predictable Rewards Through Timestamp Manipulation

Description

  • The Pizza Drop contract randomly assigns rewards between 100-500 APT to registered users.

  • The contract uses block timestamp modulo for randomness generation, which is predictable and can be manipulated to assign specific reward amounts.

#[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 (in Octas: 10^8 smallest unit)
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}

Risk

Likelihood:

  • Block timestamps can be predicted with high accuracy

  • Validators can manipulate block timestamps within reasonable bounds

  • The modulo operation makes specific outcomes easily targetable by timing transactions

  • The randomness pattern is fully deterministic based on block inclusion time

Impact:

  • Fair random distribution of rewards is compromised

  • Owner can time registrations to assign high or low rewards to certain users

  • Validators can manipulate timestamps to benefit specific addresses

Proof of Concept

The following test demonstrates how the timestamp-based randomness can be manipulated to consistently get either minimum (100) or maximum (500) rewards:

#[test(deployer = @pizza_drop, user1 = @0x123, user2 = @0x456, framework = @0x1)]
fun test_timestamp_manipulation(deployer: &signer, user1: &signer, user2: &signer, framework: &signer) acquires State, ModuleData {
use aptos_framework::timestamp;
let user1_addr = signer::address_of(user1);
let user2_addr = signer::address_of(user2);
// Initialize timestamp for testing
timestamp::set_time_has_started_for_testing(framework);
let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(framework);
let deployer_coins = coin::mint<AptosCoin>(100000, &mint_cap); // 100000 APT
coin::deposit<AptosCoin>(@pizza_drop, deployer_coins);
init_module(deployer);
fund_pizza_drop(deployer, 50000); // 50000 APT
// Test Case 1: Set timestamp to get minimum amount (100)
// We want timestamp % 401 = 0
timestamp::update_global_time_for_test(4010000); // Using a multiple of 401
register_pizza_lover(deployer, user1_addr);
let amount1 = get_claimed_amount(user1_addr);
assert!(amount1 == 100, 1); // Should get minimum amount
// Test Case 2: Set timestamp to get maximum amount (500)
// We want timestamp % 401 = 400
timestamp::update_global_time_for_test(4010000 + 400); // timestamp % 401 = 400
register_pizza_lover(deployer, user2_addr);
let amount2 = get_claimed_amount(user2_addr);
assert!(amount2 == 500, 2); // Should get maximum amount
// Clean up
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}

This test shows that:

  1. By setting timestamp to a multiple of 401, we get minimum reward (100)

  2. By setting timestamp to (multiple of 401) + 400, we get maximum reward (500)

  3. Validators can manipulate timestamps to target specific reward amounts

  4. Owner can time their transactions to target higher or lower rewards

Recommended Mitigation

The vulnerability requires two key changes:

  1. Replace timestamp-based randomness with Aptos's secure randomness module:

+ 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_amount = 100 + random_val; // 100-500 APT
+ let random_val = randomness::u64_range(1, 6);
+ let random_amount = random_val * 100; // 100-500 APT (in Octas: 10^8 smallest unit)
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
  1. Remove public visibility from register_pizza_lover function to prevent unsafe exposure and add the #[randomness] attribute which is required for entry functions calling randomnesss features:

+ #[randomness]
+ entry fun register_pizza_lover(owner: &signer, user: address) acquires ModuleData, State {
- public entry fun register_pizza_lover(owner: &signer, user: address) acquires ModuleData, State {
let state = borrow_global_mut<State>(get_resource_address());
assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
get_random_slice(user);
event::emit(PizzaLoverRegistered { user });
}

Impact of Changes

  1. Uses Aptos's secure randomness API instead of predictable timestamps

  2. Prevents timestamp manipulation attacks

  3. Ensures fair distribution of rewards

  4. Properly annotates randomness usage with #[randomness] attribute

  5. Ensures safe randomness by not exposing it in a public function.

This change ensures that reward amounts cannot be predicted or manipulated by validators or users, maintaining the fairness of the airdrop distribution.

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.