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

IHP-unit mismatch: Sends 100–500 octas, not 100–500 APT

Root + Impact

Description

Normal behavior: A successful claim should transfer 100–500 APT to the user, matching the public spec. GitHub
Problem: The code derives amount = 100 + (timestamp % 401) and uses it directly as the transfer amount. On Aptos, native coin amounts are in octas (1 APT = 10^8); without scaling, users receive only 100–500 octas (~0.000001–0.000005 APT), i.e., 1e-8× the intended value.

// Root cause in the codebase with @> marks to highlight the relevant section
// Vulnerable shape (schematic)
let rand = (timestamp::now_microseconds() % 401);
let amt = 100 + rand; // <-- units: octas (u64), NOT APT
transfer_from_contract(recipient, amt);

Risk

Likelihood:

  • The incorrect unit is used whenever a claim executes; no special timing or state is required.

  • Tests or local demos that don’t multiply by the scaling factor will always under-pay silently.


Impact:

  • Severe underpayment to all claimants (brand, legal, and contest failure risk).

Economic mis-accounting (internal pool math, dashboards, and comms diverge from reality).


Proof of Concept

Observe any successful claim’s credited amount and divide by 10^8: the result is ~0.000001–0.000005 APT, contradicting the README promise of 100–500 APT.

module tests::unit_mismatch_poc {
use std::signer;
use aptos_framework::coin;
use aptos_framework::aptos_coin::{Self as aptos_coin, AptosCoin};
use aptos_framework::account;
use aptos_framework::timestamp;
/// Import the contest module by named address (set on CLI).
/// Adjust the module name if different in your repo, e.g., `@pizza::pizza_drop`.
use @pizza_drop::pizza_drop;
/// This PoC expects the contract to pay **100–500 APT** per README/spec.
/// It asserts the credited amount is within [100, 500] **APT** after scaling,
/// so it FAILS before the fix (where amounts are unscaled octas) and PASSES after.
#[test(deployer=@0xC0FFEE, owner=@0xB0B, user=@0xA11CE, framework=@0x1)]
fun test_unit_mismatch_claim_pays_in_octas_NOT_APT_fails_pre_fix(
deployer: &signer,
owner: &signer,
user: &signer,
framework: &signer
) acquires aptos_coin::Capabilities {
// --- Framework coin setup (test-only) ---
// Give us mint/burn caps for AptosCoin in tests.
let (_burn_cap, mint_cap) = aptos_coin::initialize_for_test(framework);
// Ensure coin stores exist.
coin::register<AptosCoin>(owner);
coin::register<AptosCoin>(user);
// Start controllable time (to make randomness deterministic if needed).
timestamp::set_time_has_started_for_testing(framework);
// Optional: pin time for repeatability. You can vary to hit different draws.
timestamp::set_time_microseconds(framework, 1234567);
// --- Deploy/init the pizza module (adjust if your module uses a different init) ---
// If your module uses a dedicated init, call it here. Otherwise remove this.
// e.g., @pizza_drop::pizza_drop::init_module(deployer);
// (Comment out if not required in your codebase.)
// --- Fund the pool from owner ---
// Mint 1,000 APT to the owner and deposit.
let minted = aptos_coin::mint(&mint_cap, 1_000 * coin::scaling_factor<AptosCoin>());
coin::deposit<AptosCoin>(signer::address_of(owner), minted);
// Fund the pizza-drop pool with 600 APT (in octas).
// NOTE: Your function name/signature may differ; adjust accordingly.
@pizza_drop::pizza_drop::fund_pizza_drop(owner, 600 * coin::scaling_factor<AptosCoin>());
// --- Owner registers the user (so the claim path is the "happy path") ---
@pizza_drop::pizza_drop::register_pizza_lover(owner, signer::address_of(user));
// --- Observe user's balance before claim ---
let before = coin::balance<AptosCoin>(signer::address_of(user));
// --- User claims ---
@pizza_drop::pizza_drop::claim_pizza_slice(user);
// --- Observe user's balance after claim ---
let after = coin::balance<AptosCoin>(signer::address_of(user));
let delta = after - before;
// --- EXPECTATION (per README/spec): user should receive 100–500 APT ---
let scale = coin::scaling_factor<AptosCoin>(); // 10^8 octas/APT
let min_expected = 100 * scale;
let max_expected = 500 * scale;
// These assertions FAIL before the fix (because delta is 100–500 octas, i.e., << min_expected)
// and PASS after you multiply the random "base" by `scale` in the contract.
assert!(delta >= min_expected, 0x1001); // too small → paid in octas, not APT
assert!(delta <= max_expected, 0x1002); // sanity bound
}
}

Recommended Mitigation

Multiply by the coin scaling factor; validate bounds after scaling; update tests accordingly.

let base = 100 + (timestamp::now_microseconds() % 401); // 100..500 (APT units)
let scale = coin::scaling_factor<AptosCoin>(); // 10^8 for APT
let amt = base * scale; // 100..500 APT in octas
transfer_from_contract(recipient, amt);
Updates

Appeal created

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

Incorrect APT value

Support

FAQs

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