Post
Share your knowledge.
How to Implement Flash Loans in Sui Move Smart Contracts?
Overview
I'm developing a DeFi protocol on Sui that requires flash loan functionality—uncollateralized loans that must be repaid within a single transaction.
Key Requirements:
- Atomic Execution – Loan + repayment must occur in one TXN.
- Fee Enforcement – Guaranteed profit for the liquidity pool.
- Reentrancy Safety – Leverage Move’s ownership model.
- Gas Efficiency – Minimize overhead.
Current Progress & Challenges
✔ Basic loan logic implemented but lacks atomic enforcement.
❌ Uncertainty in handling loan fee calculations.
❌ Seeking best practices for multi-asset flash loans.
Key Questions
1. Architecture & Security
- What’s the most secure architecture for Sui flash loans?
- What are the common vulnerabilities to avoid?
2. Fee Enforcement
- How to atomically enforce fees (ensure repayment + profit)?
3. Sui-Specific Optimizations
- Are there Sui-specific optimizations (vs. EVM flash loans)?
- Best patterns for multi-asset support?
- Sui
Answers
8To create secure and gas-efficient flash loans on Sui using Move, leverage Sui's object-centric model, strong ownership, and atomic transactions. While Sui lacks native flash loan primitives, you can implement them via custom modules, transaction blocks, and strict access control.
Secure Flash Loan Architecture Wrap your loan pool logic in a shared object for multi-user access. A LoanPool struct will hold liquidity. Borrowers will use an entry function to temporarily control loaned assets. They must return the loan plus fee within the same atomic transaction block, or the transaction will fail.
Here's a simplified structure:
Code snippet
struct LoanPool has key { liquidity: Balance, fee_bps: u64 }
public entry fun flash_loan( pool: &mut LoanPool, amount: u64, borrower: &signer, ctx: &mut TxContext ) { let loan = coin::split(&mut pool.liquidity, amount); let expected_fee = (amount * pool.fee_bps) / 10_000;
borrower_contract::on_flash_loan(borrower, loan, expected_fee, ctx);
let (repaid, fee) = borrower_contract::return_funds(ctx);
assert!(coin::value(&repaid) == amount, E_NOT_REPAID);
assert!(coin::value(&fee) >= expected_fee, E_FEE_TOO_LOW);
coin::merge(&mut pool.liquidity, repaid);
coin::merge(&mut pool.liquidity, fee);
} Security Tip: Prevent borrowers from accessing or modifying the pool's liquidity beyond their loan by using encapsulation and not exposing internal mutable references.
Avoiding Common Vulnerabilities Incorrect Fee Math: Always round up fee calculations to avoid underpayment.
No Repay Check: Ensure the loan and fee are returned; otherwise, the pool can be drained.
Reentrancy: Move's linear type system inherently prevents reentrancy, but avoid exposing internal state or calling external entry functions mid-operation.
Enforcing Fees Atomically Sui's transaction atomicity guarantees that all operations within a transaction succeed or all revert. Use assert!() statements to verify the loan and fee repayment. If repayment conditions aren't met, the transaction aborts with a custom error code (e.g., E_NOT_REPAID, E_FEE_TOO_LOW).
Sui-Specific Optimizations & Multi-Asset Flash Loans Object Mutability Control: Use has key and public(entry) for tight scope.
No Gas Refunds: Pre-calculate gas usage and fee budgets accurately.
Move's Linear Type System: Ensures loaned objects are returned before the transaction ends.
For multi-asset support, use a LoanPool for each token type and allow batch borrowing via vectors. Validate each asset's repayment and fee independently within the same call.
Wrap your loan pool logic in a shared object for multi-user access. A LoanPool struct will hold liquidity. Borrowers will use an entry function to temporarily control loaned assets. They must return the loan plus fee within the same atomic transaction block, or the transaction will fail.
struct LoanPool has key { liquidity: Balance, fee_bps: u64 }
public entry fun flash_loan(
pool: &mut LoanPool,
amount: u64,
borrower: &signer,
ctx: &mut TxContext
) {
let loan = coin::split(&mut pool.liquidity, amount);
let expected_fee = (amount * pool.fee_bps) / 10_000;
borrower_contract::on_flash_loan(borrower, loan, expected_fee, ctx);
let (repaid, fee) = borrower_contract::return_funds(ctx);
assert!(coin::value(&repaid) == amount, E_NOT_REPAID);
assert!(coin::value(&fee) >= expected_fee, E_FEE_TOO_LOW);
coin::merge(&mut pool.liquidity, repaid);
coin::merge(&mut pool.liquidity, fee);
}
To securely implement flash loans in Sui Move, design your smart contract to ensure the loan, its usage, and full repayment (plus fee) occur within a single atomic transaction.
The safest structure involves a public entry function that takes a callback (or similar mechanism) for the borrower's logic. Your contract retains control by:
Withdrawing the loan amount.
Calling the borrower's logic.
Verifying the full repayment (loan + fee) before redepositing funds.
This approach leverages Move's strong resource and ownership checks to prevent issues like reentrancy or fund loss.
Key Vulnerabilities to Avoid:
Unvalidated borrower logic.
Unchecked repayments.
Misuse of shared objects.
1. Core Flash Loan Architecture
Minimal Viable Implementation
module flash::vault {
use sui::coin;
use sui::balance;
use sui::tx_context;
use sui::transfer;
struct FlashVault<phantom T> has key {
id: UID,
reserves: Balance<T>,
fee_bps: u64 // 5 = 0.05%
}
public entry fn execute_flash_loan(
vault: &mut FlashVault<T>,
amount: u64,
callback: vector<u8>, // Serialized MoveCall
ctx: &mut TxContext
) {
let loan = balance::split(&mut vault.reserves, amount);
let loan_coin = coin::from_balance(loan, ctx);
// Execute borrower's logic
move_call::decode_and_execute(callback, loan_coin, ctx);
// Repayment check
let repaid = coin::into_balance(loan_coin);
let required = amount + (amount * vault.fee_bps / 10_000);
assert!(balance::value(&repaid) >= required, ENotRepaid);
balance::join(&mut vault.reserves, repaid);
}
}
Key Components:
- Atomic flow: Loan → Callback → Repayment in one TXN
- Fee enforcement: Calculated and checked before state changes
- Type-safe: Phantom type
Tprevents asset mixing
2. Advanced Patterns
Multi-Asset Flash Loans
struct MultiVault has key {
id: UID,
pools: Table<TypeName, Balance<T>>
}
public entry fn multi_asset_loan(
vault: &mut MultiVault,
amounts: vector<u64>,
types: vector<TypeName>,
callback: vector<u8>,
ctx: &mut TxContext
) {
let loans = vector::empty();
let i = 0;
while (i < vector::length(&types)) {
let pool = table::borrow_mut(&mut vault.pools, types[i]);
vector::push_back(&mut loans, balance::split(pool, amounts[i]));
i = i + 1;
};
move_call::execute(callback, loans, ctx);
// Repayment checks omitted for brevity
}
Optimized Fee Calculation
// Precompute with 128-bit math to avoid overflow
let fee = ((amount as u128) * (vault.fee_bps as u128) / 10_000) as u64;
let required = amount + fee;
3. Sui-Specific Advantages
Reentrancy Protection
Move’s ownership model prevents:
// EVM vulnerability impossible in Move:
function maliciousCallback() external {
vault.flashLoan(...); // Re-entrancy attack
}
Shared Object Parallelism
Process multiple flash loans in parallel:
public entry fn parallel_loans(
vault1: &mut FlashVault<USDC>,
vault2: &mut FlashVault<SUI>,
ctx: &mut TxContext
) {
// Runs in single TXN
}
4. Security Considerations
Critical Checks
- Callback Validation:
let package = tx_context::sender(ctx);
assert!(is_whitelisted(package), EUntrusted);
- Frontrunning Protection:
let start_gas = tx_context::gas_remaining(ctx);
// ... execute callback ...
let used_gas = start_gas - tx_context::gas_remaining(ctx);
assert!(used_gas < MAX_ALLOWED_GAS, EGasGriefing);
Common Vulnerabilities
Unbounded Loans:
// Bad: No reserve checks
balance::split(&mut vault.reserves, amount);
Fee-Free Paths:
// Bad: Missing fee in some code paths
if (special_case) { repay = amount; }
5. Testing Strategies
Simulation Testing
#[test]
fun test_repayment_failure() {
let vault = create_vault(1000);
let malicious_callback = bcs::to_bytes(&MaliciousCallback {});
#[expected_failure(abort_code = ENotRepaid)]
execute_flash_loan(&mut vault, 1000, malicious_callback, ctx);
}
Fuzz Testing
#[test_only]
fun fuzz_loan_repayment(amount: u64, fee_bps: u64) {
let required = amount + (amount * fee_bps / 10_000);
assert!(required > amount, EFeeUnderflow);
}
6. Gas Optimization Tips
Batch Callbacks:
// Single callback for multi-operation loans
move_call::execute(batch_operations, ctx);
Storage Rebates:
- Use
&mutreferences instead of copying large objects - Prefer
Tableovervectorfor dynamic pools
Early Termination:
if (balance::value(&vault.reserves) < amount) {
return flash::error(ENoLiquidity); // Save gas
}
- Core Flash Loan Architecture Minimal Viable Implementation
module flash::vault {
use sui::coin;
use sui::balance;
use sui::tx_context;
use sui::transfer;
struct FlashVault<phantom T> has key {
id: UID,
reserves: Balance<T>,
fee_bps: u64 // 5 = 0.05%
}
public entry fn execute_flash_loan(
vault: &mut FlashVault<T>,
amount: u64,
callback: vector<u8>, // Serialized MoveCall
ctx: &mut TxContext
) {
let loan = balance::split(&mut vault.reserves, amount);
let loan_coin = coin::from_balance(loan, ctx);
// Execute borrower's logic
move_call::decode_and_execute(callback, loan_coin, ctx);
// Repayment check
let repaid = coin::into_balance(loan_coin);
let required = amount + (amount * vault.fee_bps / 10_000);
assert!(balance::value(&repaid) >= required, ENotRepaid);
balance::join(&mut vault.reserves, repaid);
}
}
Key Components:
Atomic flow: Loan → Callback → Repayment in one TXN Fee enforcement: Calculated and checked before state changes Type-safe: Phantom type T prevents asset mixing
To implement secure and gas-efficient flash loans on Sui using Move, you must leverage Sui’s object-centric model, strong ownership system, and transaction atomicity. While Sui doesn’t have native flash loan primitives like some EVM platforms, it supports everything required for this through custom modules, transaction blocks, and strict access control.
✅ 1. Architecture & Security: Secure Flash Loan Structure on Sui
You should wrap your loan pool logic in a shared object so multiple users/contracts can access it. Here's the recommended structure:
A LoanPool struct holds the liquidity (usually as a Balance
Borrowers interact via an entry function that gives them temporary control of the loaned assets.
They must perform their logic and return the loan + fee within the same transaction block, or it fails atomically.
Here’s a basic sketch:
struct LoanPool
public entry fun flash_loan
// Call the borrower’s contract logic
borrower_contract::on_flash_loan(borrower, loan, expected_fee, ctx);
// Expect repayment
let (repaid, fee) = borrower_contract::return_funds(ctx);
assert!(coin::value(&repaid) == amount, E_NOT_REPAID);
assert!(coin::value(&fee) >= expected_fee, E_FEE_TOO_LOW);
coin::merge(&mut pool.liquidity, repaid);
coin::merge(&mut pool.liquidity, fee);
}
Security Tip: Make sure the borrower cannot access or mutate the pool liquidity beyond their loan. Use encapsulation and no internal mutable references exposed outside your module.
🔐 Common Vulnerabilities to Avoid
Incorrect fee math: Always round up fee calculations to prevent underpayment.
No repay check: If you skip asserting the loan and fee were returned, the pool will be drained.
Reentrancy via shared objects: Even though Move’s linear type system prevents reentrancy by default, don't expose internal state or call external entry functions mid-operation.
💰 2. Enforcing Fees Atomically in Sui
Sui guarantees transaction atomicity—either everything succeeds, or it reverts. Use this to enforce repayment + fee in the same entry call. You can write custom assert!()s to check if the total returned amount meets your conditions.
If users try to repay less, the transaction aborts with a custom error code like:
const E_NOT_REPAID: u64 = 0x1; const E_FEE_TOO_LOW: u64 = 0x2;
The key point is: Move ensures no state is saved unless all checks pass.
⚡ 3. Sui-Specific Optimizations & Multi-Asset Flash Loans
Sui Optimizations:
Object Mutability Control: Use has key and public(entry) to tightly scope what’s mutable.
No Gas Refunds: Ensure you pre-calculate gas usage and fee budget accurately.
Move’s Linear Type System: Use it to ensure the loaned object must come back before the transaction ends.
Multi-Asset Support Pattern:
Use a LoanPool
public entry fun flash_loan_multi(
pools: &mut vector<LoanPool
Be careful to validate each asset’s repayment and fee independently within the same call.
🛠️ Tips for Testing on Localnet/Testnet
Use sui move build and sui move test to validate fee logic and edge cases.
Use sui client call and sui client dry-run to simulate flash loan execution.
Monitor gas usage per call and keep your vector operations minimal.
📚 Learn More & References
Flash loan concepts on Sui: https://docs.sui.io/build/programming-model/objects
Move Balance
Example DeFi patterns: https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples
By tightly scoping access, leveraging atomic transaction guarantees, and using Move’s strict type safety, you can build secure, efficient, and multi-asset-ready flash loan logic on Sui that rivals or surpasses what’s possible on EVM.
Calculate the required repayment (loan + fee) before passing control to the borrower. After the borrower's logic executes, use assert!() to compare the returned amount against this expected value. Any shortfall will cause the entire transaction to revert, keeping funds safe
Sui's Advantages: Sui Move offers advantages over EVM chains:
Linear Type System: Prevents double-spending and mismanagement of funds.
No Fallback Functions or Reentrancy Guards: These are unnecessary due to Move's design.
Multi-Asset Support: Implement a generic flash loan function using type parameters, or use enums/wrappers for unified asset-specific logic.
Simplified Structure Example:
public entry fun execute_flash_loan<CoinType>(
pool: &mut FlashLoanPool,
amount: u64,
borrower: address,
callback: vector<u8>, // or a function pointer-like approach
ctx: &mut TxContext
) {
let fee = calculate_fee(amount);
withdraw_from_pool<CoinType>(pool, amount, ctx);
// Call borrower logic with loaned funds (pseudo-code)
call_borrower(callback, borrower, amount);
let repayment = get_repayment<CoinType>(borrower);
assert!(repayment == amount + fee, EFlashLoan::InsufficientRepayment);
deposit_to_pool<CoinType>(pool, repayment, ctx);
}
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410