Skip to content

Commit

Permalink
add basic auctions, work on english auction
Browse files Browse the repository at this point in the history
  • Loading branch information
ggonzalez94 committed Dec 7, 2024
1 parent 089d47a commit a9a3f2a
Show file tree
Hide file tree
Showing 2 changed files with 393 additions and 0 deletions.
154 changes: 154 additions & 0 deletions src/DutchAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title DutchAuction
* @notice Implements a standard Dutch auction that starts at a given high price and goes down over time.
* @dev Extend this to modify the price decay logic, asset transfer, or other mechanics.
*/
contract DutchAuction {
// -------------------------
// State Variables
// -------------------------

/// @notice The address of the seller
address public seller;

/// @notice The auction start time
uint256 public startTime;

/// @notice The time in seconds after the start at which the auction fully ends (no price goes below floor)
uint256 public duration;

/// @notice The initial starting price at `startTime`
uint256 public startPrice;

/// @notice The lowest possible price at the end of the auction
uint256 public floorPrice;

/// @notice Indicates if the auction has been successfully purchased
bool public purchased;

/// @notice The buyer who successfully purchased the item
address public buyer;

// -------------------------
// Events
// -------------------------

event AuctionStarted(
address indexed seller, uint256 startPrice, uint256 floorPrice, uint256 startTime, uint256 duration
);

event Purchased(address indexed buyer, uint256 amount);
event FundsWithdrawn(address indexed recipient, uint256 amount);

// -------------------------
// Modifiers
// -------------------------

modifier onlySeller() {
require(msg.sender == seller, "Only seller can call this");
_;
}

modifier auctionActive() {
require(block.timestamp >= startTime, "Auction not started yet.");
require(!purchased, "Item already purchased.");
require(block.timestamp < startTime + duration, "Auction ended without purchase.");
_;
}

// -------------------------
// Constructor
// -------------------------

/**
* @param _startPrice The price at the beginning of the auction
* @param _floorPrice The minimum price at the end of the auction
* @param _startTime The start time of the auction (timestamp in seconds)
* @param _duration The total duration of the price decrease
*/
constructor(uint256 _startPrice, uint256 _floorPrice, uint256 _startTime, uint256 _duration) {
require(_floorPrice <= _startPrice, "Floor price must be <= start price.");
require(_duration > 0, "Duration must be > 0.");
require(_startTime >= block.timestamp, "Start time must be in the future or now.");

seller = msg.sender;
startPrice = _startPrice;
floorPrice = _floorPrice;
startTime = _startTime;
duration = _duration;

emit AuctionStarted(seller, startPrice, floorPrice, startTime, duration);
}

// -------------------------
// Public / External Functions
// -------------------------

/**
* @notice Buy the item at the current price.
* @dev The auction ends immediately upon purchase. Any excess payment is refunded.
*/
function buy() external payable auctionActive {
uint256 price = currentPrice();
require(msg.value >= price, "Not enough funds sent.");

purchased = true;
buyer = msg.sender;

// Refund excess if overpaid
uint256 excess = msg.value > price ? (msg.value - price) : 0;
if (excess > 0) {
payable(msg.sender).transfer(excess);
emit FundsWithdrawn(msg.sender, excess);
}

// Transfer funds to the seller
payable(seller).transfer(price);
emit FundsWithdrawn(seller, price);

emit Purchased(msg.sender, price);

// Transfer the asset to the buyer (to be implemented by extending the contract)
_transferAsset(msg.sender);
}

/**
* @notice Returns the current price based on how much time has elapsed.
*/
function currentPrice() public view returns (uint256) {
return _currentPrice();
}

// -------------------------
// Extension Points
// -------------------------

/**
* @dev Hook to calculate the current price. You can override this in a child contract
* to change the price function. The default is a linear decrease from `startPrice` to `floorPrice`.
*/
function _currentPrice() internal view virtual returns (uint256) {
if (block.timestamp <= startTime) {
return startPrice;
}

uint256 elapsed = block.timestamp > startTime ? block.timestamp - startTime : 0;
if (elapsed >= duration) {
return floorPrice;
}

uint256 priceDecrease = ((startPrice - floorPrice) * elapsed) / duration;
return startPrice - priceDecrease;
}

/**
* @notice Hook for child contracts to handle asset transfer logic.
* For example, transfer an NFT from `seller` to `buyer`.
*/
function _transferAsset(address to) internal virtual {
// No-op in base contract
}
}
239 changes: 239 additions & 0 deletions src/EnglishAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

/**
* @title EnglishAuction
* @notice Implements a standard English auction with a reserve price and a predefined end time, plus optional anti-sniping time extensions.
* @dev Designed to be extended. Provides hooks to customize bid validation, increments, and asset transfers.
*/
abstract contract EnglishAuction {
/// @notice The address of the item’s seller
address private immutable seller;

/// @notice The minimum price at which the auction will start
uint256 private immutable reservePrice;

/// @notice Timestamp (in seconds) at which the auction ends
uint256 private endTime;

/// @notice The current highest bid amount
uint256 private highestBid;

/// @notice The address of the highest bidder
address private highestBidder;

/// @notice Indicates if the auction has been finalized
bool private finalized;

/// @notice Mapping of addresses to refunds they can withdraw
mapping(address bidder => uint256 amount) private refunds;

// -------------------------
// Anti-Sniping Variables (https://en.wikipedia.org/wiki/Auction_sniping)
// -------------------------

/// @notice If a bid is placed within `extensionThreshold` seconds of the endTime,
/// the auction endTime is extended by `extensionPeriod` seconds.
/// If you don't want to extend the auction in the case of a last minute bid, set this to 0.
/// But it is highly recommended to have some extension period, as it will discourage sniping.
uint256 private immutable extensionThreshold;
uint256 private immutable extensionPeriod;

event AuctionStarted(address indexed seller, uint256 reservePrice, uint256 endTime);
event NewHighestBid(address indexed bidder, uint256 amount);
event AuctionFinalized(address indexed winner, uint256 amount);
event RefundAvailable(address indexed bidder, uint256 amount);
event FundsWithdrawn(address indexed recipient, uint256 amount);
event AuctionExtended(uint256 newEndTime);

// TODO: Add parameters to the errors where relevant
error AuctionAlreadyFinalized();
error AuctionNotYetEnded();
error AuctionEnded();
error OnlySellerCanCall();
error BidNotHighEnough();
// -------------------------
// Modifiers
// -------------------------

modifier onlySeller() {
_checkSeller();
_;
}

modifier notFinalized() {
_checkAuctionNotFinalized();
_;
}

modifier auctionOngoing() {
_checkAuctionOngoing();
_;
}

modifier auctionEnded() {
_checkAuctionEnded();
_;
}

/**
* @param _reservePrice The minimum bid required to start winning
* @param _duration The number of seconds from now until the auction ends
* @param _extensionThreshold If a bid is placed within this many seconds of the end, time is extended
* @param _extensionPeriod How many seconds to extend the auction by when triggered
*/
constructor(
address _seller,
uint256 _reservePrice,
uint256 _duration,
uint256 _extensionThreshold,
uint256 _extensionPeriod
) {
seller = _seller;
reservePrice = _reservePrice;
endTime = block.timestamp + _duration;
extensionThreshold = _extensionThreshold;
extensionPeriod = _extensionPeriod;

emit AuctionStarted(seller, reservePrice, endTime);
}

// -------------------------
// Public / External Functions
// -------------------------

/**
* @notice Place a bid higher than the current highest bid, respecting the reserve price.
* @dev Uses a withdrawal pattern for previous highest bidder refunds.
* This function is `virtual` to allow overriding bidding logic.
*/
function placeBid() external payable virtual auctionOngoing notFinalized {
_beforeBid(msg.sender, msg.value);

_validateBidIncrement(msg.value);

// Move the old highest bid into a refundable balance
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid;
emit RefundAvailable(highestBidder, highestBid);
}

// Update highest bid
highestBid = msg.value;
highestBidder = msg.sender;

// Anti-sniping: if we're close to the end, extend the auction
_maybeExtendAuction();

_afterBid(msg.sender, msg.value);

emit NewHighestBid(msg.sender, msg.value);
}

/**
* @notice Withdraw refunds owed to the caller due to being outbid.
*/
function withdrawRefund() external {
uint256 amount = refunds[msg.sender];
require(amount > 0, "No refund available.");

refunds[msg.sender] = 0;
(bool success,) = payable(msg.sender).call{value: amount}("");
require(success, "Transfer failed");

emit FundsWithdrawn(msg.sender, amount);
}

/**
* @notice Finalizes the auction, transferring funds to the seller and the asset to the winner.
* @dev This function is `virtual` to allow customization. Anyone can call it after the auction ends.
*/
function finalizeAuction() external virtual notFinalized auctionEnded {
finalized = true;

if (highestBidder != address(0)) {
// TODO: Should we separate the withdraw of funds to the seller in another function?
(bool success,) = payable(seller).call{value: highestBid}("");
require(success, "Transfer failed");

emit FundsWithdrawn(seller, highestBid);

// Transfer asset to the winner
_transferAssetToWinner(highestBidder);
}

emit AuctionFinalized(highestBidder, highestBid);
}

// -------------------------
// Internal & Private Functions
// -------------------------

/**
* @dev Hook that runs before a bid is processed.
* Useful for additional checks, KYC, whitelisting, or logging.
* By default, it does nothing.
*/
function _beforeBid(address bidder, uint256 amount) internal virtual {
// No-op: override to implement custom checks (e.g. whitelists, pause checks)
}

/**
* @dev Hook that runs after a bid is processed.
* Useful for additional logic like tracking metrics or state.
*/
function _afterBid(address bidder, uint256 amount) internal virtual {
// No-op: override to implement custom logic after bidding
}

/**
* @dev Checks if the provided bid meets the increment requirements.
* By default, requires bid > highestBid and >= reservePrice.
* Override to impose specific increments (e.g. 5% higher than current highestBid).
*/
function _validateBidIncrement(uint256 newBid) internal view virtual {
if (newBid <= highestBid || newBid < reservePrice) revert BidNotHighEnough();
}

/**
* @dev Extends the auction if a bid is placed near the end.
* If `endTime - block.timestamp < extensionThreshold`, extend by `extensionPeriod`.
*/
function _maybeExtendAuction() internal {
// TODO:can we simplify this to:
// uint256 timeLeft = endTime - block.timestamp; since endTime should always be in the future?
uint256 timeLeft = endTime > block.timestamp ? endTime - block.timestamp : 0;
if (timeLeft < extensionThreshold && extensionPeriod > 0) {
endTime += extensionPeriod;
emit AuctionExtended(endTime);
}
}

/**
* @dev Internal hook that MUST be overridden by the implementing contract to handle
* the transfer of assets (e.g., NFTs, custom digital assets) to the auction winner.
* This function is called during auction finalization.
* @param winner The address of the highest bidder who won the auction
* @custom:example
* function _transferAssetToWinner(address winner) internal override {
* nft.transferFrom(address(this), winner, tokenId);
* }
*/
function _transferAssetToWinner(address winner) internal virtual;

function _checkSeller() internal view {
if (msg.sender != seller) revert OnlySellerCanCall();
}

function _checkAuctionNotFinalized() internal view {
if (finalized) revert AuctionAlreadyFinalized();
}

function _checkAuctionEnded() internal view {
if (block.timestamp < endTime) revert AuctionNotYetEnded();
}

function _checkAuctionOngoing() internal view {
if (block.timestamp >= endTime) revert AuctionEnded();
}
}

0 comments on commit a9a3f2a

Please sign in to comment.