Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mystenlabs/sui/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates a flash loan implementation - a key DeFi primitive that allows uncollateralized borrowing within a single transaction.
Flash Loan Implementation
From examples/move/flash_lender/sources/example.move:
module flash_lender::example {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
/// Shared object offering flash loans
public struct FlashLender<phantom T> has key {
id: UID,
to_lend: Balance<T>, // Available funds
fee: u64, // Fee per loan
}
/// Hot potato receipt - MUST be repaid
public struct Receipt<phantom T> {
flash_lender_id: ID,
repay_amount: u64,
}
/// Admin capability
public struct AdminCap has key, store {
id: UID,
flash_lender_id: ID,
}
const ELoanTooLarge: u64 = 0;
const EInvalidRepaymentAmount: u64 = 1;
const ERepayToWrongLender: u64 = 2;
const EAdminOnly: u64 = 3;
/// Create a flash lender
public fun new<T>(
to_lend: Balance<T>,
fee: u64,
ctx: &mut TxContext
): AdminCap {
let id = object::new(ctx);
let flash_lender_id = id.uid_to_inner();
let flash_lender = FlashLender { id, to_lend, fee };
// Share the lender so anyone can borrow
transfer::share_object(flash_lender);
// Return admin capability to creator
AdminCap { id: object::new(ctx), flash_lender_id }
}
/// Borrow funds - returns loan and receipt
public fun loan<T>(
self: &mut FlashLender<T>,
amount: u64,
ctx: &mut TxContext,
): (Coin<T>, Receipt<T>) {
assert!(self.to_lend.value() >= amount, ELoanTooLarge);
let loan = coin::take(&mut self.to_lend, amount, ctx);
let repay_amount = amount + self.fee;
let receipt = Receipt {
flash_lender_id: object::id(self),
repay_amount,
};
(loan, receipt)
}
/// Repay loan - consumes receipt
public fun repay<T>(
self: &mut FlashLender<T>,
payment: Coin<T>,
receipt: Receipt<T>
) {
let Receipt { flash_lender_id, repay_amount } = receipt;
assert!(object::id(self) == flash_lender_id, ERepayToWrongLender);
assert!(payment.value() == repay_amount, EInvalidRepaymentAmount);
coin::put(&mut self.to_lend, payment)
}
/// Admin can withdraw funds
public fun withdraw<T>(
self: &mut FlashLender<T>,
admin: &AdminCap,
amount: u64,
ctx: &mut TxContext,
): Coin<T> {
assert!(object::borrow_id(self) == &admin.flash_lender_id, EAdminOnly);
coin::take(&mut self.to_lend, amount, ctx)
}
}
Key Concepts
Hot Potato Pattern
The Receipt struct has no abilities, so it cannot be:
- Dropped (
drop)
- Stored (
store)
- Copied (
copy)
- Transferred (
key)
The ONLY way to get rid of it is to call repay(), forcing loan repayment:
public struct Receipt<phantom T> {
flash_lender_id: ID,
repay_amount: u64,
// No abilities! Must be consumed
}
Shared Object for Lending
transfer::share_object(flash_lender);
Makes the lender accessible to everyone.
Admin Capability Pattern
public struct AdminCap has key, store {
id: UID,
flash_lender_id: ID, // Tied to specific lender
}
Using Flash Loans
Simple arbitrage example
public fun arbitrage(
lender: &mut FlashLender<SUI>,
dex1: &mut Pool,
dex2: &mut Pool,
ctx: &mut TxContext,
) {
// 1. Borrow funds
let (loan, receipt) = lender.loan(1000000, ctx);
// 2. Trade on DEX 1
let token = dex1.swap_sui_to_token(loan);
// 3. Trade on DEX 2
let mut profit = dex2.swap_token_to_sui(token);
// 4. Separate repayment from profit
let repayment = profit.split(receipt.repay_amount(), ctx);
// 5. Repay loan
lender.repay(repayment, receipt);
// 6. Keep profit
transfer::public_transfer(profit, ctx.sender());
}
Testing Flash Loans
From the source code tests:
#[test]
fun test_flash_loan() {
use sui::test_scenario as ts;
use sui::coin;
use sui::sui::SUI;
let mut ts = ts::begin(@0x0);
// Admin creates flash lender with 100 SUI, fee of 1
{
ts.next_tx(ADMIN);
let coin = coin::mint_for_testing<SUI>(100, ts.ctx());
let bal = coin.into_balance();
let cap = new(bal, 1, ts.ctx());
transfer::public_transfer(cap, ADMIN);
};
// Alice borrows and repays
{
ts.next_tx(ALICE);
let mut lender: FlashLender<SUI> = ts.take_shared();
let (loan, receipt) = lender.loan(10, ts.ctx());
// Simulate profit
let mut profit = coin::mint_for_testing<SUI>(1, ts.ctx());
profit.join(loan);
lender.repay(profit, receipt);
ts::return_shared(lender);
};
// Admin withdraws fees
{
ts.next_tx(ADMIN);
let cap = ts.take_from_sender();
let mut lender: FlashLender<SUI> = ts.take_shared();
// Pool grew from 100 to 101 (fee collected)
assert!(lender.max_loan() == 101, 0);
let coin = lender.withdraw(&cap, 1, ts.ctx());
transfer::public_transfer(coin, ADMIN);
ts::return_shared(lender);
ts.return_to_sender(cap);
};
ts.end();
}
Deploying Flash Lender
cd examples/move/flash_lender
sui client publish --gas-budget 100000000
# First get some SUI coins to lend
sui client call \
--package 0xPACKAGE_ID \
--module example \
--function new \
--args 0xCOIN_BALANCE 100 \
--type-args "0x2::sui::SUI" \
--gas-budget 10000000
Advanced DeFi Patterns
Liquidity pools
public struct Pool<phantom X, phantom Y> has key {
id: UID,
reserve_x: Balance<X>,
reserve_y: Balance<Y>,
lp_supply: Supply<LP<X, Y>>,
}
Automated market makers
public fun swap<X, Y>(
pool: &mut Pool<X, Y>,
input: Coin<X>,
ctx: &mut TxContext,
): Coin<Y> {
let input_amount = input.value();
let output_amount = calculate_output(
input_amount,
pool.reserve_x.value(),
pool.reserve_y.value(),
);
coin::put(&mut pool.reserve_x, input);
coin::take(&mut pool.reserve_y, output_amount, ctx)
}
Best Practices
1. Use hot potato for forced actions
// Receipt MUST be consumed
public struct Receipt<phantom T> {
// No abilities
}
2. Validate repayments
assert!(payment.value() == repay_amount, EInvalidRepayment);
assert!(lender_id == receipt.lender_id, EWrongLender);
3. Separate concerns with capabilities
public fun admin_only(cap: &AdminCap, ...) { /* ... */ }
public fun anyone(...) { /* ... */ }
4. Emit events for tracking
event::emit(LoanTaken {
borrower: ctx.sender(),
amount,
fee,
});
Next Steps