streets.move::unstake() does not check whether the user has already recieved 4 CRED coins per rapper token staked + A player can receive more than 4 CRED coins per rapper token by restaking
Description
-
A player can receive a max of 4 CRED coins per rapper token staked.
-
streets.move::unstake()
does not check whether the user has already recieved 4 CRED coins per rapper token staked
-
This means that a player can restake their rapper token and receive an arbitrary amound of CRED coins exceeding the 4 CRED coins for every rapper token limit imposed by streets::unstake()
.
public entry fun unstake(staker: &signer, module_owner: &signer, rapper_token: Object<Token>) acquires StakeInfo {
@>
@> * per rapper token staked.
@> */
let staker_addr = signer::address_of(staker);
let token_id = object::object_address(&rapper_token);
assert!(exists<StakeInfo>(staker_addr), E_TOKEN_NOT_STAKED);
let stake_info = borrow_global<StakeInfo>(staker_addr);
assert!(stake_info.owner == staker_addr, E_NOT_OWNER);
let staked_duration = timestamp::now_seconds() - stake_info.start_time_seconds;
let days_staked = staked_duration / 86400;
if (days_staked > 0) {
let (wk, ha, ss, cr, wins) = one_shot::read_stats(token_id);
let final_wk = if (days_staked >= 1) { false } else { wk };
let final_ha = if (days_staked >= 2) { false } else { ha };
let final_ss = if (days_staked >= 3) { false } else { ss };
let final_cr = if (days_staked >= 4) { true } else { cr };
if (days_staked >= 1) { cred_token::mint(module_owner, staker_addr, 1); };
if (days_staked >= 2) { cred_token::mint(module_owner, staker_addr, 1); };
if (days_staked >= 3) { cred_token::mint(module_owner, staker_addr, 1); };
if (days_staked >= 4) { cred_token::mint(module_owner, staker_addr, 1); };
one_shot::write_stats(token_id, final_wk, final_ha, final_ss, final_cr, wins);
};
let StakeInfo { start_time_seconds: _, owner: _ } = move_from<StakeInfo>(staker_addr);
one_shot::transfer_record_only(token_id, @battle_addr, staker_addr);
object::transfer(module_owner, rapper_token, staker_addr);
event::emit(UnstakedEvent {
owner: staker_addr,
token_id,
staked_duration,
});
}
Risk
Likelihood:
-
A player receives 4 CRED coins by staking a rapper token for 4 or more days.
-
They then restake the same rapper token for another 4 days and receives more CRED coins, exceeding the max limit of 4 imposed by the streets::unstake()
function.
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
}
Create an additional tests/streets_exceeding_4_creds.move
file containing:
#[test_only]
module battle_addr::streets_exceeding_4_creds {
use std::signer;
use aptos_framework::account;
use aptos_framework::object::{Self as object, 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 aptos_framework::timestamp;
use aptos_token_v2::token::{Self as token};
use std::string::{Self, 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 test_stake_exceeding_4_creds(
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 module_owner = account::create_account_for_test(@battle_addr);
let player1_address = signer::address_of(player1);
account::create_account_for_test(player1_address);
cred_token::test_init_module(&module_owner);
one_shot::mint_rapper_fixed(&module_owner, player1);
let balance1 = one_shot::balance_of(player1_address);
assert!(balance1 == 1, 1);
let ids = one_shot::get_token_ids(player1_address);
p_vector_u8(b"ids");
p_vector_address(&ids);
let token_id0 = vector::borrow(&ids, 0);
p_vector_u8(b"token_id0");
p(token_id0);
let token_object0 = object::address_to_object<token::Token>(*token_id0);
p_vector_u8(b"token_object0");
debug::print(&token_object0);
p(&token_object0);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"before staking now_secs");
p(&now_secs);
streets::stake(player1, token_object0);
timestamp::fast_forward_seconds(10*SECS_PER_DAY);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"after staking now_secs");
p(&now_secs);
p_vector_u8(b"unstake()");
streets::unstake(player1, &module_owner, token_object0);
let final_skill = one_shot::public_skill_of(*token_id0);
p_vector_u8(b"final_skill");
p(&final_skill);
assert!(final_skill == 75, 2);
let player1_cred = coin::balance<cred_token::CRED>(player1_address);
p_vector_u8(b"player1_cred");
p(&player1_cred);
* we wait after the 4th day.
*/
assert!(player1_cred == 4, 3);
streets::stake(player1, token_object0);
timestamp::fast_forward_seconds(4*SECS_PER_DAY);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"after 2nd staking now_secs");
p(&now_secs);
streets::unstake(player1, &module_owner, token_object0);
let final_skill = one_shot::public_skill_of(*token_id0);
assert!(final_skill == 75, 2);
let player1_cred = coin::balance<cred_token::CRED>(player1_address);
p_vector_u8(b"player1_cred");
p(&player1_cred);
assert!(player1_cred == 8, 3);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
}
Run with:
aptos move test --filter test_stake_exceeding_4_creds
Output:
Running Move unit tests
[debug] "test_init_module()"
[debug] "init_module()"
[debug] "ids"
[debug] [ @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a ]
[debug] "token_id0"
[debug] @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
[debug] "token_object0"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] "before staking now_secs"
[debug] 0
[debug] "after staking now_secs"
[debug] 864000
[debug] "unstake()"
[debug] "final_skill"
[debug] 75
[debug] "player1_cred"
[debug] 4
[debug] "after 2nd staking now_secs"
[debug] 1209600
[debug] "player1_cred"
[debug] 8
[ PASS ] 0x42::streets_exceeding_4_creds::test_stake_exceeding_4_creds
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}
Recommended Mitigation
In streets_move::unstake()
:
+ use aptos_framework::coin;
public entry fun unstake(staker: &signer, module_owner: &signer, rapper_token: Object<Token>) acquires StakeInfo {
let staker_addr = signer::address_of(staker);
let token_id = object::object_address(&rapper_token);
assert!(exists<StakeInfo>(staker_addr), E_TOKEN_NOT_STAKED);
let stake_info = borrow_global<StakeInfo>(staker_addr);
assert!(stake_info.owner == staker_addr, E_NOT_OWNER);
let staked_duration = timestamp::now_seconds() - stake_info.start_time_seconds;
let days_staked = staked_duration / 86400;
if (days_staked > 0) {
let (wk, ha, ss, cr, wins) = one_shot::read_stats(token_id);
let final_wk = if (days_staked >= 1) { false } else { wk };
let final_ha = if (days_staked >= 2) { false } else { ha };
let final_ss = if (days_staked >= 3) { false } else { ss };
let final_cr = if (days_staked >= 4) { true } else { cr };
+ /* Get number of rapper tokens the staker has.
+ * Assume this is the number of unstaked rappers plus this one they're staking now.
+ * Assume they can only effectively stake only one rapper at a time.
+ */
+ let number_of_rappers = one_shot::balance_of(staker_addr) + 1;
+ /* The max CRED coins the staker can receive is 4 multiplied by the number
+ * rapper tokens in their possession.
+ */
+ let max_cred_coins = 4*number_of_rappers;
+ let cred_coins = coin::balance<cred_token::CRED>(staker_addr);
- if (days_staked >= 1) { cred_token::mint(module_owner, staker_addr, 1); };
+ if (days_staked >= 1 && cred_coins < max_cred_coins) {
+ cred_token::mint(module_owner, staker_addr, 1);
+ cred_coins = coin::balance<cred_token::CRED>(staker_addr);
+ };
- if (days_staked >= 2) { cred_token::mint(module_owner, staker_addr, 1); };
+ if (days_staked >= 2 && cred_coins < max_cred_coins) {
+ cred_token::mint(module_owner, staker_addr, 1);
+ cred_coins = coin::balance<cred_token::CRED>(staker_addr);
+ };
- if (days_staked >= 3) { cred_token::mint(module_owner, staker_addr, 1); };
+ if (days_staked >= 3 && cred_coins < max_cred_coins) {
+ cred_token::mint(module_owner, staker_addr, 1);
+ cred_coins = coin::balance<cred_token::CRED>(staker_addr);
+ };
- if (days_staked >= 4 && cred_coins < max_cred_coins) {
+ cred_token::mint(module_owner, staker_addr, 1);
+ //cred_coins = coin::balance<cred_token::CRED>(staker_addr);
+ };
one_shot::write_stats(token_id, final_wk, final_ha, final_ss, final_cr, wins);
};
let StakeInfo { start_time_seconds: _, owner: _ } = move_from<StakeInfo>(staker_addr);
one_shot::transfer_record_only(token_id, @battle_addr, staker_addr);
object::transfer(module_owner, rapper_token, staker_addr);
event::emit(UnstakedEvent {
owner: staker_addr,
token_id,
staked_duration,
});
}
Add test_stake_not_exceeding_4_creds()
to streets_exceeding_4_creds.move
:
#[test(player1 = @0x100, framework = @0x1)]
public fun test_stake_not_exceeding_4_creds(
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 module_owner = account::create_account_for_test(@battle_addr);
let player1_address = signer::address_of(player1);
account::create_account_for_test(player1_address);
cred_token::test_init_module(&module_owner);
one_shot::mint_rapper_fixed(&module_owner, player1);
let balance1 = one_shot::balance_of(player1_address);
assert!(balance1 == 1, 1);
let ids = one_shot::get_token_ids(player1_address);
p_vector_u8(b"ids");
p_vector_address(&ids);
let token_id0 = vector::borrow(&ids, 0);
p_vector_u8(b"token_id0");
p(token_id0);
let token_object0 = object::address_to_object<token::Token>(*token_id0);
p_vector_u8(b"token_object0");
debug::print(&token_object0);
p(&token_object0);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"before staking now_secs");
p(&now_secs);
streets::stake(player1, token_object0);
timestamp::fast_forward_seconds(10*SECS_PER_DAY);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"after staking now_secs");
p(&now_secs);
p_vector_u8(b"unstake()");
streets::unstake_fixed(player1, &module_owner, token_object0);
let final_skill = one_shot::public_skill_of(*token_id0);
p_vector_u8(b"final_skill");
p(&final_skill);
assert!(final_skill == 75, 2);
let player1_cred = coin::balance<cred_token::CRED>(player1_address);
p_vector_u8(b"player1_cred");
p(&player1_cred);
* we wait after the 4th day.
*/
assert!(player1_cred == 4, 3);
streets::stake(player1, token_object0);
timestamp::fast_forward_seconds(4*SECS_PER_DAY);
let now_secs = timestamp::now_seconds();
p_vector_u8(b"after 2nd staking now_secs");
p(&now_secs);
streets::unstake_fixed(player1, &module_owner, token_object0);
let final_skill = one_shot::public_skill_of(*token_id0);
assert!(final_skill == 75, 2);
let player1_cred = coin::balance<cred_token::CRED>(player1_address);
p_vector_u8(b"player1_cred");
p(&player1_cred);
* by restaking.
*/
assert!(player1_cred == 4, 3);
coin::destroy_burn_cap(burn_cap);
coin::destroy_mint_cap(mint_cap);
}
Run with:
aptos move test --filter test_stake_not_exceeding_4_creds
Output:
Running Move unit tests
[debug] "test_init_module()"
[debug] "init_module()"
[debug] "ids"
[debug] [ @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a ]
[debug] "token_id0"
[debug] @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
[debug] "token_object0"
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] 0x1::object::Object<0x4::token::Token> {
inner: @0xe46a3c36283330c97668b5d4693766b8626420a5701c18eb64026075c3ec8a0a
}
[debug] "before staking now_secs"
[debug] 0
[debug] "after staking now_secs"
[debug] 864000
[debug] "unstake()"
[debug] "final_skill"
[debug] 75
[debug] "player1_cred"
[debug] 4
[debug] "after 2nd staking now_secs"
[debug] 1209600
[debug] "player1_cred"
[debug] 4
[ PASS ] 0x42::streets_exceeding_4_creds::test_stake_not_exceeding_4_creds
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}