forked from MystenLabs/sui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[move] Add an example of an auction implementation (MystenLabs#756)
* [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
Showing
3 changed files
with
284 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} | ||
} |