Skip to content

Commit

Permalink
English auction contract
Browse files Browse the repository at this point in the history
  • Loading branch information
tienngovan committed Oct 23, 2023
1 parent a2f4ffb commit c514e11
Show file tree
Hide file tree
Showing 3 changed files with 453 additions and 0 deletions.
309 changes: 309 additions & 0 deletions contracts/FeralfileEnglishAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import "./FeralfileSaleData.sol";
import "./ECDSASigner.sol";
import "./IFeralfileVault.sol";

contract FFV4 is FeralfileSaleData {
function buyArtworks(
bytes32 r_,
bytes32 s_,
uint8 v_,
SaleData calldata saleData_
) external payable {}
}

contract FeralfileEnglishAuction is Ownable, FeralfileSaleData, ECDSASigner {
struct EnglishAuction {
bytes32 id;
uint256 startAt;
uint256 endAt;
uint256 extendDuration;
uint256 extendThreshold;
uint256 minIncreaseFactor;
uint256 minIncreaseAmount;
uint256 minPrice;
}

struct Bid {
address bidder;
uint256 amount;
bool fromFeralFile;
}

mapping(bytes32 => EnglishAuction) public auctions;
mapping(bytes32 => Bid) public auctionLatestBid;

constructor(address signer_) ECDSASigner(signer_) {}

function getAuctionID(
address tokenContractAddr_,
uint256[] memory tokenIDs_
) public view returns (bytes32) {
// order the tokenIDs based on certain rule
for (uint i = 0; i < tokenIDs_.length - 1; i++) {
uint minIndex = i;
for (uint j = i + 1; j < tokenIDs_.length; j++) {
if (tokenIDs_[j] < tokenIDs_[minIndex]) {
minIndex = j;
}
}
(tokenIDs_[i], tokenIDs_[minIndex]) = (
tokenIDs_[minIndex],
tokenIDs_[i]
);
}

// hash the (contractAddr + ordered tokenIDs)
bytes32 message = keccak256(
abi.encode(block.chainid, tokenContractAddr_, tokenIDs_)
);

// return hash as identifier
return message;
}

function auctionOngoing(bytes32 aucId_) public view returns (uint256) {
// check the auction status and return
require(
auctions[aucId_].id != 0,
"FeralfileEnglishAuction: auction not found"
);

return block.timestamp;

// return
// auctions[aucId_].startAt <= block.timestamp &&
// auctions[aucId_].endAt >= block.timestamp;
}

function setEnglishAuctions(
EnglishAuction[] calldata auctions_
) external onlyOwner {
// for-loop to set the auction
// able to reset an auction if there's no winner and highest bid = 0
for (uint256 i = 0; i < auctions_.length; i++) {
EnglishAuction memory auc_ = auctions[auctions_[i].id];

require(
auc_.id == 0,
"FeralfileEnglishAuction: auction already exist"
);

require(
auctions_[i].startAt > block.timestamp,
"FeralfileEnglishAuction: auction start time should be in the future"
);

require(
auctions_[i].endAt > auctions_[i].startAt,
"FeralfileEnglishAuction: auction end time should be after start time"
);

require(
auctions_[i].extendDuration > 0,
"FeralfileEnglishAuction: auction extend duration should be greater than 0"
);

require(
auctions_[i].extendThreshold > 0,
"FeralfileEnglishAuction: auction extend threshold should be greater than 0"
);

require(
auctions_[i].minIncreaseFactor > 0,
"FeralfileEnglishAuction: auction min increase factor should be greater than 0"
);

require(
auctions_[i].minIncreaseAmount > 0,
"FeralfileEnglishAuction: auction min increase amount should be greater than 0"
);

require(
auctions_[i].minPrice > 0,
"FeralfileEnglishAuction: auction min price should be greater than 0"
);

auctions[auctions_[i].id] = auctions_[i];
}
}

function isValidBidRequest(
EnglishAuction memory auction_,
Bid memory lastBid_,
address bidder_,
uint256 amount_,
uint256 timestamp_
) internal pure returns (bool) {
// make sure the auction exist
require(
auction_.id.length > 0,
"FeralfileEnglishAuction: auction not found"
);
// check if auction started
require(
auction_.startAt < timestamp_,
"FeralfileEnglishAuction: auction not started"
);
// check if now() under auction end time
require(
auction_.endAt > timestamp_,
"FeralfileEnglishAuction: auction already ended"
);
// check if current winner not equal to current bidder (optional)
require(
lastBid_.bidder != bidder_,
"FeralfileEnglishAuction: bidder is the current winner"
);
// check bidding amount > highest bid amount
require(
amount_ > lastBid_.amount,
"FeralfileEnglishAuction: bid amount should be greater than highest bid amount"
);
if (lastBid_.amount > 0) {
// check bidding amount follow the minimum increment
uint256 minIncreaseAmount_ = (lastBid_.amount *
auction_.minIncreaseFactor) / 100;
if (minIncreaseAmount_ < auction_.minIncreaseAmount) {
minIncreaseAmount_ = auction_.minIncreaseAmount;
}
require(
amount_ >= lastBid_.amount + minIncreaseAmount_,
"FeralfileEnglishAuction: bid amount should follow the minimum increment"
);
} else {
// check bidding amount follow the minimum price
require(
amount_ >= auction_.minPrice,
"FeralfileEnglishAuction: bid amount should be greater than minimum price"
);
}
return true;
}

function bidOnAuction(bytes32 aucId_) external payable {
// get the auction object
EnglishAuction memory auction_ = auctions[aucId_];
// get the last auction bid object
Bid memory lastBid_ = auctionLatestBid[aucId_];
// verify the bid is valid
require(
isValidBidRequest(
auction_,
lastBid_,
msg.sender,
msg.value,
block.timestamp
),
"FeralfileEnglishAuction: invalid bid"
);
// transfer last winning bid amount to last bidder if fromFeralFile is false
if (!lastBid_.fromFeralFile) {
payable(lastBid_.bidder).transfer(lastBid_.amount);
}
// replace the new bid to bid map
auctionLatestBid[aucId_] = Bid({
bidder: msg.sender,
amount: msg.value,
fromFeralFile: false
});
// if the gap to end time is lower than threshold, update the auction end time to now() + extend duration
if (auction_.endAt - block.timestamp < auction_.extendThreshold) {
auctions[aucId_].endAt = block.timestamp + auction_.extendDuration;
}
// emit new bid event
emit NewBid(aucId_, msg.sender, msg.value);
}

function bidOnAuctionByFeralFile(
bytes32 aucId_,
address bidder_,
uint256 amount_,
uint256 timestamp_,
bytes32 r_,
bytes32 s_,
uint8 v_
) external {
// get the auction object
EnglishAuction memory auction_ = auctions[aucId_];
// get the last auction bid object
Bid memory lastBid_ = auctionLatestBid[aucId_];
// verify the bid is valid
require(
isValidBidRequest(auction_, lastBid_, bidder_, amount_, timestamp_),
"FeralfileEnglishAuction: invalid bid"
);
// make sure the auction exist
// get the auction object
// get the last auction bid object
// check if current winner not equal to current bidder (optional)
// check if now() under auction end time
// check bidding amount > highest bid amount
// check bidding amount follow the minimum increment
// check timestamp should in 10 mins

// check the signature of (aucId + bidder + amount + timestamp) to make sure the request is from Feral File
bytes32 message_ = keccak256(
abi.encode(block.chainid, aucId_, bidder_, amount_, timestamp_)
);
require(
isValidSignature(message_, r_, s_, v_),
"FeralfileEnglishAuction: invalid signature"
);
// transfer last winning bid amount to last bidder if fromFeralFile is false
if (!lastBid_.fromFeralFile) {
payable(lastBid_.bidder).transfer(lastBid_.amount);
}
// replace the new bid to bid map
auctionLatestBid[aucId_] = Bid({
bidder: bidder_,
amount: amount_,
fromFeralFile: true
});
// if the gap to end time is lower than threshold, update the auction end time to now() + extend duration
if (auction_.endAt - block.timestamp < auction_.extendThreshold) {
auctions[aucId_].endAt = block.timestamp + auction_.extendDuration;
}
// emit new bid event
emit NewBid(aucId_, bidder_, amount_);
}

function settleAuction(
bytes32 r_,
bytes32 s_,
uint8 v_,
address tokenContractAddr_,
SaleData memory saleData_
) external onlyOwner {
// transfer winning bid amount to Feral File Vault contract if winning bid is crypto bid
bytes32 aucId_ = keccak256(
abi.encodePacked(
getAuctionID(tokenContractAddr_, saleData_.tokenIds)
)
);
EnglishAuction memory auction_ = auctions[aucId_];
require(
auction_.id.length > 0,
"FeralfileEnglishAuction: auction not found"
);
Bid memory lastBid_ = auctionLatestBid[aucId_];
require(
lastBid_.bidder != address(0),
"FeralfileEnglishAuction: no bid"
);

saleData_.destination = lastBid_.bidder;
FFV4(tokenContractAddr_).buyArtworks(r_, s_, v_, saleData_);
}

event NewBid(
bytes32 indexed auctionId,
address indexed bidder,
uint256 indexed amount
);
}
11 changes: 11 additions & 0 deletions migrations/251_feralfile_english_auction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
var FeralfileEnglishAuction = artifacts.require("FeralfileEnglishAuction");

const argv = require("minimist")(process.argv.slice(2), {
string: ["exhibition_signer"],
});

module.exports = function (deployer) {
let exhibition_signer =
argv.exhibition_signer || "0xdB33365a8730de2F7574ff1189fB9D337bF4c36d";
deployer.deploy(FeralfileEnglishAuction, exhibition_signer);
};
Loading

0 comments on commit c514e11

Please sign in to comment.