Beginner FriendlyGameFi
100 EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Balance Desynchronization Report

Root + Impact

Description

  • The PizzaDrop protocol maintains internal balance tracking through the state.balance variable, which is updated only when the owner funds the contract via fund_pizza_drop(). This tracked balance is used for allocation validation and pool management decisions. The protocol assumes that state.balance accurately represents the actual APT available in the resource account.

  • External users can directly transfer APT to the resource account address, bypassing the internal accounting system entirely. Since the resource account address is deterministic and publicly calculable, any APT sent directly via coin::transfer<AptosCoin>() increases the actual balance without updating state.balance. This creates a permanent desynchronization where externally transferred funds become untrackable and effectively locked within the protocol.


The vulnerability stems from the dual balance tracking system in the protocol:

public entry fun fund_pizza_drop(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();
coin::transfer<AptosCoin>(owner, resource_addr, amount); @> // Increases actual balance
state.balance = state.balance + amount; @> // Updates tracked balance
}
// However, external transfers bypass this update:
// coin::transfer<AptosCoin>(external_user, resource_addr, amount); @> // Only increases actual balance
// state.balance remains unchanged @> // No internal accounting update

Risk

Likelihood:

  • Resource account addresses are deterministic and publicly calculable using create_resource_account(deployer, b"pizza_drop"), making the target address discoverable by any user analyzing the protocol.

  • External integrations, airdrops, or user mistakes commonly result in direct token transfers to contract addresses, especially when users interact with multiple DeFi protocols simultaneously.

Impact:

  • Permanent fund lock occurs when external APT transfers bypass internal accounting, creating unrecoverable funds that exist in the resource account but remain invisible to protocol operations and forever excluded from distribution mechanisms.

  • Protocol operational failure results from inaccurate balance reporting through get_pizza_pool_balance(), leading to incorrect administrative decisions, user interface display errors, and potential inability to assess actual protocol reserves.

Proof of Concept


The following test demonstrates how external APT transfers create permanent balance desynchronization:

#[test(deployer = @pizza_drop, external_user = @0x456, framework = @0x1)]
fun test_h3_balance_desync_vulnerability(
deployer: &signer,
external_user: &signer,
framework: &signer
) acquires State, ModuleData {
// Test setup code omitted for brevity
// Initial proper funding
fund_pizza_drop(deployer, 10000);
let initial_tracked = get_pizza_pool_balance(); // 10000
let initial_actual = get_actual_apt_balance(); // 10000
// Calculate resource account address (publicly available)
let resource_addr = get_resource_address();
// External user sends APT directly to resource account
coin::transfer<AptosCoin>(external_user, resource_addr, 5000);
let final_tracked = get_pizza_pool_balance(); // Still 10000 - unchanged
let final_actual = get_actual_apt_balance(); // Now 15000 - increased
// Verify permanent desynchronization
assert!(final_tracked == 10000, 1); // Internal tracking unchanged
assert!(final_actual == 15000, 2); // Actual balance increased
assert!(final_actual != final_tracked, 3); // Permanent desynchronization
}

Recommended Mitigation


Implement balance synchronization mechanisms to handle external transfers:

public entry fun claim_pizza_slice(user: &signer) acquires ModuleData, State {
let user_addr = signer::address_of(user);
let state = borrow_global_mut<State>(get_resource_address());
+ // Synchronize tracked balance with actual balance before operations
+ let actual_balance = coin::balance<AptosCoin>(get_resource_address());
+ state.balance = actual_balance;
assert!(table::contains(&state.users_claimed_amount, user_addr), E_NOT_REGISTERED);
assert!(!table::contains(&state.claimed_users, user_addr), E_ALREADY_CLAIMED);
let amount = *table::borrow(&state.users_claimed_amount, user_addr);
- assert!(state.balance >= amount, E_INSUFFICIENT_FUND);
+ assert!(actual_balance >= amount, E_INSUFFICIENT_FUND);
// Rest of function unchanged
}
Updates

Appeal created

bube Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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