Skip to content

Commit

Permalink
[move] Flash loan example
Browse files Browse the repository at this point in the history
Implement a basic flash loan by leveraging "hot potatoes": struct values without `key`, `drop`, or `store` that must be eliminated by the end of the transaction that creates them. Taking out a flash loan creates a hot potato that forces repayment.

To make this clean, I had to rearrange some of the `Coin` code to separate entrypoint versions of functions that require an unused `TxContext` from copies of the same function that do not need this. Makes me wonder if once we have `public(entrypoint)` or an entrypoint annotation, we should allow entrypoints to omit `&mut TxContext` if they don't need it...
  • Loading branch information
sblackshear committed Mar 14, 2022
1 parent 90201f4 commit 20e6878
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 24 deletions.
1 change: 1 addition & 0 deletions sui_programmability/examples/defi/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# DeFi

* FlashLoan: a flash loan is a loan that must be initiated and repaid during the same transaction. This implementation works for any currency type, and is a good illustration of the power of Move [abilities](https://diem.github.io/move/abilities.html) and the "hot potato" design pattern.
* Escrow: an atomic swap leveraging an escrow agent that is trusted for liveness, but not safety (i.e., the agent cannot steal the goods being swapped).
* Uniswap 1.0-style DEX (coming soon).
172 changes: 172 additions & 0 deletions sui_programmability/examples/defi/sources/FlashLender.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/// A flash loan that works for any Coin type
module DeFi::FlashLender {
use Sui::Coin::{Self, Coin};
use Sui::ID::{Self, ID, VersionedID};
use Sui::Transfer;
use Sui::TxContext::{Self, TxContext};

/// A shared object offering flash loans to any buyer willing to pay `fee`.
struct FlashLender<phantom T> has key {
id: VersionedID,
/// Coins available to be lent to prospective borrowers
to_lend: Coin<T>,
/// Number of `Coin<T>`'s that will be charged for the loan.
/// In practice, this would probably be a percentage, but
/// we use a flat fee here for simplicity.
fee: u64,
}

/// A "hot potato" struct recording the number of `Coin<T>`'s that
/// were borrowed. Because this struct does not have the `key` or
/// `store` ability, it cannot be transferred or otherwise placed in
/// persistent storage. Because it does not have the `drop` ability,
/// it cannot be discarded. Thus, the only way to get rid of this
/// struct is to call `repay` sometime during the transaction that created it,
/// which is exactly what we want from a flash loan.
struct Receipt<phantom T> {
/// ID of the flash lender object the debt holder borrowed from
flash_lender_id: ID,
/// Total amount of funds the borrower must repay: amount borrowed + the fee
repay_amount: u64
}

/// An object conveying the privilege to withdraw funds from and deposit funds to the
/// `FlashLender` instance with ID `flash_lender_id`. Initially granted to the creator
/// of the `FlashLender`, and only one `AdminCap` per lender exists.
struct AdminCap has key, store {
id: VersionedID,
flash_lender_id: ID,
}

/// Attempted to borrow more than the `FlashLender` has.
/// Try borrowing a smaller amount.
const ELOAN_TOO_LARGE: u64 = 0;

/// Tried to repay an amount other than `repay_amount` (i.e., the amount borrowed + the fee).
/// Try repaying the proper amount.
const EINVALID_REPAYMENT_AMOUNT: u64 = 1;

/// Attempted to repay a `FlashLender` that was not the source of this particular debt.
/// Try repaying the correct lender.
const EREPAY_TO_WRONG_LENDER: u64 = 2;

/// Attempted to perform an admin-only operation without valid permissions
/// Try using the correct `AdminCap`
const EADMIN_ONLY: u64 = 3;

/// Attempted to withdraw more than the `FlashLender` has.
/// Try withdrawing a smaller amount.
const EWITHDRAW_TOO_LARGE: u64 = 4;

// === Creating a flash lender ===

/// Create a shared `FlashLender` object that makes `to_lend` available for borrowing.
/// Any borrower will need to repay the borrowed amount and `fee` by the end of the
/// current transaction.
public fun new<T>(to_lend: Coin<T>, fee: u64, ctx: &mut TxContext): AdminCap {
let id = TxContext::new_id(ctx);
let flash_lender_id = *ID::inner(&id);
let flash_lender = FlashLender { id, to_lend, fee };
// make the `FlashLender` a shared object so anyone can request loans
Transfer::share_object(flash_lender);
// give the creator admin permissions
AdminCap { id: TxContext::new_id(ctx), flash_lender_id }
}

/// Same as `new`, but transfer `WithdrawCap` to the transaction sender
public fun create<T>(to_lend: Coin<T>, fee: u64, ctx: &mut TxContext) {
let withdraw_cap = new(to_lend, fee, ctx);
Transfer::transfer(withdraw_cap, TxContext::sender(ctx))
}

// === Core functionality: requesting a loan and repaying it ===

/// Request a loan of `amount` from `lender`. The returned `Receipt<T>` "hot potato" ensures
/// that the borrower will call `repay(lender, ...)` later on in this tx.
/// Aborts if `amount` is greater that the amount that `lender` has available for lending.
public fun loan<T>(
self: &mut FlashLender<T>, amount: u64, ctx: &mut TxContext
): (Coin<T>, Receipt<T>) {
let to_lend = &mut self.to_lend;
assert!(Coin::value(to_lend) >= amount, ELOAN_TOO_LARGE);
let loan = Coin::withdraw(to_lend, amount, ctx);

let repay_amount = amount + self.fee;
let receipt = Receipt { flash_lender_id: *ID::id(self), repay_amount };
(loan, receipt)
}

/// Repay the loan recorded by `receipt` to `lender` with `payment`.
/// Aborts if the repayment amount is incorrect or `lender` is not the `FlashLender`
/// that issued the original loan.
public fun repay<T>(self: &mut FlashLender<T>, payment: Coin<T>, receipt: Receipt<T>) {
let Receipt { flash_lender_id, repay_amount } = receipt;
assert!(ID::id(self) == &flash_lender_id, EREPAY_TO_WRONG_LENDER);
assert!(Coin::value(&payment) == repay_amount, EINVALID_REPAYMENT_AMOUNT);

Coin::join(&mut self.to_lend, payment)
}

// === Admin-only functionality ===

/// Allow admin for `self` to withdraw funds.
public fun withdraw<T>(
self: &mut FlashLender<T>,
admin_cap: &AdminCap,
amount: u64,
ctx: &mut TxContext
): Coin<T> {
// only the holder of the `AdminCap` for `self` can withdraw funds
check_admin(self, admin_cap);

let to_lend = &mut self.to_lend;
assert!(Coin::value(to_lend) >= amount, EWITHDRAW_TOO_LARGE);
Coin::withdraw(to_lend, amount, ctx)
}

/// Allow admin to add more funds to `self`
public fun deposit<T>(
self: &mut FlashLender<T>, admin_cap: &AdminCap, coin: Coin<T>, _ctx: &mut TxContext
) {
// only the holder of the `AdminCap` for `self` can deposit funds
check_admin(self, admin_cap);

Coin::join(&mut self.to_lend, coin)
}

/// Allow admin to update the fee for `self`
public fun update_fee<T>(
self: &mut FlashLender<T>, admin_cap: &AdminCap, new_fee: u64, _ctx: &mut TxContext
) {
// only the holder of the `AdminCap` for `self` can update the fee
check_admin(self, admin_cap);

self.fee = new_fee
}

fun check_admin<T>(self: &FlashLender<T>, admin_cap: &AdminCap) {
assert!(ID::id(self) == &admin_cap.flash_lender_id, EADMIN_ONLY);
}

// === Reads ===

/// Return the current fee for `self`
public fun fee<T>(self: &FlashLender<T>): u64 {
self.fee
}

/// Return the maximum amount available for borrowing
public fun max_loan<T>(self: &FlashLender<T>): u64 {
Coin::value(&self.to_lend)
}

/// Return the amount that the holder of `self` must repay
public fun repay_amount<T>(self: &Receipt<T>): u64 {
self.repay_amount
}

/// Return the amount that the holder of `self` must repay
public fun flash_lender_id<T>(self: &Receipt<T>): ID {
self.flash_lender_id
}
}
56 changes: 56 additions & 0 deletions sui_programmability/examples/defi/tests/FlashLenderTests.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#[test_only]
module DeFi::FlashLenderTests {
use DeFi::FlashLender::{Self, AdminCap, FlashLender};
use Sui::Coin;
use Sui::GAS::GAS;
use Sui::TestScenario;

#[test]
fun flash_loan_example() {
let admin = @0x1;
let borrower = @0x2;

// admin creates a flash lender with 100 coins and a fee of 1 coin
let scenario = &mut TestScenario::begin(&admin);
{
let ctx = TestScenario::ctx(scenario);
let coin = Coin::mint_for_testing<GAS>(100, ctx);
FlashLender::create(coin, 1, ctx);
};
// borrower requests and repays a loan of 10 coins + the fee
TestScenario::next_tx(scenario, &borrower);
{
let lender = TestScenario::remove_object<FlashLender<GAS>>(scenario);
let ctx = TestScenario::ctx(scenario);

let (loan, receipt) = FlashLender::loan(&mut lender, 10, ctx);
// in practice, borrower does something (e.g., arbitrage) to make a profit from the loan.
// simulate this by min ting the borrower 5 coins.
let profit = Coin::mint_for_testing<GAS>(5, ctx);
Coin::join(&mut profit, loan);
let to_keep = Coin::withdraw(&mut profit, 4, ctx);
Coin::keep(to_keep, ctx);
FlashLender::repay(&mut lender, profit, receipt);

TestScenario::return_object(scenario, lender);
};
// admin withdraws the 1 coin profit from lending
TestScenario::next_tx(scenario, &admin);
{
let lender = TestScenario::remove_object<FlashLender<GAS>>(scenario);
let admin_cap = TestScenario::remove_object<AdminCap>(scenario);
let ctx = TestScenario::ctx(scenario);

// max loan size should have increased because of the fee payment
assert!(FlashLender::max_loan(&lender) == 101, 0);
// withdraw 1 coin from the pool available for lending
let coin = FlashLender::withdraw(&mut lender, &admin_cap, 1, ctx);
// max loan size should decrease accordingly
assert!(FlashLender::max_loan(&lender) == 100, 0);
Coin::keep(coin, ctx);

TestScenario::return_object(scenario, lender);
TestScenario::return_object(scenario, admin_cap);
}
}
}
62 changes: 39 additions & 23 deletions sui_programmability/framework/sources/Coin.move
Original file line number Diff line number Diff line change
Expand Up @@ -30,45 +30,32 @@ module Sui::Coin {
Transfer::transfer(c, recipient)
}

/// Transfer `c` to the sender of the current transaction
public fun keep<T>(c: Coin<T>, ctx: &TxContext) {
transfer(c, TxContext::sender(ctx))
}

/// Consume the coin `c` and add its value to `self`.
/// Aborts if `c.value + self.value > U64_MAX`
public fun join<T>(self: &mut Coin<T>, c: Coin<T>, _ctx: &mut TxContext) {
public fun join<T>(self: &mut Coin<T>, c: Coin<T>) {
let Coin { id, value } = c;
ID::delete(id);
self.value = self.value + value
}

/// Join everything in `coins` with `self`
public fun join_vec<T>(self: &mut Coin<T>, coins: vector<Coin<T>>, ctx: &mut TxContext) {
public fun join_vec<T>(self: &mut Coin<T>, coins: vector<Coin<T>>) {
let i = 0;
let len = Vector::length(&coins);
while (i < len) {
let coin = Vector::remove(&mut coins, i);
join(self, coin, ctx);
join(self, coin);
i = i + 1
};
// safe because we've drained the vector
Vector::destroy_empty(coins)
}

/// Split coin `self` to two coins, one with balance `split_amount`,
/// and the remaining balance is left is `self`.
public fun split<T>(self: &mut Coin<T>, split_amount: u64, ctx: &mut TxContext) {
let new_coin = withdraw(self, split_amount, ctx);
Transfer::transfer(new_coin, TxContext::sender(ctx));
}

/// Split coin `self` into multiple coins, each with balance specified
/// in `split_amounts`. Remaining balance is left in `self`.
public fun split_vec<T>(self: &mut Coin<T>, split_amounts: vector<u64>, ctx: &mut TxContext) {
let i = 0;
let len = Vector::length(&split_amounts);
while (i < len) {
split(self, *Vector::borrow(&split_amounts, i), ctx);
i = i + 1;
};
}

/// Subtract `value` from `self` and create a new coin
/// worth `value` with ID `id`.
/// Aborts if `value > self.value`
Expand Down Expand Up @@ -138,15 +125,44 @@ module Sui::Coin {
Transfer::transfer(c, recipient)
}

// ---Entrypoints---
// === Entrypoints ===

/// Send `amount` units of `c` to `recipient
/// Aborts with `EVALUE` if `amount` is greater than or equal to `amount`
public fun transfer_<T>(c: &mut Coin<T>, amount: u64, recipient: address, ctx: &mut TxContext) {
Transfer::transfer(withdraw(c, amount, ctx), recipient)
}

// ---Test-only code---
/// Consume the coin `c` and add its value to `self`.
/// Aborts if `c.value + self.value > U64_MAX`
public fun join_<T>(self: &mut Coin<T>, c: Coin<T>, _ctx: &mut TxContext) {
join(self, c)
}

/// Join everything in `coins` with `self`
public fun join_vec_<T>(self: &mut Coin<T>, coins: vector<Coin<T>>, _ctx: &mut TxContext) {
join_vec(self, coins)
}

/// Split coin `self` to two coins, one with balance `split_amount`,
/// and the remaining balance is left is `self`.
public fun split<T>(self: &mut Coin<T>, split_amount: u64, ctx: &mut TxContext) {
let new_coin = withdraw(self, split_amount, ctx);
Transfer::transfer(new_coin, TxContext::sender(ctx));
}

/// Split coin `self` into multiple coins, each with balance specified
/// in `split_amounts`. Remaining balance is left in `self`.
public fun split_vec<T>(self: &mut Coin<T>, split_amounts: vector<u64>, ctx: &mut TxContext) {
let i = 0;
let len = Vector::length(&split_amounts);
while (i < len) {
split(self, *Vector::borrow(&split_amounts, i), ctx);
i = i + 1;
};
}

// === Test-only code ===

#[test_only]
/// Mint coins of any type for (obviously!) testing purposes only
Expand Down
2 changes: 1 addition & 1 deletion sui_types/src/coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{

pub const COIN_MODULE_NAME: &IdentStr = ident_str!("Coin");
pub const COIN_STRUCT_NAME: &IdentStr = COIN_MODULE_NAME;
pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join");
pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join_");
pub const COIN_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec");

// Rust version of the Move Sui::Coin::Coin type
Expand Down

0 comments on commit 20e6878

Please sign in to comment.