Skip to content

Commit

Permalink
[move] Add an example of an auction implementation (MystenLabs#756)
Browse files Browse the repository at this point in the history
* [move] Add an example of an auction implementation

* Addressed review comments

* Moved auction implementation to the defi directory

* Addressed additional review comments
  • Loading branch information
awelc authored Mar 15, 2022
1 parent 1aa9f8f commit 5722f3b
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 0 deletions.
1 change: 1 addition & 0 deletions sui_programmability/examples/defi/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 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.
* Auction: example implementation of the [English auction](https://en.wikipedia.org/wiki/English_auction).
* 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).
162 changes: 162 additions & 0 deletions sui_programmability/examples/defi/sources/Auction.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/// This is an implementation of an English auction
/// (https://en.wikipedia.org/wiki/English_auction). There are 3 types
/// of parties participating in an auction:
/// - auctioneer - this is a trusted party that runs the auction
/// - owner - this is the original owner of an item that is sold at an
/// auction; the owner submits a request to an auctioneer that runs
/// the auction
/// - bidders - these are parties interested in purchasing items sold
/// at an auction; they submit bids to an auctioneer to affect the
/// state of an auction
///
/// A typical lifetime of an auction looks as follows:
/// - auction starts by the owner sending an item to be sold along with
/// its own address to the auctioneer who creates and initializes an
/// auction
/// - bidders send bid to the auctioneer for a given auction
/// consisting of the funds they intend to use for the item's purchase
/// and their addresses
/// - the auctioneer periodically inspects the bids:
/// - if the inspected bid is higher than the current bid (initially
/// 0), the auction is updated with the current bid and funds
/// representing previous highest bid are sent to the original owner
/// - otherwise (bid is too low) the bidder's funds are sent back to
/// the bidder and the auction remains unchanged
/// - the auctioneer eventually ends the auction
/// - if no bids were received, the item goes back to the original owner
/// - otherwise the funds accumulated in the auction go to the
/// original owner and the item goes to the bidder that won the
/// auction
module DeFi::Auction {
use Std::Option::{Self, Option};

use Sui::Coin::{Self, Coin};
use Sui::GAS::GAS;
use Sui::ID::{Self, ID, VersionedID};
use Sui::Transfer;
use Sui::TxContext::{Self,TxContext};

// Error codes.

/// A bid submitted for the wrong (e.g. non-existent) auction.
const EWRONG_AUCTION: u64 = 1;

/// Stores information about an auction bid.
struct BidData has store {
/// Coin representing the current (highest) bid.
funds: Coin<GAS>,
/// Address of the highest bidder.
highest_bidder: address,
}

/// Maintains the state of the auction owned by a trusted
/// auctioneer.
struct Auction<T: key + store> has key {
id: VersionedID,
/// Item to be sold.
to_sell: T,
/// Owner of the time to be sold.
owner: address,
/// Data representing the highest bid (starts with no bid)
bid_data: Option<BidData>,
}

/// Represents a bid sent by a bidder to the auctioneer.
struct Bid has key {
id: VersionedID,
/// Address of the bidder
bidder: address,
/// ID of the Auction object this bid is intended for
auction_id: ID,
/// Coin used for bidding.
coin: Coin<GAS>
}

// Entry functions.

/// Creates an auction. It would be more natural to generate
/// auction_id in crate_auction and be able to return it so that
/// it can be shared with bidders but we cannot do this at the
/// moment. This is executed by the owner of the asset to be
/// auctioned.
public fun create_auction<T: key + store >(to_sell: T, id: VersionedID, auctioneer: address, ctx: &mut TxContext) {
// A question one might asked is how do we know that to_sell
// is owned by the caller of this entry function and the
// answer is that it's checked by the runtime.
let auction = Auction<T> {
id,
to_sell,
owner: TxContext::sender(ctx),
bid_data: Option::none(),
};
Transfer::transfer(auction, auctioneer);
}

/// Creates a bid a and send it to the auctioneer along with the
/// ID of the auction. This is executed by a bidder.
public fun bid(coin: Coin<GAS>, auction_id: ID, auctioneer: address, ctx: &mut TxContext) {
let bid = Bid {
id: TxContext::new_id(ctx),
bidder: TxContext::sender(ctx),
auction_id,
coin,
};
Transfer::transfer(bid, auctioneer);
}

/// Updates the auction based on the information in the bid
/// (update auction if higher bid received and send coin back for
/// bids that are too low). This is executed by the auctioneer.
public fun update_auction<T: key + store>(auction: &mut Auction<T>, bid: Bid, _ctx: &mut TxContext) {
let Bid { id, bidder, auction_id, coin } = bid;
ID::delete(id);

assert!(ID::inner(&auction.id) == &auction_id, EWRONG_AUCTION);
if (Option::is_none(&auction.bid_data)) {
// first bid
let bid_data = BidData {
funds: coin,
highest_bidder: bidder,
};
Option::fill(&mut auction.bid_data, bid_data);
} else {
let prev_bid_data = Option::borrow(&mut auction.bid_data);
if (Coin::value(&coin) > Coin::value(&prev_bid_data.funds)) {
// a bid higher than currently highest bid received
let new_bid_data = BidData {
funds: coin,
highest_bidder: bidder
};
// update auction to reflect highest bid
let BidData { funds, highest_bidder } = Option::swap(&mut auction.bid_data, new_bid_data);
// transfer previously highest bid to its bidder
Coin::transfer(funds, highest_bidder);
} else {
// a bid is too low - return funds to the bidder
Coin::transfer(coin, bidder);
}
}
}

/// Ends the auction - transfers item to the currently highest
/// bidder or to the original owner if no bids have been placed.
public fun end_auction<T: key + store>(auction: Auction<T>, _ctx: &mut TxContext) {
let Auction { id, to_sell, owner, bid_data } = auction;
ID::delete(id);

if (Option::is_some<BidData>(&bid_data)) {
// bids have been placed - send funds to the original item
// owner and the item to the highest bidder
let BidData { funds, highest_bidder } = Option::extract(&mut bid_data);
Transfer::transfer(funds, owner);
Transfer::transfer(to_sell, highest_bidder);
} else {
// no bids placed - send the item back to the original owner
Transfer::transfer(to_sell, owner);
};
// there is no bid data left regardless of the result, but the
// option still needs to be destroyed
Option::destroy_none(bid_data);
}
}
121 changes: 121 additions & 0 deletions sui_programmability/examples/defi/tests/Auction.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#[test_only]
module DeFi::AuctionTests {
use Std::Vector;

use Sui::Coin::{Self, Coin};
use Sui::GAS::GAS;
use Sui::ID::{Self, VersionedID};
use Sui::TestScenario::Self;
use Sui::TxContext::{Self, TxContext};

use DeFi::Auction::{Self, Auction, Bid};

const WRONG_ITEM_VALUE: u64 = 1;

// Example of an object type that could be sold at an auction.
struct SomeItemToSell has key, store {
id: VersionedID,
value: u64,
}

// Initializes the "state of the world" that mimicks what should
// be available in Sui genesis state (e.g., mints and distributes
// coins to users).
fun init(ctx: &mut TxContext, bidders: vector<address>) {
while (!Vector::is_empty(&bidders)) {
let bidder = Vector::pop_back(&mut bidders);
let coin = Coin::mint_for_testing(100, ctx);
Coin::transfer<GAS>(coin, bidder);
};
}

#[test]
public fun simple_auction_test() {
let auctioneer = @0xABBA;
let owner = @0xACE;
let bidder1 = @0xFACE;
let bidder2 = @0xCAFE;


let scenario = &mut TestScenario::begin(&auctioneer);
{
let bidders = Vector::empty();
Vector::push_back(&mut bidders, bidder1);
Vector::push_back(&mut bidders, bidder2);
init(TestScenario::ctx(scenario), bidders);
};

// a transaction by the item owner to put it for auction
TestScenario::next_tx(scenario, &owner);
let ctx = TestScenario::ctx(scenario);
let to_sell = SomeItemToSell {
id: TxContext::new_id(ctx),
value: 42,
};
// generate unique auction ID (it would be more natural to
// generate one in crate_auction and return it, but we cannot
// do this at the moment)
let id = TxContext::new_id(ctx);
// we need to dereference (copy) right here rather wherever
// auction_id is used - otherwise id would still be considered
// borrowed and could not be passed argument to a function
// consuming it
let auction_id = *ID::inner(&id);
Auction::create_auction(to_sell, id, auctioneer, ctx);

// a transaction by the first bidder to create an put a bid
TestScenario::next_tx(scenario, &bidder1);
{
let coin = TestScenario::remove_object<Coin<GAS>>(scenario);

Auction::bid(coin, auction_id, auctioneer, TestScenario::ctx(scenario));
};

// a transaction by the auctioneer to update state of the auction
TestScenario::next_tx(scenario, &auctioneer);
{
let auction = TestScenario::remove_object<Auction<SomeItemToSell>>(scenario);

let bid = TestScenario::remove_object<Bid>(scenario);
Auction::update_auction(&mut auction, bid, TestScenario::ctx(scenario));

TestScenario::return_object(scenario, auction);
};
// a transaction by the second bidder to create an put a bid (a
// bid will fail as it has the same value as that of the first
// bidder's)
TestScenario::next_tx(scenario, &bidder2);
{
let coin = TestScenario::remove_object<Coin<GAS>>(scenario);

Auction::bid(coin, auction_id, auctioneer, TestScenario::ctx(scenario));
};

// a transaction by the auctioneer to update state of the auction
TestScenario::next_tx(scenario, &auctioneer);
{
let auction = TestScenario::remove_object<Auction<SomeItemToSell>>(scenario);

let bid = TestScenario::remove_object<Bid>(scenario);
Auction::update_auction(&mut auction, bid, TestScenario::ctx(scenario));

TestScenario::return_object(scenario, auction);
};

// a transaction by the auctioneer to end auction
TestScenario::next_tx(scenario, &auctioneer);
{
let auction = TestScenario::remove_object<Auction<SomeItemToSell>>(scenario);
Auction::end_auction(auction, TestScenario::ctx(scenario));
};

// a transaction to check if the first bidder won (as the
// second bidder's bid was the same as that of the first one)
TestScenario::next_tx(scenario, &bidder1);
{
let acquired_item = TestScenario::remove_object<SomeItemToSell>(scenario);
assert!(acquired_item.value == 42, WRONG_ITEM_VALUE);
TestScenario::return_object(scenario, acquired_item);
};
}
}

0 comments on commit 5722f3b

Please sign in to comment.