Challenge 8: Flash!
In this challenge, you will explore a decentralized exchange (DEX) with a critical flaw you can exploit to capture the flag. This exchange operates with two tokens—CTFA and CTFB—and features a vault that allows users to take flash loans. Your objective is to manipulate the token balances effectively to obtain the flag by using the vulnerabilities in the DEX's flash loan mechanism. To solve this challenge, you will have to have a deep understanding of programmable transaction blocks (PTBs) and how to build them using the TS SDK or the CLI.
Deployed Contract Addresses:
MintA<0xfd9284dd49737957297ebb10a9bbf73964453696c037b9e59f0fb4d23fc13aee::ctfa::CTFA>: 0x87cde5710a62e890058cf841ac3efa9f7b9db7a34f59c07621f994addbf06b9a
MintB<0xfd9284dd49737957297ebb10a9bbf73964453696c037b9e59f0fb4d23fc13aee::ctfb::CTFB>: 0x4395d877fb46f8f9929a090eab7c1a69ec3fadcc5dbd491c0cc04716a8e18164
Package: 0xfd9284dd49737957297ebb10a9bbf73964453696c037b9e59f0fb4d23fc13aee
CoinMetadata<0xfd9284dd49737957297ebb10a9bbf73964453696c037b9e59f0fb4d23fc13aee::ctfa::CTFA>: 0xcf7289e7211f6df0d63f4485bf5b3978ce58f3a55c1989811033f88ed586f35a
CoinMetadata<0xfd9284dd49737957297ebb10a9bbf73964453696c037b9e59f0fb4d23fc13aee::ctfb::CTFB>: 0x475243f98ca1707225743502003d34b207ffc8b6f6b0bd2e8531eee0555f6085
Contracts
firstcoin.move
module ctf::ctfa {
use iota::coin::{Self, Coin, TreasuryCap};
public struct CTFA has drop {}
public struct MintA<phantom CTFA> has key, store {
id: UID,
cap: TreasuryCap<CTFA>
}
fun init(witness: CTFA, ctx: &mut TxContext) {
// Get a treasury cap for the coin and give it to the transaction sender
let (treasury_cap, metadata) = coin::create_currency<CTFA>(witness, 1, b"CTFA", b"CTF A Coin", b"Token for the CTF", option::none(), ctx);
let mint = MintA<CTFA> {
id: object::new(ctx),
cap:treasury_cap
};
transfer::share_object(mint);
transfer::public_freeze_object(metadata);
}
public(package) fun mint_for_vault<CTFA>(mut mint: MintA<CTFA>, ctx: &mut TxContext): Coin<CTFA> {
let coina = coin::mint<CTFA>(&mut mint.cap, 100, ctx);
coin::mint_and_transfer(&mut mint.cap, 10, tx_context::sender(ctx), ctx);
let MintA<CTFA> {
id: ida,
cap: capa
} = mint;
object::delete(ida);
transfer::public_freeze_object(capa);
coina
}
}
secondcoin.move
module ctf::ctfb {
use iota::coin::{Self, Coin, TreasuryCap};
public struct CTFB has drop {}
public struct MintB<phantom CTFB> has key, store {
id: UID,
cap: TreasuryCap<CTFB>
}
fun init(witness: CTFB, ctx: &mut TxContext) {
// Get a treasury cap for the coin and give it to the transaction sender
let (treasury_cap, metadata) = coin::create_currency<CTFB>(witness, 1, b"CTFB", b"CTF B Coin", b"Token for the CTF", option::none(), ctx);
let mint = MintB<CTFB> {
id: object::new(ctx),
cap:treasury_cap
};
transfer::share_object(mint);
transfer::public_freeze_object(metadata);
}
public(package) fun mint_for_vault<CTFB>(mut mint: MintB<CTFB>, ctx: &mut TxContext): Coin<CTFB> {
let coinb = coin::mint<CTFB>(&mut mint.cap, 100, ctx);
coin::mint_and_transfer(&mut mint.cap, 10, tx_context::sender(ctx), ctx);
let MintB<CTFB> {
id: ida,
cap: capa
} = mint;
object::delete(ida);
transfer::public_freeze_object(capa);
coinb
}
}
vault.move
module ctf::vault{
use iota::coin::{Self, Coin};
use iota::balance::{Self, Balance};
use ctf::ctfa::{Self, MintA};
use ctf::ctfb::{Self, MintB};
public struct Vault<phantom A, phantom B> has key {
id: UID,
coin_a: Balance<A>,
coin_b: Balance<B>,
flashed: bool
}
public struct Flag has key, store {
id: UID,
user: address
}
public struct Receipt {
id: ID,
a_to_b: bool,
repay_amount: u64
}
public entry fun initialize<A,B>(capa: MintA<A>, capb: MintB<B>,ctx: &mut TxContext) {
let vault = Vault<A, B> {
id: object::new(ctx),
coin_a: coin::into_balance(ctfa::mint_for_vault(capa, ctx)),
coin_b: coin::into_balance(ctfb::mint_for_vault(capb, ctx)),
flashed: false
};
transfer::share_object(vault);
}
public fun flash<A,B>(vault: &mut Vault<A,B>, amount: u64, a_to_b: bool, ctx: &mut TxContext): (Coin<A>, Coin<B>, Receipt) {
assert!(!vault.flashed, 1);
let (coin_a, coin_b) = if (a_to_b) {
(coin::zero<A>(ctx), coin::from_balance(balance::split(&mut vault.coin_b, amount ), ctx))
}
else {
(coin::from_balance(balance::split(&mut vault.coin_a, amount ), ctx), coin::zero<B>(ctx))
};
let receipt = Receipt {
id: object::id(vault),
a_to_b,
repay_amount: amount
};
vault.flashed = true;
(coin_a, coin_b, receipt)
}
public fun repay_flash<A,B>(vault: &mut Vault<A,B>, coina: Coin<A>, coinb: Coin<B>, receipt: Receipt) {
let Receipt {
id: _,
a_to_b: a2b,
repay_amount: amount
} = receipt;
if (a2b) {
assert!(coin::value(&coinb) >= amount, 0);
} else {
assert!(coin::value(&coina) >= amount, 1);
};
balance::join(&mut vault.coin_a, coin::into_balance(coina));
balance::join(&mut vault.coin_b, coin::into_balance(coinb));
vault.flashed = false;
}
public fun swap_a_to_b<A,B>(vault: &mut Vault<A,B>, coina:Coin<A>, ctx: &mut TxContext): Coin<B> {
let amount_out_B = coin::value(&coina) * balance::value(&vault.coin_b) / balance::value(&vault.coin_a);
coin::put<A>(&mut vault.coin_a, coina);
coin::take(&mut vault.coin_b, amount_out_B, ctx)
}
public fun swap_b_to_a<A,B>(vault: &mut Vault<A,B>, coinb:Coin<B>, ctx: &mut TxContext): Coin<A> {
let amount_out_A = coin::value(&coinb) * balance::value(&vault.coin_a) / balance::value(&vault.coin_b);
coin::put<B>(&mut vault.coin_b, coinb);
coin::take(&mut vault.coin_a, amount_out_A, ctx)
}
#[allow(lint(self_transfer))]
public fun get_flag<A,B>(vault: &Vault<A,B>, ctx: &mut TxContext) {
assert!(
balance::value(&vault.coin_a) == 0 && balance::value(&vault.coin_b) == 0, 123
);
transfer::public_transfer(Flag {
id: object::new(ctx),
user: tx_context::sender(ctx)
}, tx_context::sender(ctx));
}
}
Good luck in capturing your eighth flag!
Related Articles
This challenge will test your understanding of the Object Model, the Coin Standard, and PTBs. You will need to use your knowledge of these concepts to exploit the DEX's flash loan mechanism and capture the flag.
The DEX programmer pulled an all-nighter before writing the flash loan mechanism, making a critical mistake.