one_shot::mint_rapper() calls token::create() with a fixed token name "Rapper" + Rapper tokens do not have a unique name
Description
public entry fun mint_rapper(module_owner: &signer, to: address)
acquires Collection, RapperStats {
let owner_addr = signer::address_of(module_owner);
assert!(owner_addr == @battle_addr, 1 );
if (!exists<Collection>(@battle_addr)) {
init_module(module_owner);
};
if (!exists<RapperStats>(@battle_addr)) {
let stats_table = table::new<address, StatsData>();
let owner_table = table::new<address, u64>();
move_to(module_owner, RapperStats { stats: stats_table, owner_counts: owner_table });
};
let _ = borrow_global<Collection>(@battle_addr);
@> let tok_ref = token::create(
@> module_owner,
@> string::utf8(COLLECTION_NAME),
@> string::utf8(b"A new rapper enters the scene."),
@> string::utf8(b"Rapper"),
@> option::none(),
@> string::utf8(b""),
@> );
let token_obj = object::object_from_constructor_ref<token::Token>(&tok_ref);
let token_id = object::address_from_constructor_ref(&tok_ref);
let stats_res = borrow_global_mut<RapperStats>(@battle_addr);
table::add(&mut stats_res.stats, token_id, StatsData {
owner: to,
weak_knees: true,
heavy_arms: true,
spaghetti_sweater: true,
calm_and_ready: false,
battles_won: 0,
});
if (table::contains(&stats_res.owner_counts, to)) {
let cnt = table::borrow_mut(&mut stats_res.owner_counts, to);
*cnt = *cnt + 1;
} else {
table::add(&mut stats_res.owner_counts, to, 1);
};
event::emit(MintRapperEvent { minter: to, token_id });
object::transfer(module_owner, token_obj, to);
}
Risk
Likelihood:
Impact:
Proof of Concept
Add this test_init_module()
wrapper function to cred_token.move
so that we can call the init_module()
function from our unit tests.
#[test_only]
public fun test_init_module(sender: &signer) {
init_module(sender);
}
Add the functions mint_rapper_fixed()
and get_token_ids()
to one_shot.move
so that players can retrieve their rapper token_id
for unit testing.
#[test_only]
public entry fun mint_rapper_fixed(module_owner: &signer, to: &signer)
acquires Collection, RapperStats, TokenIDs {
let owner_addr = signer::address_of(module_owner);
assert!(owner_addr == @battle_addr, 1 );
if (!exists<Collection>(@battle_addr)) {
init_module(module_owner);
};
if (!exists<RapperStats>(@battle_addr)) {
let stats_table = table::new<address, StatsData>();
let owner_table = table::new<address, u64>();
move_to(module_owner, RapperStats { stats: stats_table, owner_counts: owner_table });
};
let _ = borrow_global<Collection>(@battle_addr);
let tok_ref = token::create(
module_owner,
string::utf8(COLLECTION_NAME),
string::utf8(b"A new rapper enters the scene."),
string::utf8(b"Rapper"),
option::none(),
string::utf8(b""),
);
let token_obj = object::object_from_constructor_ref<token::Token>(&tok_ref);
let token_id = object::address_from_constructor_ref(&tok_ref);
let to_address = signer::address_of(to);
let stats_res = borrow_global_mut<RapperStats>(@battle_addr);
table::add(&mut stats_res.stats, token_id, StatsData {
owner: to_address,
weak_knees: true,
heavy_arms: true,
spaghetti_sweater: true,
calm_and_ready: false,
battles_won: 0,
});
if (table::contains(&stats_res.owner_counts, to_address)) {
let cnt = table::borrow_mut(&mut stats_res.owner_counts, to_address);
*cnt = *cnt + 1;
} else {
table::add(&mut stats_res.owner_counts, to_address, 1);
};
event::emit(MintRapperEvent { minter: to_address, token_id });
object::transfer(module_owner, token_obj, to_address);
if (exists<TokenIDs>(to_address)) {
let token_ids = borrow_global_mut<TokenIDs>(to_address);
vector::push_back(&mut token_ids.ids, token_id);
} else {
let v = vector::empty<address>();
vector::push_back(&mut v, token_id);
let token_ids = TokenIDs{
ids: v
};
move_to(to,token_ids);
};
}
#[test_only]
#[view]
public fun get_token_ids(player_address: address): vector<address> acquires TokenIDs {
assert!(exists<TokenIDs>(player_address), E_NO_TOKEN_IDS);
let token_ids = borrow_global<TokenIDs>(player_address);
token_ids.ids
}
Add this test_init_module()
wrapper function to rap_battle.move
so that we can call the init_module()
function from our unit tests.
#[test_only]
public fun test_init_module(sender: &signer) {
init_module(sender);
}
Create an additional tests/rap_same_name.move
file containing:
#[test_only]
module battle_addr::rap_same_name {
use std::signer;
use aptos_framework::account;
use aptos_framework::object;
use battle_addr::cred_token::{Self as cred_token};
use battle_addr::one_shot::{Self as one_shot};
use battle_addr::streets::{Self as streets};
use battle_addr::rap_battle::{Self as rap_battle};
use aptos_framework::timestamp;
use aptos_token_v2::token::{Self as token};
use std::string;
use std::debug;
use std::vector;
use aptos_framework::coin;
fun p<T>(s: &T) {
debug::print(s);
}
fun p_vector_u8(s: vector<u8>) {
p(&string::utf8(s));
}
fun p_vector_address(s: &vector<address>) {
debug::print(s);
}
const SECS_PER_DAY: u64 = 86400;
#[test(player1 = @0x100, framework = @0x1)]
public fun rap_same_name (
player1: &signer,
framework: &signer
) {
let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(framework);
timestamp::set_time_has_started_for_testing(framework);
let player1_address = signer::address_of(player1);
let module_owner = account::create_account_for_test(@battle_addr);
account::create_account_for_test(player1_address);
let player1_address = signer::address_of(player1);
cred_token::test_init_module(&module_owner);
rap_battle::test_init_module(&module_owner);
one_shot::mint_rapper_fixed(&module_owner, player1);
one_shot::mint_rapper_fixed(&module_owner, player1);
let balance1 = one_shot::balance_of(player1_address);
p_vector_u8(b"balance1");
p(&balance1);
assert!(balance1 == 2, 1);
let player1_rapper_ids = one_shot::get_token_ids(player1_address);
p_vector_u8(b"player1_rapper_ids");
p_vector_address(&player1_rapper_ids);
let player1_token_id0 = vector::borrow(&player1_rapper_ids, 0);
p_vector_u8(b"player1_token_id0");
p(player1_token_id0);
let player1_token_object0 =
object::address_to_object<token::Token>(*player1_token_id0);
p_vector_u8(b"player1_token_object0");
p(&player1_token_object0);
let player1_token0_name = token::name<token::Token>(player1_token_object0);
p_vector_u8(b"player1_token0_name");
p(&player1_token0_name);
let player1_token_id1 = vector::borrow(&player1_rapper_ids, 1);
p_vector_u8(b"player1_token_id1");
p(player1_token_id1);
let player1_token_object1 =
object::address_to_object<token::Token>(*player1_token_id1);
p_vector_u8(b"player1_token_object1");
p(&player1_token_object1);
let player1_token1_name = token::name<token::Token>(player1_token_object1);
p_vector_u8(b"player1_token1_name");
p(&player1_token1_name);
assert!(player1_token0_name == player1_token1_name, 1);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
}
Run with:
aptos move test --filter rap_same_name::rap_same_name
Output:
Running Move unit tests
[debug] "init_module()"
[debug] "balance1"
[debug] 2
[debug] "player1_rapper_ids"
[debug] [ @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a, @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2 ]
[debug] "player1_token_id0"
[debug] @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
[debug] "player1_token_object0"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] "player1_token0_name"
[debug] "Rapper"
[debug] "player1_token_id1"
[debug] @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2
[debug] "player1_token_object1"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2
}
[debug] "player1_token1_name"
[debug] "Rapper"
[ PASS ] 0x42::rap_same_name::rap_same_name
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}
Recommended Mitigation
Modify one_shot::mint_rapper()
to keep track of a global token count variable which increments on each Rapper token minted.
+ use std::string;
+ use std::string_utils;
+ struct TokenCount has key {
+ count: u64
+ };
#[test_only]
- public entry fun mint_rapper(module_owner: &signer, to: address)
+ public entry fun mint_rapper_unique_name(module_owner: &signer, to: &signer)
- acquires Collection, RapperStats {
+ acquires Collection, RapperStats, TokenIDs, TokenCount {
let owner_addr = signer::address_of(module_owner);
assert!(owner_addr == @battle_addr, 1 /* E_NOT_AUTHORIZED */);
// 🔧 Lazy-init if needed (unit tests don’t auto-run init_module)
if (!exists<Collection>(@battle_addr)) {
init_module(module_owner);
};
if (!exists<RapperStats>(@battle_addr)) {
// This case shouldn’t happen if init_module just ran, but keep it safe.
let stats_table = table::new<address, StatsData>();
let owner_table = table::new<address, u64>();
move_to(module_owner, RapperStats { stats: stats_table, owner_counts: owner_table });
};
+ if (!exists<TokenCount>(@battle_addr)) {
+ move_to(module_owner, TokenCount {
+ count: 0
+ });
+ };
// Safe to assume collection/stats exist now
let _ = borrow_global<Collection>(@battle_addr);
+ let token_count = borrow_global_mut<TokenCount>(@battle_addr);
+ let count_string = string_utils::to_string(&token_count.count);
+ let name = string::utf8(b"Rapper");
+ string::append(&mut name, count_string);
let tok_ref = token::create(
module_owner,
string::utf8(COLLECTION_NAME),
string::utf8(b"A new rapper enters the scene."),
- string::utf8(b"Rapper"),
+ name,
option::none(),
string::utf8(b""),
);
let token_obj = object::object_from_constructor_ref<token::Token>(&tok_ref);
let token_id = object::address_from_constructor_ref(&tok_ref);
+ let to_address = signer::address_of(to);
let stats_res = borrow_global_mut<RapperStats>(@battle_addr);
table::add(&mut stats_res.stats, token_id, StatsData {
- owner: to,
+ owner: to_address,
weak_knees: true,
heavy_arms: true,
spaghetti_sweater: true,
calm_and_ready: false,
battles_won: 0,
});
// increment owner count
- if (table::contains(&stats_res.owner_counts, to)) {
+ if (table::contains(&stats_res.owner_counts, to_address)) {
- let cnt = table::borrow_mut(&mut stats_res.owner_counts, to);
+ let cnt = table::borrow_mut(&mut stats_res.owner_counts, to_address);
*cnt = *cnt + 1;
} else {
- table::add(&mut stats_res.owner_counts, to, 1);
+ table::add(&mut stats_res.owner_counts, to_address, 1);
};
- event::emit(MintRapperEvent { minter: to, token_id });
+ event::emit(MintRapperEvent { minter: to_address, token_id });
- object::transfer(module_owner, token_obj, to);
+ object::transfer(module_owner, token_obj, to_address);
+ token_count.count += 1;
+
+ if (exists<TokenIDs>(to_address)) {
+ // TokenIDs already exists, perform upddate
+ let token_ids = borrow_global_mut<TokenIDs>(to_address);
+ vector::push_back(&mut token_ids.ids, token_id);
+ } else {
+ // first publish
+ let v = vector::empty<address>();
+ vector::push_back(&mut v, token_id);
+ let token_ids = TokenIDs{
+ ids: v
+ };
+ move_to(to,token_ids);
+ };
}
Add to tests/rap_same_name.move
:
#[test(player1 = @0x100, framework = @0x1)]
public fun rap_unique_name (
player1: &signer,
framework: &signer
) {
let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(framework);
timestamp::set_time_has_started_for_testing(framework);
let player1_address = signer::address_of(player1);
let module_owner = account::create_account_for_test(@battle_addr);
account::create_account_for_test(player1_address);
let player1_address = signer::address_of(player1);
cred_token::test_init_module(&module_owner);
rap_battle::test_init_module(&module_owner);
one_shot::mint_rapper_unique_name(&module_owner, player1);
one_shot::mint_rapper_unique_name(&module_owner, player1);
let balance1 = one_shot::balance_of(player1_address);
p_vector_u8(b"balance1");
p(&balance1);
assert!(balance1 == 2, 1);
let player1_rapper_ids = one_shot::get_token_ids(player1_address);
p_vector_u8(b"player1_rapper_ids");
p_vector_address(&player1_rapper_ids);
let player1_token_id0 = vector::borrow(&player1_rapper_ids, 0);
p_vector_u8(b"player1_token_id0");
p(player1_token_id0);
let player1_token_object0 =
object::address_to_object<token::Token>(*player1_token_id0);
p_vector_u8(b"player1_token_object0");
p(&player1_token_object0);
let player1_token0_name = token::name<token::Token>(player1_token_object0);
p_vector_u8(b"player1_token0_name");
p(&player1_token0_name);
let player1_token_id1 = vector::borrow(&player1_rapper_ids, 1);
p_vector_u8(b"player1_token_id1");
p(player1_token_id1);
let player1_token_object1 =
object::address_to_object<token::Token>(*player1_token_id1);
p_vector_u8(b"player1_token_object1");
p(&player1_token_object1);
let player1_token1_name = token::name<token::Token>(player1_token_object1);
p_vector_u8(b"player1_token1_name");
p(&player1_token1_name);
assert!(player1_token0_name != player1_token1_name, 1);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
Run with:
aptos move test --filter rap_same_name::rap_unique_name
Output:
Running Move unit tests
[debug] "init_module()"
[debug] "balance1"
[debug] 2
[debug] "player1_rapper_ids"
[debug] [ @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a, @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2 ]
[debug] "player1_token_id0"
[debug] @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
[debug] "player1_token_object0"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] "player1_token0_name"
[debug] "Rapper0"
[debug] "player1_token_id1"
[debug] @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2
[debug] "player1_token_object1"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xfab16b00983f01e5c2b7682472a4f4c3e5929fbba987958570b6290c02817df2
}
[debug] "player1_token1_name"
[debug] "Rapper1"
[ PASS ] 0x42::rap_same_name::rap_unique_name
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}