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

Predictable timestamp-based “randomness” enables payout gaming; combined with mass registration it accelerates pool drain

Root + Impact

Description

  • Normal behavior:
    Each registered user should receive a random slice size in the advertised range (100–500), where “random” means unpredictable to users and not steerable by timing.

  • Issue:
    The assigned slice is computed as 100 + (timestamp::now_microseconds() % 401), which is fully deterministic from the current time. An attacker can time or spam registrations to bias toward high values (near 500). When combined with easy address creation (and especially with H1 if not fixed), this enables systematic payout maximization and accelerates pool depletion.

#[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(); // fully deterministic from the current time
@> let random_val = time % 401; // not so random
@> let random_amount = 100 + random_val; // deterministic: 100 + (now % 401)
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}

Risk

Likelihood:

  • The output depends solely on local time; attackers can control when they call registration to hit favorable residues modulo 401 (e.g., target 500 by choosing now % 401 == 400).

  • There is no rate limit, commit-reveal, or other entropy source to resist timing/spam strategies.

  • Even with owner-only registration, the owner (or colluding parties) can still time registrations, breaking fairness.

Impact:

  • Payout gaming / fairness breach: Attackers (or favored parties) systematically harvest higher amounts than honest users by timing calls.

  • Accelerated pool depletion: In combination with mass address creation (Sybil behavior), average payout per claimed address increases → the pool drains faster.

  • Forward escalation: If the advertised 100–500 refers to APT (as per spec) and amounts are later scaled to APT (×1e8), this predictability becomes a high-severity financial issue.

Proof of Concept

#[test(deployer = @pizza_drop, user1 = @0x111, framework = @0x1)]
fun poc_timestamp_randomness_is_predictable(
deployer: &signer,
user1: &signer,
framework: &signer
) acquires State, ModuleData {
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
use aptos_framework::account;
use aptos_framework::coin;
// Enable test time and init coin testing caps
timestamp::set_time_has_started_for_testing(framework);
let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(framework);
// Accounts + module init
account::create_account_for_test(@pizza_drop);
account::create_account_for_test(signer::address_of(user1));
init_module(deployer);
// Pick a time where (t % 401) == 400 → expected amount = 500
timestamp::update_global_time_for_test(4_950_745); // 401*12_345 + 400
let t = timestamp::now_microseconds();
let u = signer::address_of(user1);
register_pizza_lover(deployer, u);
let amt = get_claimed_amount(u);
// Determinism checks
assert!(amt == 100 + (t % 401), 1);
assert!(amt == 500, 2);
// Clean up capabilities
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}

Recommended Mitigation

- // DO NOT derive "randomness" from time.
- let time = timestamp::now_microseconds();
- let random_val = time % 401;
- let random_amount = 100 + random_val;
+ // Use a proper on-chain randomness source or a commit–reveal scheme.
+ // Examples of safer approaches:
+ // 1) Use the chain's randomness API (e.g., a randomness capability injected via #[randomness])
+ // and derive an unbiased u64, then map to [100, 500] carefully.
+ // 2) Use a commit–reveal flow (user commits a salt; reveal happens later using a block value).
+ // IMPORTANT: Avoid time-based entropy. Consider anti-Sybil/rate-limiting if needed.
+ let r /*: u64 from a secure randomness source */;
+ let random_amount = 100 + (r % 401); // or use a uniform mapping to avoid modulo bias if required
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.