Skip to content

Commit

Permalink
add dutch auction tests and fmt
Browse files Browse the repository at this point in the history
  • Loading branch information
ggonzalez94 committed Jan 1, 2025
1 parent 5853465 commit 8255e2d
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 37 deletions.
4 changes: 4 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ src = "src"
out = "out"
libs = ["lib"]

[fmt]
sort_imports = true
wrap_comments = true

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
39 changes: 26 additions & 13 deletions src/BaseSealedBidAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
* @title BaseSealedBidAuction
* @notice A base contract for sealed-bid auctions with a commit-reveal scheme and over-collateralization.
* Each user has exactly one active bid, which can be overwritten (topped up) before `commitDeadline`.
* This contract only handles commit-reveal and overcollateralization logic, and can be used with different auction types.
* It is recommended to use one of the child contracts(`FirstPriceSealedBidAuction` or `SecondPriceSealedBidAuction`) instead
* of using this contract directly, as they implement the logic for determining the winner, final price, and update the contract state accordingly.
* This contract only handles commit-reveal and overcollateralization logic, and can be used with different
* auction types.
* It is recommended to use one of the child contracts(`FirstPriceSealedBidAuction` or
* `SecondPriceSealedBidAuction`) instead
* of using this contract directly, as they implement the logic for determining the winner, final price, and
* update the contract state accordingly.
* @dev
* Privacy is achieved by hashing the commit and allowing overcollaterilzation.
* The contract ensure bidders commit(are not able to back out of their bid) by taking custody fo the funds.
* The contract ensures that bidders always reveal their bids, otherwise their funds are stuck(this can be customized by overriding `_checkWithdrawal`)
* The contract ensures that bidders always reveal their bids, otherwise their funds are stuck(this can be customized
* by overriding `_checkWithdrawal`)
* - Bidder commits by providing a `commitHash` plus some ETH collateral >= intended bid.
* - If they want to raise or change their hidden bid, they call `commitBid` again with a new hash, sending more ETH.
* - During reveal, user reveals `(salt, amount)`. If `collateral < amount`, reveal fails.
* - Child contracts handle final pricing logic (first-price or second-price).
* - This design is heavily inspired by [OverCollateralizedAuction from a16z](https://github.com/a16z/auction-zoo/blob/main/src/sealed-bid/over-collateralized-auction/OverCollateralizedAuction.sol)
* - This design is heavily inspired by [OverCollateralizedAuction from
* a16z](https://github.com/a16z/auction-zoo/blob/main/src/sealed-bid/over-collateralized-auction/OverCollateralizedAuction.sol)
*/
abstract contract BaseSealedBidAuction is ReentrancyGuard {
/// @notice The address of the seller or beneficiary
Expand Down Expand Up @@ -141,12 +146,14 @@ abstract contract BaseSealedBidAuction is ReentrancyGuard {

/**
* @notice Commit a sealed bid or update an existing commitment with more collateral.
* @dev It is strongly recommended that salt is a random value, and the bid is overcollateralized to avoid leaking information about the bid value.
* @dev It is strongly recommended that salt is a random value, and the bid is overcollateralized to avoid leaking
* information about the bid value.
* - Overwrites the old commitHash with the new one (if any).
* - Accumulates the new ETH into user’s collateral.
* @param commitHash The hash commitment to the bid, computed as
* `bytes20(keccak256(abi.encode(salt, bidValue)))`
* It is strongly recommended that salt is generated offchain, and is a random value, to avoid other actors from guessing the bid value.
* It is strongly recommended that salt is generated offchain, and is a random value, to avoid
* other actors from guessing the bid value.
*/
function commitBid(bytes20 commitHash) external payable {
if (block.timestamp < startTime || block.timestamp > commitDeadline) revert NotInCommitPhase();
Expand All @@ -170,8 +177,10 @@ abstract contract BaseSealedBidAuction is ReentrancyGuard {

/**
* @notice Reveal the actual bid.
* @dev This function only validates the amount and salt are correct, and updates the amount of unrevealed bids left.
* The logic for determining if the bid is the best(e.g. highest bid for a first-price auction), update the records and handle refunds is handled in the child contract
* @dev This function only validates the amount and salt are correct, and updates the amount of unrevealed bids
* left.
* The logic for determining if the bid is the best(e.g. highest bid for a first-price auction), update the
* records and handle refunds is handled in the child contract
* by implementing the `_handleRevealedBid` function.
* @param salt Random salt used in commit
* @param bidAmount The actual bid amount user is paying
Expand Down Expand Up @@ -272,7 +281,8 @@ abstract contract BaseSealedBidAuction is ReentrancyGuard {

/**
* @dev Sends funds to the seller after the auction has been finalized.
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning them)
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning
* them)
* @param amount The amount of proceeds to withdraw.
*/
function _withdrawSellerProceeds(uint96 amount) internal virtual {
Expand All @@ -282,11 +292,14 @@ abstract contract BaseSealedBidAuction is ReentrancyGuard {

/// @dev Checks if a withdrawal can be performed for `bidder`.
/// - It requires that the bidder revealed their bid on time and locks the funds in the contract otherwise.
/// This is done to incentivize bidders to always reveal, instead of whitholding if they realize they overbid.
/// This logic can be customized by overriding this function, to allow for example locked funds to be withdrawn to the seller.
/// This is done to incentivize bidders to always reveal, instead of whitholding if they realize they
/// overbid.
/// This logic can be customized by overriding this function, to allow for example locked funds to be withdrawn
/// to the seller.
/// Or to allow late reveals for bids that were lower than the winner's bid.
/// Or to apply a late reveal penalty, but still allow the bidder to withdraw their funds.
/// WARNING: Be careful when overrding, as it can create incentives where bidders don't reveal if they realize they overbid.
/// WARNING: Be careful when overrding, as it can create incentives where bidders don't reveal if they realize
/// they overbid.
/// @param bidder The address of the bidder to check
/// @return amount The amount that can be withdrawn
function _checkWithdrawal(address bidder) internal view virtual returns (uint96) {
Expand Down
25 changes: 16 additions & 9 deletions src/DutchAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
* @title DutchAuction
* @notice A Dutch auction selling multiple identical items.
* @dev
* By default the price starts high and decreases over time with a linear curve. But this can be changed by overriding
* `currentPrice()` to implement any custom price curve, including a reverse dutch auction(where price starts low and
* increases over time).
* - The price decreases from `startPrice` to `floorPrice` over `duration`.
* - Buyers can purchase at the current price until inventory = 0 or time runs out.
* - Once time runs out or inventory hits zero, the auction is considered ended.
* - Once time runs out or inventory hits zero, the auction is considered finalized.
* - If inventory remains after time ends, the seller can reclaim them via `withdrawUnsoldAssets()`.
*
* To use this contract, you must:
* 1. Provide an implementation of `_transferAssetToBuyer(address buyer, uint256 quantity)` that transfers the
* auctioned assets (e.g. NFTs) to the buyer.
* 2. Provide an implementation of `_withdrawUnsoldAssets(address seller, uint256 quantity)` that transfers the
* unsold assets back to the seller(if not all assets are sold).
* 3. Optionally override `_beforeBuy` or `_afterBuy` to implement custom bidding logic such as
* whitelisting or additional checks.
*/
abstract contract DutchAuction is ReentrancyGuard {
/// @dev The address of the seller
Expand Down Expand Up @@ -85,11 +96,6 @@ abstract contract DutchAuction is ReentrancyGuard {
/// @dev Thrown when trying to withdraw unsold assets when all items were sold
error NoUnsoldAssetsToWithdraw();

/// @dev Thrown when floor price is set higher than start price
/// @param floorPrice The specified floor price
/// @param startPrice The specified start price
error FloorPriceExceedsStartPrice(uint256 floorPrice, uint256 startPrice);

/// @dev Thrown when auction duration is set to zero
error InvalidDuration();

Expand Down Expand Up @@ -132,7 +138,6 @@ abstract contract DutchAuction is ReentrancyGuard {
uint256 _duration,
uint256 _inventory
) {
if (_floorPrice > _startPrice) revert FloorPriceExceedsStartPrice(_floorPrice, _startPrice);
if (_duration == 0) revert InvalidDuration();
if (_startTime < block.timestamp) revert StartTimeInPast(_startTime, block.timestamp);
if (_inventory == 0) revert InvalidInventory();
Expand Down Expand Up @@ -187,7 +192,8 @@ abstract contract DutchAuction is ReentrancyGuard {
* @notice Send all funds in the contract to the seller.
* @dev By default, this will send all funds to the seller.
* It is safe to send all funds, since items are purchased immediately, so no bids are left outstanding.
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning them)
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning
* them)
* When overriding, make sure to add necessary access control.
*/
function withdrawSellerProceeds() external virtual {
Expand Down Expand Up @@ -227,7 +233,8 @@ abstract contract DutchAuction is ReentrancyGuard {
/**
* @notice Gets the current price per item at the current timestamp.
* @dev By default, the price is a linear decrease from `startPrice` to `floorPrice` over `duration`.
* Override to implement custom curve, like exponential decay or even reverse dutch auction(where price starts low and increases over time)
* Override to implement custom curve, like exponential decay or even reverse dutch auction(where price starts
* low and increases over time)
* @return The current price per item.
*/
function currentPrice() public view virtual returns (uint256) {
Expand Down
22 changes: 15 additions & 7 deletions src/EnglishAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@ pragma solidity ^0.8.20;
* @dev
* This contract is designed to be inherited and extended.
* - Optional anti-sniping mechanism: If a bid arrives close to the end, the auction is extended.
* If you don't want to extend the auction in the case of a last minute bid, set the `extensionThreshold` or `extensionPeriod` to 0.
* If you don't want to extend the auction in the case of a last minute bid, set the `extensionThreshold` or
* `extensionPeriod` to 0.
*
* To use this contract, you must:
* 1. Provide an implementation of `_transferAssetToWinner(address winner)` that transfers the
* auctioned asset (e.g., an NFT) to the auction winner.
* 2. Optionally override `_beforeBid` or `_afterBid` to implement custom bidding logic such as
* 2. Provide an implementation of `_transferAssetToSeller()` that transfers the auctioned asset (e.g., an NFT) to the
* seller in case there's no winner of the auction.
* 3. Optionally override `_beforeBid` or `_afterBid` to implement custom bidding logic such as
* whitelisting or additional checks.
* 3. Optionally override `_validateBidIncrement` if you want to require a certain increment over the previous highest bid.
* 4. Optionally override `_validateBidIncrement` if you want to require a certain increment over the previous highest
* bid.
*
* If no valid bids are placed above the reserve price by the time the auction ends, anyone can simply finalize the auction and the asset will be returned to the seller.
* If no valid bids are placed above the reserve price by the time the auction ends, anyone can simply finalize the
* auction and the asset will be returned to the seller.
*/
abstract contract EnglishAuction {
/// @dev The address of the item’s seller
Expand Down Expand Up @@ -208,8 +213,10 @@ abstract contract EnglishAuction {

/**
* @notice Sends proceeds to the seller after the auction has been finalized.
* @dev Since `sellerProceeds` is only incremented when the auction is finalized, there's no need to check the status of the auction here.
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning them)
* @dev Since `sellerProceeds` is only incremented when the auction is finalized, there's no need to check the
* status of the auction here.
* Override to implement custom logic if necessary (e.g. sending the funds to a different address or burning
* them)
* When overriding, make sure to reset the sellerProceeds to 0 and add necessary access control.
*/
function withdrawSellerProceeds() external virtual {
Expand All @@ -222,7 +229,8 @@ abstract contract EnglishAuction {
}

/**
* @notice Finalizes the auction after it ends, transfering the asset to the winner and allowing the seller to withdraw the highest bid.
* @notice Finalizes the auction after it ends, transfering the asset to the winner and allowing the seller to
* withdraw the highest bid.
* @dev Anyone can call this after the auction has ended.
* If no valid bids above the reserve were placed, no transfer occurs and sellerProceeds remains zero.
* You need to override `_transferAssetToWinner` to implement the asset transfer logic.
Expand Down
3 changes: 2 additions & 1 deletion src/FirstPriceSealedBidAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import "./BaseSealedBidAuction.sol";
* @notice An abstract contract for a first-price sealed-bid auction.
* In this format, the highest bidder pays their own bid.
* @dev
* - Child contracts must still override `_transferAssetToWinner()` and `_returnAssetToSeller()` for handling the transfer of the specific asset.
* - Child contracts must still override `_transferAssetToWinner()` and `_returnAssetToSeller()` for handling the
* transfer of the specific asset.
* - Bids below the reserve price do not produce a winner.
*/
abstract contract FirstPriceSealedBidAuction is BaseSealedBidAuction {
Expand Down
9 changes: 6 additions & 3 deletions src/SecondPriceSealedBidAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import "./BaseSealedBidAuction.sol";
/**
* @title SecondPriceSealedBidAuction
* @notice An abstract contract for a second-price sealed-bid auction(Vickrey Auction).
* In this format, the highest bidder pays the second-highest bid(or the reserve price if there are no two bids above the reserve price).
* In this format, the highest bidder pays the second-highest bid(or the reserve price if there are no two bids
* above the reserve price).
* @dev
* - Child contracts must still override `_transferAssetToWinner()` and `_returnAssetToSeller()` for handling the transfer of the specific asset.
* - Child contracts must still override `_transferAssetToWinner()` and `_returnAssetToSeller()` for handling the
* transfer of the specific asset.
* - Bids below the reserve price do not produce a winner.
*/
abstract contract SecondPriceSealedBidAuction is BaseSealedBidAuction {
Expand Down Expand Up @@ -44,7 +46,8 @@ abstract contract SecondPriceSealedBidAuction is BaseSealedBidAuction {
*/
function _handleRevealedBid(address bidder, uint96 amount) internal virtual override {
uint96 currentHighestBid = highestBid;
// If the bid is the new highest bid, update highestBid and currentWinner, but also move the old highest bid to secondHighestBid
// If the bid is the new highest bid, update highestBid and currentWinner, but also move the old highest bid to
// secondHighestBid
if (amount > currentHighestBid) {
highestBid = amount;
currentWinner = bidder;
Expand Down
4 changes: 2 additions & 2 deletions src/examples/NftDutchAuction.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "../DutchAuction.sol"; // Import the provided DutchAuction abstract contract
import "../DutchAuction.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // Import the provided DutchAuction abstract contract

/**
* @title NftDutchAuction
Expand Down
Loading

0 comments on commit 8255e2d

Please sign in to comment.