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

Anyone can self-register and set a claimable amount (owner bypass)

Root + Impact

Description

  • The module’s core assumption is that only the owner can register new airdrop participants. However, get_random_slice—the function responsible for inserting claimable amounts into the state—is declared as an entry function, meaning anyone can call it directly.

    Because of this, an attacker can skip the intended owner-only register_pizza_lover flow and insert themselves (or arbitrary addresses they control) into the airdrop state. Once inserted, they can immediately claim funds.

// Root cause in the codebase with @> marks to highlight the relevant section
public 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; // range: 100 - 500 octas
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
  • Entry exposure: Marked as entry, callable by any external signer.

  • No auth check: Lacks assert!(signer::address_of(caller) == state.owner, E_NOT_OWNER) or equivalent.

  • State mutation: Directly modifies users_claimed_amount, the central mapping of allocations.

  • Random allocation: Assigns 100–500 octas regardless of caller identity or intent.

Owner’s intended path

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);
event::emit(PizzaLoverRegistered { user });
}
  • Here, the owner must call register_pizza_lover.

  • That in turn calls get_random_slice.

  • But since get_random_slice itself is public, this indirection is irrelevant.

Risk

Likelihood:

  • Developer intended get_random_slice to be an internal helper but exposed it as callable.

Impact:

  • Loss of funds: Complete theft of the airdrop pool.

  • Bypass of business logic: Owner no longer controls who registers.

Proof of Concept

Exploit 1: Simple Self-Registration

  1. Attacker calls get_random_slice(attacker_addr).
    → Inserts attacker_addr → random_amount (100–500).

  2. Attacker calls claim_pizza_slice(&attacker_signer).
    → Transfers that amount from resource account to attacker.

  3. Repeat for multiple addresses.
    Funds drained until state.balance == 0.

Recommended Mitigation

  • Make get_random_slice a private helper (remove entry):

- public entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
+ fun get_random_slice(user_addr: address) acquires ModuleData, State {
// unchanged body
}
Updates

Appeal created

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