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

Insufficient Fund Planning Causes Permanent Fund Lock

Description

The protocol lacks proper fund management mechanisms to handle the unpredictable nature of random reward amounts. Since rewards are randomly assigned (100-500 APT), the owner cannot accurately predict total funding requirements, leading to two problematic scenarios: under-funding results in legitimate users being unable to claim their allocated rewards, while over-funding results in permanent fund lock for the owner with no recovery mechanism.

Root Cause

The combination of random reward amounts (100-500 APT) and lack of fund recovery mechanisms creates an impossible funding calculation for the owner. The protocol has no mechanism to handle insufficient funds or recover leftover tokens.

public entry fun claim_pizza_slice(user: &signer) acquires ModuleData, State {
// ...
let amount = *table::borrow(&state.users_claimed_amount, user_addr);
// This check will fail if there is not enought fund in the contract
assert!(state.balance >= amount, E_INSUFFICIENT_FUND);
// No alternative mechanism if insufficient funds
transfer_from_contract(user_addr, amount);
state.balance = state.balance - amount;
}
// Missing: No fund recovery or emergency withdrawal functions

Key issues:

  1. Random rewards make funding requirements unpredictable

  2. No mechanism to handle insufficient balance scenarios

  3. No function to withdraw/recover leftover funds

  4. Users with valid registrations cannot claim if funds are insufficient

Risk

Likelihood: High - Happens when owner under/over-funds relative to unpredictable random reward distribution

Impact: Medium - Under-funding locks user rewards, over-funding locks owner funds permanently

Impact

Medium severity because:

  • User fund denial: Legitimate registered users cannot access their allocated rewards

  • Permanent fund lock: No recovery mechanism for stuck tokens in contract

  • Unpredictable requirements: Owner cannot calculate exact funding needs due to randomness

  • Protocol functionality failure: Core claiming mechanism becomes inoperable

Proof of Concept

Demonstrated scenario where insufficient funding locks user rewards:

#[test(deployer= @pizza_drop, user = @0x123, user2 = @0x124, framework = @0x1)]
#[expected_failure(abort_code = E_INSUFFICIENT_FUND)]
fun test_insufficient_funds_lock(deployer: &signer, user: &signer, user2: &signer, framework: &signer) {
// Setup with limited funding in
// Amount will be in Octas: 10^8 smallest unit
// Deliberately low funding to simulate left of after X users already claim
let funding_amount = 336;
// Register users at specific timestamps for predictable amounts
timestamp::update_global_time_for_test(1000000000);
register_pizza_lover(deployer, user_addr); // Gets 335 APT
timestamp::update_global_time_for_test(1000000001);
register_pizza_lover(deployer, user_addr2); // Gets 336 APT
// Total needed: 335 + 336 = 671 APT
// Total funded: 336 APT
// Shortfall: 335 APT
claim_pizza_slice(user); // ✅ Claims 335 APT, remaining: 1 APT
claim_pizza_slice(user2); // ❌ Needs 336 APT but only 1 APT left - FAILS!
// user2's 336 APT reward is now permanently inaccessible
// The 1 APT leftover is also permanently stuck
}

Fund calculation impossibility:

With N users getting random amounts (100-500 APT each):
- Minimum funding needed: N × 100 APT
- Maximum funding needed: N × 500 APT
- Actual funding needed: Unknown until all registrations complete
- Risk of under-funding: High
- Risk of over-funding: Also high, with no recovery mechanism

Note: The current project tests demonstrate that the owner intends to fund the contract before calling register_pizza_lover. An alternative solution could have been to register users first to know their exact reward amounts, then fund the contract accordingly. However, this is not what the current test implementation showcases - the owner must fund upfront without knowing the total required amount.
And we ll still have an issue with unclaimed funds that won't be withdraw by the owner

Recommended Mitigation

Add proper fund management and recovery mechanisms:

+ // Emergency fund recovery function
+ public entry fun emergency_withdraw(owner: &signer, amount: u64) acquires ModuleData, State {
+ let state = borrow_global_mut<State>(get_resource_address());
+ assert!(signer::address_of(owner) == state.owner, E_NOT_OWNER);
+
+ let resource_addr = get_resource_address();
+ let actual_balance = coin::balance<AptosCoin>(resource_addr);
+ assert!(actual_balance >= amount, E_INSUFFICIENT_FUND);
+
+ transfer_from_contract(signer::address_of(owner), amount);
+ state.balance = state.balance - amount;
+ }
public entry fun claim_pizza_slice(user: &signer) acquires ModuleData, State {
// ...
+ // Graceful handling of insufficient funds
+ if (state.balance < amount) {
+ // Give user whatever is left in the pool
+ let available = state.balance;
+ if (available > 0) {
+ transfer_from_contract(user_addr, available);
+ state.balance = 0;
+ };
+ // Update user's claimed amount to reflect partial payment
+ *table::borrow_mut(&mut state.users_claimed_amount, user_addr) = available;
+ } else {
// Normal claiming process
transfer_from_contract(user_addr, amount);
state.balance = state.balance - amount;
+ };
}

Benefits:

  • Fund recovery: Owner can withdraw leftover funds after distribution

  • Graceful degradation: Users get partial payments instead of complete failure

  • Emergency handling: Owner can manage unexpected funding scenarios

  • Protocol sustainability: Prevents permanent fund locks

Updates

Appeal created

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

Lack of withdraw mechanism

Support

FAQs

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