Skip to content

Commit

Permalink
[Move] Added example of an auction using shared objects (MystenLabs#856)
Browse files Browse the repository at this point in the history
* Added implementation of an auction using shared objects

* [Move] Added example of an auction using shared objects and refactored existing code to share common parts with the other implementation

* Modified shared auction implementation to conform to shared object semantics plus added (forgotten) shared auction tests

* Renamed shared auction implementation

* Renamed shared TicTacToe implementation

* Removed development-time comments

* Updated readme

* Another round of changes based on review feedback

* Added missing renames
  • Loading branch information
awelc authored Mar 18, 2022
1 parent dd5af2f commit 289c0de
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 96 deletions.
3 changes: 2 additions & 1 deletion sui_programmability/examples/defi/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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).
* Auction: example implementation of the [English auction](https://en.wikipedia.org/wiki/English_auction) using single-owner objects only
* SharedAuction: example implementation of the [English auction](https://en.wikipedia.org/wiki/English_auction) using shared objects
* 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).
97 changes: 17 additions & 80 deletions sui_programmability/examples/defi/sources/Auction.move
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// 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:
/// (https://en.wikipedia.org/wiki/English_auction) using single-owner
/// objects only. 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
Expand All @@ -18,8 +19,9 @@
/// 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
/// there is no bid), 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
Expand All @@ -29,39 +31,20 @@
/// auction
module DeFi::Auction {
use Std::Option::{Self, Option};

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

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


// 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,
Expand All @@ -80,16 +63,8 @@ module DeFi::Auction {
/// 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(),
};
public fun create_auction<T: key + store>(to_sell: T, id: VersionedID, auctioneer: address, ctx: &mut TxContext) {
let auction = AuctionLib::create_auction(id, to_sell, ctx);
Transfer::transfer(auction, auctioneer);
}

Expand All @@ -111,52 +86,14 @@ module DeFi::Auction {
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);
}
}
assert!(AuctionLib::auction_id(auction) == &auction_id, EWRONG_AUCTION);
AuctionLib::update_auction(auction, bidder, coin);
}

/// Ends the auction - transfers item to the currently highest
/// bidder or to the original owner if no bids have been placed.
/// bidder or to the original owner if no bids have been
/// placed. This is executed by the auctioneer.
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);
AuctionLib::end_and_destroy_auction(auction);
}
}
130 changes: 130 additions & 0 deletions sui_programmability/examples/defi/sources/AuctionLib.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/// This is a helper module for implementing two versions of an
/// English auction (https://en.wikipedia.org/wiki/English_auction),
/// one using single-owner objects only and the other using shared
/// objects.
module DeFi::AuctionLib {
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};

friend DeFi::Auction;
friend DeFi::SharedAuction;

/// 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. It only really needs to be wrapped in
/// Option if Auction represents a shared object but we do it
/// for single-owner Auctions for better code re-use.
to_sell: Option<T>,
/// Owner of the time to be sold.
owner: address,
/// Data representing the highest bid (starts with no bid)
bid_data: Option<BidData>,
}

public(friend) fun auction_id<T: key + store>(auction: &Auction<T>): &ID {
ID::inner(&auction.id)
}

public(friend) fun auction_owner<T: key + store>(auction: &Auction<T>): address {
auction.owner
}

/// Creates an auction. This is executed by the owner of the asset to be
/// auctioned.
public(friend) fun create_auction<T: key + store>(id: VersionedID, to_sell: T, ctx: &mut TxContext): Auction<T> {
// 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.
Auction<T> {
id,
to_sell: Option::some(to_sell),
owner: TxContext::sender(ctx),
bid_data: Option::none(),
}
}

/// 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).
public fun update_auction<T: key + store>(auction: &mut Auction<T>, bidder: address, coin: Coin<GAS>) {
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(&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.
fun end_auction<T: key + store>(to_sell: &mut Option<T>, owner: address, bid_data: &mut Option<BidData>) {
let item = Option::extract(to_sell);
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(bid_data);
Transfer::transfer(funds, owner);
Transfer::transfer(item, highest_bidder);
} else {
// no bids placed - send the item back to the original owner
Transfer::transfer(item, owner);
};
}

/// Ends auction and destroys auction object (can only be used if
/// Auction is single-owner object) - transfers item to the
/// currently highest bidder or to the original owner if no bids
/// have been placed.
public fun end_and_destroy_auction<T: key + store>(auction: Auction<T>) {
let Auction { id, to_sell, owner, bid_data } = auction;
ID::delete(id);

end_auction(&mut to_sell, owner, &mut bid_data);

Option::destroy_none(bid_data);
Option::destroy_none(to_sell);
}

/// Ends auction (should only be used if Auction is a shared
/// object) - transfers item to the currently highest bidder or to
/// the original owner if no bids have been placed.
public fun end_shared_auction<T: key + store>(auction: &mut Auction<T>) {
end_auction(&mut auction.to_sell, auction.owner, &mut auction.bid_data);
}


}
66 changes: 66 additions & 0 deletions sui_programmability/examples/defi/sources/SharedAuction.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/// This is an implementation of an English auction
/// (https://en.wikipedia.org/wiki/English_auction) using shared
/// objects. There are types of participants:
/// - owner - this is the original owner of an item that is sold at an
/// auction; the owner creates an auction and ends it the time of her
/// choice
/// - bidders - these are parties interested in purchasing items sold
/// at an auction; similarly to the owner they have access to the
/// auction object and can submit bids to change its state
/// A typical lifetime of an auction looks as follows:
/// - auction is created by the owner and shared with the bidders
/// - bidders submit bids to try out-biding one another
/// - if a submitted bid is higher than the current bid (initially
/// there is no bid), 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 owner eventually ends the auction
/// - if no bids were received, the item goes back to the owner
/// - otherwise the funds accumulated in the auction go to the owner
/// and the item goes to the bidder that won the auction
module DeFi::SharedAuction {
use Sui::Coin::Coin;
use Sui::GAS::GAS;
use Sui::Transfer;
use Sui::TxContext::{Self,TxContext};

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

// Error codes.

/// An attempt to end auction by a different user than the owner
const EWRONG_OWNER: u64 = 1;

// Entry functions.

/// Creates an auction. This is executed by the owner of the asset
/// to be auctioned.
public fun create_auction<T: key + store >(to_sell: T, ctx: &mut TxContext) {
let auction = AuctionLib::create_auction(TxContext::new_id(ctx), to_sell, ctx);
Transfer::share_object(auction);
}

/// Sends a bid to the auction. The result is either successful
/// change of the auction state (if bid was high enough) or return
/// of the funds (if the bid was too low). This is executed by a
/// bidder.
public fun bid<T: key + store>(coin: Coin<GAS>, auction: &mut Auction<T>, ctx: &mut TxContext) {
let bidder = TxContext::sender(ctx);
AuctionLib::update_auction(auction, bidder, coin);
}

/// Ends the auction - transfers item to the currently highest
/// bidder or back to the original owner if no bids have been
/// placed. This is executed by the owner of the asset to be
/// auctioned.
public fun end_auction<T: key + store>(auction: &mut Auction<T>, ctx: &mut TxContext) {
let owner = AuctionLib::auction_owner(auction);
assert!(TxContext::sender(ctx) == owner, EWRONG_OWNER);
AuctionLib::end_shared_auction(auction);
}

}
Loading

0 comments on commit 289c0de

Please sign in to comment.