Description
The protocol intends to reward users with 100-500 APT tokens per slice, but due to a missing unit conversion, users receive 100-500 Octas instead. Since 1 APT = 10^8 Octas, users receive rewards that are 100 million times smaller than intended, making claims economically unfeasible due to gas costs exceeding reward values.
Root Cause
The get_random_slice
function stores reward amounts directly as Octas (100-500) without converting to APT units:
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);
}
When claiming, the program transfers the raw Octas amount without conversion:
public entry fun claim_pizza_slice(user: &signer) acquires ModuleData, State {
...
let amount = *table::borrow(&state.users_claimed_amount, user_addr);
...
transfer_from_contract(user_addr, amount);
...
}
fun transfer_from_contract(to: address, amount: u64) acquires ModuleData {
...
coin::transfer<AptosCoin>(&resource_signer, to, amount);
...
}
Risk
Likelihood: High - Affects every user claim transaction
Impact: High - Users receive rewards 100 million times smaller than intended, making protocol unusable
Impact
-
Users lose ~99.999999% of intended rewards
-
Claims become economically unviable (gas > reward)
-
Protocol funds remain locked due to lack of recovery mechanism
-
Severe reputation damage to the project
Proof of Concept
In this example the owner fund the program with 1000 APT and the user got only 100 Octas.
#[test(deployer = @pizza_drop, user = @0x123, framework = @0x1)]
#[expected_failure(abort_code = 3)]
fun test_invalid_decimals_apt(deployer: &signer, user: &signer, framework: &signer) acquires State, ModuleData {
use aptos_framework::account;
use aptos_framework::timestamp;
use aptos_framework::aptos_coin;
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(user));
debug::print(&utf8(b"=== PIZZA DROP WITH APT TEST ==="));
init_module(deployer);
debug::print(&utf8(b"Pizza drop module initialized"));
let decimal_base = 100000000;
let funding_amount = 10000 * decimal_base;
let deployer_coins = coin::mint<AptosCoin>(funding_amount, &mint_cap);
coin::register<AptosCoin>(deployer);
coin::deposit<AptosCoin>(@pizza_drop, deployer_coins);
let deployer_balance = coin::balance<AptosCoin>(@pizza_drop);
debug::print(&utf8(b"Deployer APT balance: "));
debug::print(&deployer_balance);
let contract_funding = 1000 * decimal_base;
fund_pizza_drop(deployer, contract_funding);
let contract_balance = get_actual_apt_balance();
debug::print(&utf8(b"Contract APT balance: "));
debug::print(&contract_balance);
assert!(contract_balance == contract_funding, 1);
let user_addr = signer::address_of(user);
register_pizza_lover(deployer, user_addr);
assert!(is_registered(user_addr), 2);
debug::print(&utf8(b"User registered successfully"));
let assigned_amount = get_claimed_amount(user_addr);
debug::print(&utf8(b"User assigned amount: "));
debug::print(&assigned_amount);
assert!(assigned_amount >= 100 * decimal_base && assigned_amount <= 500 * decimal_base, 3);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
The test failed as the amount received by the user is in Octas and not in APT
Recommended Mitigation
Convert random amounts to proper APT units in Octas:
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;
+ let random_amount = (100 + random_val) * 100000000; // Convert APT to Octas
table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}