From 6dd8520c3172a2f085dc304bd52b611b735560ef Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 17 May 2024 12:14:18 -0500 Subject: [PATCH 01/13] feat: fixed price batch implementation --- .../modules/auctions/IFixedPriceBatch.sol | 93 ++++ src/modules/auctions/FPB.sol | 443 ++++++++++++++++++ 2 files changed, 536 insertions(+) create mode 100644 src/interfaces/modules/auctions/IFixedPriceBatch.sol create mode 100644 src/modules/auctions/FPB.sol diff --git a/src/interfaces/modules/auctions/IFixedPriceBatch.sol b/src/interfaces/modules/auctions/IFixedPriceBatch.sol new file mode 100644 index 00000000..6946bb74 --- /dev/null +++ b/src/interfaces/modules/auctions/IFixedPriceBatch.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.19; + +import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; + +interface IFixedPriceBatch is IBatchAuction { + // ========== ERRORS ========== // + + error Auction_WrongState(uint96 lotId); + error Bid_WrongState(uint96 lotId, uint64 bidId); + error NotPermitted(address caller); + + // ========== DATA STRUCTURES ========== // + + /// @notice The status of an auction lot + enum LotStatus { + Created, + Settled + } + + /// @notice The status of a bid + /// @dev Bid status will also be set to claimed if the bid is cancelled/refunded + enum BidStatus { + Submitted, + Claimed + } + + struct AuctionDataParams { + uint256 price; + uint24 minFillPercent; + } + + struct AuctionData { + uint256 price; // 32 - slot 1 + LotStatus status; // 1 + + uint64 nextBidId; // 8 + + bool settlementCleared; // 1 = 10 - end of slot 2 + uint256 totalBidAmount; // 32 - slot 3 + uint256 minFilled; // 32 - slot 4 + } + + /// @notice Core data for a bid + /// + /// @param bidder The address of the bidder + /// @param amount The amount of the bid + /// @param referrer The address of the referrer + /// @param status The status of the bid + struct Bid { + address bidder; // 20 + + uint96 amount; // 12 = 32 - end of slot 1 + address referrer; // 20 + + BidStatus status; // 1 = 21 - end of slot 2 + } + + /// @notice Struct containing partial fill data for a lot + /// + /// @param bidId The ID of the bid + /// @param refund The amount to refund to the bidder + /// @param payout The amount to payout to the bidder + struct PartialFill { + uint64 bidId; // 8 + + uint96 refund; // 12 = 20 - end of slot 1 + uint256 payout; // 32 - slot 2 + } + + // ========== AUCTION INFORMATION ========== // + + /// @notice Returns the `Bid` and `EncryptedBid` data for a given lot and bid ID + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + /// @return bid The `Bid` data + function getBid(uint96 lotId_, uint64 bidId_) external view returns (Bid memory bid); + + /// @notice Returns the `AuctionData` data for an auction lot + /// + /// @param lotId_ The lot ID + /// @return auctionData_ The `AuctionData` + function getAuctionData(uint96 lotId_) + external + view + returns (AuctionData memory auctionData_); + + /// @notice Returns the `PartialFill` data for an auction lot + /// + /// @param lotId_ The lot ID + /// @return hasPartialFill True if a partial fill exists + /// @return partialFill The `PartialFill` data + function getPartialFill(uint96 lotId_) + external + view + returns (bool hasPartialFill, PartialFill memory partialFill); +} diff --git a/src/modules/auctions/FPB.sol b/src/modules/auctions/FPB.sol new file mode 100644 index 00000000..2137d4ca --- /dev/null +++ b/src/modules/auctions/FPB.sol @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: BSL-1.1 +pragma solidity 0.8.19; + +// Interfaces +import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +// External libraries +import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; + +// Auctions +import {AuctionModule} from "src/modules/Auction.sol"; +import {BatchAuctionModule} from "src/modules/auctions/BatchAuctionModule.sol"; + +import {Module, Veecode, toVeecode} from "src/modules/Modules.sol"; + +contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { + // ========== STATE VARIABLES ========== // + + /// @notice FPBA-specific auction data for a lot + /// @dev Access via `getAuctionData()` + mapping(uint96 lotId => AuctionData) internal _auctionData; + + /// @notice General information about bids on a lot + /// @dev Access via `getBid()` + mapping(uint96 lotId => mapping(uint64 bidId => Bid)) internal _bids; + + /// @notice Partial fill data for a lot + /// @dev Each FPBA can have at most one partial fill + /// Access via `getPartialFill()` + mapping(uint96 lotId => PartialFill) internal _lotPartialFill; + + // ========== SETUP ========== // + + constructor(address auctionHouse_) AuctionModule(auctionHouse_) { + // Set the minimum auction duration to 1 day initially + minAuctionDuration = 1 days; + + // Set the dedicated settle period to 1 day initially + dedicatedSettlePeriod = 1 days; + } + + /// @inheritdoc Module + function VEECODE() public pure override returns (Veecode) { + return toVeecode("01FPBA"); + } + + // ========== AUCTION ========== // + + /// @inheritdoc AuctionModule + /// @dev This function assumes: + /// - The lot ID has been validated + /// - The start and duration of the lot have been validated + /// + /// This function reverts if: + /// - The parameters cannot be decoded into the correct format + /// - The price is zero + /// + /// @param params_ ABI-encoded data of type `uint256` + function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal override { + // Decode the auction params + AuctionDataParams memory params = abi.decode(params_, (AuctionDataParams)); + + // Validate the price is not zero + if (params.price == 0) revert Auction_InvalidParams(); + + // minFillPercent must be less than or equal to 100% + if (params.minFillPercent > _ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + + // Set the auction data + AuctionData storage data = _auctionData[lotId_]; + data.price = params.price; + // data.status = LotStatus.Created; // Set by default + // data.nextBidId = 0; // Set by default + // data.totalBidAmount = 0; // Set by default + // We round up to be conservative with the minimums + data.minFilled = + Math.fullMulDivUp(lot_.capacity, params.minFillPercent, _ONE_HUNDRED_PERCENT); + } + + /// @inheritdoc AuctionModule + /// @dev This function assumes the following: + /// - The lot ID has been validated + /// - The caller has been authorized + /// - The auction has not concluded + /// + /// This function performs the following: + /// - Sets the auction status to settled, and prevents claiming of proceeds + /// + /// This function reverts if: + /// - The auction is active or has not concluded + function _cancelAuction(uint96 lotId_) internal override { + // Validation + // Batch auctions cannot be cancelled once started, otherwise the seller could cancel the auction after bids have been submitted + _revertIfLotActive(lotId_); + + // Set auction status to settled + // No bids could have been submitted at this point so there will not be any claimed + _auctionData[lotId_].status = LotStatus.Settled; + } + + // ========== BID ========== // + + /// @inheritdoc BatchAuctionModule + /// @dev This function performs the following: + /// - Validates inputs + /// - Stores the bid + /// - Conditionally ends the auction if the bid fills the lot (bid may be partial fill) + /// - Returns the bid ID + /// + /// This function assumes: + /// - The lot ID has been validated + /// - The caller has been authorized + /// - The auction is active + /// + /// This function reverts if: + /// - Amount is zero + /// - Amount is greater than the maximum uint96 + function _bid( + uint96 lotId_, + address bidder_, + address referrer_, + uint256 amount_, + bytes calldata + ) internal override returns (uint64 bidId) { + // Amount cannot be zero or greater than the maximum uint96 + if (amount_ == 0 || amount_ > type(uint96).max) revert Auction_InvalidParams(); + + // Load the lot and auction data + Lot memory lot = lotData[lotId_]; + AuctionData storage data = _auctionData[lotId_]; + + // Get the bid ID and increment the next bid ID + bidId = data.nextBidId++; + + // Store the bid + _bids[lotId_][bidId] = Bid({ + bidder: bidder_, + amount: uint96(amount_), + referrer: referrer_, + status: BidStatus.Submitted + }); + + // Increment the total bid amount + data.totalBidAmount += amount_; + + // Calculate the new filled capacity including this bid + // If greater than or equal to the lot capacity, the auction should end + // If strictly greater than, then this bid is a partial fill + // If not, then the payout is calculated at the full amount and the auction proceeds + uint256 baseScale = 10 ** lotData[lotId_].baseTokenDecimals; + uint256 newFilledCapacity = Math.fullMulDiv(data.totalBidAmount, baseScale, data.price); + if (newFilledCapacity >= lot.capacity) { + // If partial fill, then calculate new payout and refund + if (newFilledCapacity > lot.capacity) { + // Calculate the new payout from the remaining capacity + uint256 payout = lot.capacity + - Math.fullMulDiv(data.totalBidAmount - amount_, baseScale, data.price); + uint256 refund = amount_ - Math.fullMulDivUp(payout, data.price, baseScale); // TODO rounding up to prevent refund from being too large, check this + + // Store the partial fill + // We can cast refund to uint96 because it is less than amount_ which is less than type(uint96).max + _lotPartialFill[lotId_] = + PartialFill({bidId: bidId, refund: uint96(refund), payout: payout}); + + // Decrement the total bid amount by the refund + data.totalBidAmount -= refund; + } + + // End the auction and settle the lot since no more bids can be submitted + // This is atypical ordering, but allows claims to be made immediately without another transaction to settle + lotData[lotId_].conclusion = uint48(block.timestamp); + _settle(lotId_, 0); // we could use the return variables, but we know that they are the same as totalBidAmount and the capacity since this path is only invoked when the auction sells out + + // Set the lot values that are typically set in BatchAuctionModule.settle + lotData[lotId_].purchased = data.totalBidAmount; + lotData[lotId_].sold = lot.capacity; + // There is no auction output, so we don't need to set it + } + } + + function _refundBid( + uint96 lotId_, + uint64 bidId_, + uint256, + address + ) internal override returns (uint256 refund) { + // Load auction and bid data + AuctionData storage data = _auctionData[lotId_]; + Bid storage bid = _bids[lotId_][bidId_]; + + // Update the bid status + bid.status = BidStatus.Claimed; + + // Update the total bid amount + data.totalBidAmount -= bid.amount; + + // Refund is the bid amount + refund = bid.amount; + } + + function _claimBids( + uint96 lotId_, + uint64[] calldata bidIds_ + ) internal override returns (BidClaim[] memory bidClaims, bytes memory auctionOutput) { + uint256 len = bidIds_.length; + bidClaims = new BidClaim[](len); + for (uint256 i; i < len; i++) { + // Validate + _revertIfBidInvalid(lotId_, bidIds_[i]); + _revertIfBidClaimed(lotId_, bidIds_[i]); + + // Set the bid status to claimed + _bids[lotId_][bidIds_[i]].status = BidStatus.Claimed; + + // Load the bid claim data + bidClaims[i] = _getBidClaim(lotId_, bidIds_[i]); + } + + return (bidClaims, auctionOutput); + } + + // ========== SETTLEMENT ========== // + + function _settle( + uint96 lotId_, + uint256 + ) + internal + override + returns (uint256 totalIn_, uint256 totalOut_, bool finished_, bytes memory auctionOutput_) + { + // Set the auction status to settled + _auctionData[lotId_].status = LotStatus.Settled; + + // Calculate the filled capacity + uint256 filledCapacity = Math.fullMulDiv( + _auctionData[lotId_].totalBidAmount, + 10 ** lotData[lotId_].baseTokenDecimals, + _auctionData[lotId_].price + ); + + // If the filled capacity is less than the minimum filled capacity, the auction will not clear + if (filledCapacity < _auctionData[lotId_].minFilled) { + // Doesn't clear so we don't set the settlementCleared flag before returning + + // totalIn and totalOut are not set since the auction does not clear + return (totalIn_, totalOut_, true, auctionOutput_); + } + + // Otherwise, the auction will clear so we set the settlementCleared flag + _auctionData[lotId_].settlementCleared = true; + + // Set the output values + totalIn_ = _auctionData[lotId_].totalBidAmount; + totalOut_ = filledCapacity; + finished_ = true; + } + + function _abort(uint96 lotId_) internal override { + // Set the auction status to settled + _auctionData[lotId_].status = LotStatus.Settled; + + // Auction doesn't clear so we don't set the settlementCleared flag + } + + // ========== AUCTION INFORMATION ========== // + + /// @inheritdoc IFixedPriceBatch + /// @dev This function reverts if: + /// - The lot ID is invalid + /// - The bid ID is invalid + function getBid(uint96 lotId_, uint64 bidId_) external view returns (Bid memory bid) { + _revertIfLotInvalid(lotId_); + _revertIfBidInvalid(lotId_, bidId_); + + return _bids[lotId_][bidId_]; + } + + /// @inheritdoc IFixedPriceBatch + /// @dev This function reverts if: + /// - The lot ID is invalid + function getAuctionData(uint96 lotId_) + external + view + override + returns (AuctionData memory auctionData_) + { + _revertIfLotInvalid(lotId_); + + return _auctionData[lotId_]; + } + + /// @inheritdoc IFixedPriceBatch + /// @dev For ease of use, this function determines if a partial fill exists. + /// + /// This function reverts if: + /// - The lot ID is invalid + /// - The lot is not settled + function getPartialFill(uint96 lotId_) + external + view + returns (bool hasPartialFill, PartialFill memory partialFill) + { + _revertIfLotInvalid(lotId_); + _revertIfLotNotSettled(lotId_); + + partialFill = _lotPartialFill[lotId_]; + hasPartialFill = partialFill.bidId != 0; + + return (hasPartialFill, partialFill); + } + + /// @inheritdoc IBatchAuction + /// @dev This function is not implemented in fixed price batch since bid IDs are not stored in an array + /// A proxy is using the nextBidId to determine how many bids have been submitted, but this doesn't consider refunds + function getNumBids(uint96) external view override returns (uint256) {} + + /// @inheritdoc IBatchAuction + /// @dev This function is not implemented in fixed price batch since bid IDs are not stored in an array + function getBidIds(uint96, uint256, uint256) external view override returns (uint64[] memory) {} + + /// @inheritdoc IBatchAuction + /// @dev This function is not implemented in fixed price batch since bid IDs are not stored in an array + function getBidIdAtIndex(uint96, uint256) external view override returns (uint64) {} + + /// @inheritdoc IBatchAuction + /// @dev This function reverts if: + /// - The lot ID is invalid + /// - The lot is not settled (since there would be no claim) + /// - The bid ID is invalid + function getBidClaim( + uint96 lotId_, + uint64 bidId_ + ) external view override returns (BidClaim memory bidClaim) { + _revertIfLotInvalid(lotId_); + _revertIfLotNotSettled(lotId_); + _revertIfBidInvalid(lotId_, bidId_); + + return _getBidClaim(lotId_, bidId_); + } + + /// @notice Returns the `BidClaim` data for a given lot and bid ID + /// @dev This function assumes: + /// - The lot ID has been validated + /// - The bid ID has been validated + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + /// @return bidClaim The `BidClaim` data + function _getBidClaim( + uint96 lotId_, + uint64 bidId_ + ) internal view returns (BidClaim memory bidClaim) { + // Load bid data + Bid memory bidData = _bids[lotId_][bidId_]; + + // Load the bidder and referrer addresses + bidClaim.bidder = bidData.bidder; + bidClaim.referrer = bidData.referrer; + + if (_auctionData[lotId_].settlementCleared) { + // settlement cleared, so bids are paid out + + if (_lotPartialFill[lotId_].bidId == bidId_) { + // Partial fill, use the stored data + bidClaim.paid = bidData.amount; + bidClaim.payout = _lotPartialFill[lotId_].payout; + bidClaim.refund = _lotPartialFill[lotId_].refund; + } else { + // Bid is paid out at full amount using the fixed price + bidClaim.paid = bidData.amount; + bidClaim.payout = Math.fullMulDiv( + bidData.amount, + 10 ** lotData[lotId_].baseTokenDecimals, + _auctionData[lotId_].price + ); + bidClaim.refund = 0; + } + } else { + // settlement not cleared, so bids are refunded + bidClaim.paid = bidData.amount; + bidClaim.payout = 0; + bidClaim.refund = bidData.amount; + } + + return bidClaim; + } + + // ========== VALIDATION ========== // + + /// @inheritdoc AuctionModule + function _revertIfLotActive(uint96 lotId_) internal view override { + if ( + _auctionData[lotId_].status == LotStatus.Created + && lotData[lotId_].start <= block.timestamp + && lotData[lotId_].conclusion > block.timestamp + ) revert Auction_WrongState(lotId_); + } + + /// @inheritdoc BatchAuctionModule + function _revertIfLotSettled(uint96 lotId_) internal view override { + // Auction must not be settled + if (_auctionData[lotId_].status == LotStatus.Settled) { + revert Auction_WrongState(lotId_); + } + } + + /// @inheritdoc BatchAuctionModule + function _revertIfLotNotSettled(uint96 lotId_) internal view override { + // Auction must be settled + if (_auctionData[lotId_].status != LotStatus.Settled) { + revert Auction_WrongState(lotId_); + } + } + + /// @inheritdoc BatchAuctionModule + function _revertIfBidInvalid(uint96 lotId_, uint64 bidId_) internal view override { + // Bid ID must be less than number of bids for lot + if (bidId_ >= _auctionData[lotId_].nextBidId) revert Auction_InvalidBidId(lotId_, bidId_); + + // Bid should have a bidder + if (_bids[lotId_][bidId_].bidder == address(0)) revert Auction_InvalidBidId(lotId_, bidId_); + } + + /// @inheritdoc BatchAuctionModule + function _revertIfNotBidOwner( + uint96 lotId_, + uint64 bidId_, + address caller_ + ) internal view override { + // Check that sender is the bidder + if (caller_ != _bids[lotId_][bidId_].bidder) revert NotPermitted(caller_); + } + + /// @inheritdoc BatchAuctionModule + function _revertIfBidClaimed(uint96 lotId_, uint64 bidId_) internal view override { + // Bid must not be refunded or claimed (same status) + if (_bids[lotId_][bidId_].status == BidStatus.Claimed) { + revert Bid_WrongState(lotId_, bidId_); + } + } +} From aa9e0cf0ef0cf774b5fb20cede94c70d4c37219b Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 17 May 2024 12:24:18 -0500 Subject: [PATCH 02/13] fix: remove early settle (was bad idea) --- src/modules/auctions/FPB.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/modules/auctions/FPB.sol b/src/modules/auctions/FPB.sol index 2137d4ca..47a182e9 100644 --- a/src/modules/auctions/FPB.sol +++ b/src/modules/auctions/FPB.sol @@ -167,15 +167,9 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { data.totalBidAmount -= refund; } - // End the auction and settle the lot since no more bids can be submitted - // This is atypical ordering, but allows claims to be made immediately without another transaction to settle + // End the auction + // We don't settle here to preserve callback and storage interactions associated with calling "settle" lotData[lotId_].conclusion = uint48(block.timestamp); - _settle(lotId_, 0); // we could use the return variables, but we know that they are the same as totalBidAmount and the capacity since this path is only invoked when the auction sells out - - // Set the lot values that are typically set in BatchAuctionModule.settle - lotData[lotId_].purchased = data.totalBidAmount; - lotData[lotId_].sold = lot.capacity; - // There is no auction output, so we don't need to set it } } From 6a0969d17ab63874fddd49b03f58077291e1c553 Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 17 May 2024 15:50:35 -0500 Subject: [PATCH 03/13] chore: organize auctions into atomic and batch folders --- script/deploy/Deploy.s.sol | 4 ++-- script/deploy/DeployBlast.s.sol | 4 ++-- script/ops/test/TestData.s.sol | 2 +- .../modules/auctions/{ => atomic}/BlastFPS.sol | 2 +- .../modules/auctions/{ => batch}/BlastEMP.sol | 2 +- src/blast/modules/auctions/batch/BlastFPB.sol | 14 ++++++++++++++ src/modules/auctions/{ => atomic}/FPS.sol | 0 src/modules/auctions/{ => batch}/EMP.sol | 0 src/modules/auctions/{ => batch}/FPB.sol | 0 test/modules/auctions/EMP/EMPTest.sol | 2 +- test/modules/auctions/EMP/abort.t.sol | 2 +- test/modules/auctions/EMP/auction.t.sol | 2 +- test/modules/auctions/EMP/bid.t.sol | 2 +- test/modules/auctions/EMP/cancelAuction.t.sol | 2 +- test/modules/auctions/EMP/claimBids.t.sol | 2 +- test/modules/auctions/EMP/decryptAndSortBids.t.sol | 2 +- test/modules/auctions/EMP/refundBid.t.sol | 2 +- test/modules/auctions/EMP/settle.t.sol | 2 +- test/modules/auctions/EMP/submitPrivateKey.t.sol | 2 +- test/modules/auctions/FPS/FPSTest.sol | 2 +- test/modules/auctions/FPS/auction.t.sol | 2 +- .../derivatives/LinearVestingEMPAIntegration.t.sol | 2 +- 22 files changed, 34 insertions(+), 20 deletions(-) rename src/blast/modules/auctions/{ => atomic}/BlastFPS.sol (83%) rename src/blast/modules/auctions/{ => batch}/BlastEMP.sol (83%) create mode 100644 src/blast/modules/auctions/batch/BlastFPB.sol rename src/modules/auctions/{ => atomic}/FPS.sol (100%) rename src/modules/auctions/{ => batch}/EMP.sol (100%) rename src/modules/auctions/{ => batch}/FPB.sol (100%) diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index 75da3adf..46b9c461 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -16,8 +16,8 @@ import {Module} from "src/modules/Modules.sol"; import {Callbacks} from "src/lib/Callbacks.sol"; // Auction modules -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; -import {FixedPriceSale} from "src/modules/auctions/FPS.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; +import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; // Derivative modules import {LinearVesting} from "src/modules/derivatives/LinearVesting.sol"; diff --git a/script/deploy/DeployBlast.s.sol b/script/deploy/DeployBlast.s.sol index ede04431..1b318253 100644 --- a/script/deploy/DeployBlast.s.sol +++ b/script/deploy/DeployBlast.s.sol @@ -6,8 +6,8 @@ import {console2} from "forge-std/Script.sol"; // System contracts import {BlastAtomicAuctionHouse} from "src/blast/BlastAtomicAuctionHouse.sol"; import {BlastBatchAuctionHouse} from "src/blast/BlastBatchAuctionHouse.sol"; -import {BlastEMP} from "src/blast/modules/auctions/BlastEMP.sol"; -import {BlastFPS} from "src/blast/modules/auctions/BlastFPS.sol"; +import {BlastEMP} from "src/blast/modules/auctions/batch/BlastEMP.sol"; +import {BlastFPS} from "src/blast/modules/auctions/atomic/BlastFPS.sol"; import {BlastLinearVesting} from "src/blast/modules/derivatives/BlastLinearVesting.sol"; import {Deploy} from "script/deploy/Deploy.s.sol"; diff --git a/script/ops/test/TestData.s.sol b/script/ops/test/TestData.s.sol index c5264820..760b7525 100644 --- a/script/ops/test/TestData.s.sol +++ b/script/ops/test/TestData.s.sol @@ -8,7 +8,7 @@ import {Script, console2} from "forge-std/Script.sol"; import {BatchAuctionHouse} from "src/BatchAuctionHouse.sol"; import {IAuctionHouse} from "src/interfaces/IAuctionHouse.sol"; import {toKeycode, toVeecode} from "src/modules/Modules.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {ECIES, Point} from "src/lib/ECIES.sol"; import {uint2str} from "src/lib/Uint2Str.sol"; diff --git a/src/blast/modules/auctions/BlastFPS.sol b/src/blast/modules/auctions/atomic/BlastFPS.sol similarity index 83% rename from src/blast/modules/auctions/BlastFPS.sol rename to src/blast/modules/auctions/atomic/BlastFPS.sol index 0486c12f..c7c80139 100644 --- a/src/blast/modules/auctions/BlastFPS.sol +++ b/src/blast/modules/auctions/atomic/BlastFPS.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSL-1.1 pragma solidity 0.8.19; -import {FixedPriceSale} from "src/modules/auctions/FPS.sol"; +import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; import {BlastGas} from "src/blast/modules/BlastGas.sol"; contract BlastFPS is FixedPriceSale, BlastGas { diff --git a/src/blast/modules/auctions/BlastEMP.sol b/src/blast/modules/auctions/batch/BlastEMP.sol similarity index 83% rename from src/blast/modules/auctions/BlastEMP.sol rename to src/blast/modules/auctions/batch/BlastEMP.sol index e737dc83..47481922 100644 --- a/src/blast/modules/auctions/BlastEMP.sol +++ b/src/blast/modules/auctions/batch/BlastEMP.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BSL-1.1 pragma solidity 0.8.19; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {BlastGas} from "src/blast/modules/BlastGas.sol"; contract BlastEMP is EncryptedMarginalPrice, BlastGas { diff --git a/src/blast/modules/auctions/batch/BlastFPB.sol b/src/blast/modules/auctions/batch/BlastFPB.sol new file mode 100644 index 00000000..10776890 --- /dev/null +++ b/src/blast/modules/auctions/batch/BlastFPB.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSL-1.1 +pragma solidity 0.8.19; + +import {FixedPriceBatch} from "src/modules/auctions/batch/FPB.sol"; +import {BlastGas} from "src/blast/modules/BlastGas.sol"; + +contract BlastEMP is FixedPriceBatch, BlastGas { + // ========== CONSTRUCTOR ========== // + + constructor( + address auctionHouse_, + address blast_ + ) FixedPriceBatch(auctionHouse_) BlastGas(auctionHouse_, blast_) {} +} diff --git a/src/modules/auctions/FPS.sol b/src/modules/auctions/atomic/FPS.sol similarity index 100% rename from src/modules/auctions/FPS.sol rename to src/modules/auctions/atomic/FPS.sol diff --git a/src/modules/auctions/EMP.sol b/src/modules/auctions/batch/EMP.sol similarity index 100% rename from src/modules/auctions/EMP.sol rename to src/modules/auctions/batch/EMP.sol diff --git a/src/modules/auctions/FPB.sol b/src/modules/auctions/batch/FPB.sol similarity index 100% rename from src/modules/auctions/FPB.sol rename to src/modules/auctions/batch/FPB.sol diff --git a/test/modules/auctions/EMP/EMPTest.sol b/test/modules/auctions/EMP/EMPTest.sol index 0451fe24..f6b2ddb6 100644 --- a/test/modules/auctions/EMP/EMPTest.sol +++ b/test/modules/auctions/EMP/EMPTest.sol @@ -13,7 +13,7 @@ import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Modules import {BatchAuctionHouse} from "src/BatchAuctionHouse.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; abstract contract EmpTest is Test, Permit2User { diff --git a/test/modules/auctions/EMP/abort.t.sol b/test/modules/auctions/EMP/abort.t.sol index a1154511..72872123 100644 --- a/test/modules/auctions/EMP/abort.t.sol +++ b/test/modules/auctions/EMP/abort.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/auction.t.sol b/test/modules/auctions/EMP/auction.t.sol index 62c9523a..278c8899 100644 --- a/test/modules/auctions/EMP/auction.t.sol +++ b/test/modules/auctions/EMP/auction.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/bid.t.sol b/test/modules/auctions/EMP/bid.t.sol index 7b836d50..71fe9bf6 100644 --- a/test/modules/auctions/EMP/bid.t.sol +++ b/test/modules/auctions/EMP/bid.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {Point} from "src/lib/ECIES.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/cancelAuction.t.sol b/test/modules/auctions/EMP/cancelAuction.t.sol index 0cd1b10c..d9bae948 100644 --- a/test/modules/auctions/EMP/cancelAuction.t.sol +++ b/test/modules/auctions/EMP/cancelAuction.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/claimBids.t.sol b/test/modules/auctions/EMP/claimBids.t.sol index d992cc44..04093cc4 100644 --- a/test/modules/auctions/EMP/claimBids.t.sol +++ b/test/modules/auctions/EMP/claimBids.t.sol @@ -7,7 +7,7 @@ import {console2} from "forge-std/console2.sol"; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/decryptAndSortBids.t.sol b/test/modules/auctions/EMP/decryptAndSortBids.t.sol index 924875b4..dfaf4480 100644 --- a/test/modules/auctions/EMP/decryptAndSortBids.t.sol +++ b/test/modules/auctions/EMP/decryptAndSortBids.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/refundBid.t.sol b/test/modules/auctions/EMP/refundBid.t.sol index ddd51418..d18faa51 100644 --- a/test/modules/auctions/EMP/refundBid.t.sol +++ b/test/modules/auctions/EMP/refundBid.t.sol @@ -6,7 +6,7 @@ import {console2} from "forge-std/console2.sol"; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/EMP/settle.t.sol b/test/modules/auctions/EMP/settle.t.sol index 714cae41..078e8beb 100644 --- a/test/modules/auctions/EMP/settle.t.sol +++ b/test/modules/auctions/EMP/settle.t.sol @@ -5,7 +5,7 @@ import {FixedPointMathLib as Math} from "solmate/utils/FixedPointMathLib.sol"; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; import {BidEncoding} from "src/lib/MaxPriorityQueue.sol"; diff --git a/test/modules/auctions/EMP/submitPrivateKey.t.sol b/test/modules/auctions/EMP/submitPrivateKey.t.sol index 90336839..2fe48dec 100644 --- a/test/modules/auctions/EMP/submitPrivateKey.t.sol +++ b/test/modules/auctions/EMP/submitPrivateKey.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IEncryptedMarginalPrice} from "src/interfaces/modules/auctions/IEncryptedMarginalPrice.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {EmpTest} from "test/modules/auctions/EMP/EMPTest.sol"; diff --git a/test/modules/auctions/FPS/FPSTest.sol b/test/modules/auctions/FPS/FPSTest.sol index e5e2b5cf..02acca73 100644 --- a/test/modules/auctions/FPS/FPSTest.sol +++ b/test/modules/auctions/FPS/FPSTest.sol @@ -11,7 +11,7 @@ import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Modules import {AtomicAuctionHouse} from "src/AtomicAuctionHouse.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {FixedPriceSale} from "src/modules/auctions/FPS.sol"; +import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; import {IFixedPriceSale} from "src/interfaces/modules/auctions/IFixedPriceSale.sol"; abstract contract FpsTest is Test, Permit2User { diff --git a/test/modules/auctions/FPS/auction.t.sol b/test/modules/auctions/FPS/auction.t.sol index 999e65f4..1e56dc3e 100644 --- a/test/modules/auctions/FPS/auction.t.sol +++ b/test/modules/auctions/FPS/auction.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; import {Module} from "src/modules/Modules.sol"; import {IAuction} from "src/interfaces/modules/IAuction.sol"; -import {FixedPriceSale} from "src/modules/auctions/FPS.sol"; +import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; import {FixedPointMathLib as Math} from "solmate/utils/FixedPointMathLib.sol"; import {FpsTest} from "test/modules/auctions/FPS/FPSTest.sol"; diff --git a/test/modules/derivatives/LinearVestingEMPAIntegration.t.sol b/test/modules/derivatives/LinearVestingEMPAIntegration.t.sol index a18d6149..bbb906c3 100644 --- a/test/modules/derivatives/LinearVestingEMPAIntegration.t.sol +++ b/test/modules/derivatives/LinearVestingEMPAIntegration.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; import {IBatchAuctionHouse} from "src/interfaces/IBatchAuctionHouse.sol"; -import {EncryptedMarginalPrice} from "src/modules/auctions/EMP.sol"; +import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {ILinearVesting} from "src/interfaces/modules/derivatives/ILinearVesting.sol"; import {LinearVesting} from "src/modules/derivatives/LinearVesting.sol"; import {Point, ECIES} from "src/lib/ECIES.sol"; From 8ce566dd452ffb842b565318b424b12ca2c0321d Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 20 May 2024 14:46:52 -0500 Subject: [PATCH 04/13] fix: port changes from baseline-v2 branch --- .../modules/auctions/IFixedPriceBatch.sol | 13 ++ src/modules/auctions/batch/FPB.sol | 129 ++++++++++++++---- 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/interfaces/modules/auctions/IFixedPriceBatch.sol b/src/interfaces/modules/auctions/IFixedPriceBatch.sol index 6946bb74..e29995a6 100644 --- a/src/interfaces/modules/auctions/IFixedPriceBatch.sol +++ b/src/interfaces/modules/auctions/IFixedPriceBatch.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; +/// @notice Interface for fixed price batch auctions interface IFixedPriceBatch is IBatchAuction { // ========== ERRORS ========== // @@ -25,11 +26,23 @@ interface IFixedPriceBatch is IBatchAuction { Claimed } + /// @notice Parameters for a fixed price auction + /// + /// @param price The fixed price of the lot + /// @param minFillPercent The minimum percentage of the lot that must be filled in order to settle (100% = 1e5) struct AuctionDataParams { uint256 price; uint24 minFillPercent; } + /// @notice Core data for an auction lot + /// + /// @param price The price of the lot + /// @param status The status of the lot + /// @param nextBidId The ID of the next bid + /// @param settlementCleared True if the settlement has been cleared + /// @param totalBidAmount The total amount of all bids + /// @param minFilled The minimum amount of the lot that must be filled in order to settle struct AuctionData { uint256 price; // 32 - slot 1 LotStatus status; // 1 + diff --git a/src/modules/auctions/batch/FPB.sol b/src/modules/auctions/batch/FPB.sol index 47a182e9..31daa650 100644 --- a/src/modules/auctions/batch/FPB.sol +++ b/src/modules/auctions/batch/FPB.sol @@ -14,6 +14,8 @@ import {BatchAuctionModule} from "src/modules/auctions/BatchAuctionModule.sol"; import {Module, Veecode, toVeecode} from "src/modules/Modules.sol"; +/// @title FixedPriceBatch +/// @notice A module for creating fixed price batch auctions contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // ========== STATE VARIABLES ========== // @@ -55,8 +57,9 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { /// This function reverts if: /// - The parameters cannot be decoded into the correct format /// - The price is zero + /// - The minimum fill percentage is greater than 100% /// - /// @param params_ ABI-encoded data of type `uint256` + /// @param params_ ABI-encoded data of type `AuctionDataParams` function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal override { // Decode the auction params AuctionDataParams memory params = abi.decode(params_, (AuctionDataParams)); @@ -70,8 +73,8 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // Set the auction data AuctionData storage data = _auctionData[lotId_]; data.price = params.price; + data.nextBidId = 1; // data.status = LotStatus.Created; // Set by default - // data.nextBidId = 0; // Set by default // data.totalBidAmount = 0; // Set by default // We round up to be conservative with the minimums data.minFilled = @@ -101,6 +104,26 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // ========== BID ========== // + function _calculatePartialFill( + uint64 bidId_, + uint256 capacity_, + uint256 capacityExpended_, + uint96 bidAmount_, + uint256 baseScale_, + uint256 price_ + ) internal pure returns (PartialFill memory) { + // Calculate the bid payout if it were fully filled + uint256 fullFill = Math.fullMulDiv(bidAmount_, baseScale_, price_); + uint256 excess = capacityExpended_ - capacity_; + + // Refund will be within the bounds of uint96 + // bidAmount is uint96, excess < fullFill, so bidAmount * excess / fullFill < bidAmount < uint96 max + uint96 refund = uint96(Math.fullMulDiv(bidAmount_, excess, fullFill)); + uint256 payout = fullFill - excess; + + return (PartialFill({bidId: bidId_, refund: refund, payout: payout})); + } + /// @inheritdoc BatchAuctionModule /// @dev This function performs the following: /// - Validates inputs @@ -122,21 +145,24 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { address referrer_, uint256 amount_, bytes calldata - ) internal override returns (uint64 bidId) { + ) internal override returns (uint64) { // Amount cannot be zero or greater than the maximum uint96 if (amount_ == 0 || amount_ > type(uint96).max) revert Auction_InvalidParams(); // Load the lot and auction data - Lot memory lot = lotData[lotId_]; + uint256 lotCapacity = lotData[lotId_].capacity; AuctionData storage data = _auctionData[lotId_]; // Get the bid ID and increment the next bid ID - bidId = data.nextBidId++; + uint64 bidId = data.nextBidId++; + + // Has already been checked to be in bounds + uint96 amount96 = uint96(amount_); // Store the bid _bids[lotId_][bidId] = Bid({ bidder: bidder_, - amount: uint96(amount_), + amount: amount96, referrer: referrer_, status: BidStatus.Submitted }); @@ -150,29 +176,41 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // If not, then the payout is calculated at the full amount and the auction proceeds uint256 baseScale = 10 ** lotData[lotId_].baseTokenDecimals; uint256 newFilledCapacity = Math.fullMulDiv(data.totalBidAmount, baseScale, data.price); - if (newFilledCapacity >= lot.capacity) { - // If partial fill, then calculate new payout and refund - if (newFilledCapacity > lot.capacity) { - // Calculate the new payout from the remaining capacity - uint256 payout = lot.capacity - - Math.fullMulDiv(data.totalBidAmount - amount_, baseScale, data.price); - uint256 refund = amount_ - Math.fullMulDivUp(payout, data.price, baseScale); // TODO rounding up to prevent refund from being too large, check this - - // Store the partial fill - // We can cast refund to uint96 because it is less than amount_ which is less than type(uint96).max - _lotPartialFill[lotId_] = - PartialFill({bidId: bidId, refund: uint96(refund), payout: payout}); - - // Decrement the total bid amount by the refund - data.totalBidAmount -= refund; - } - // End the auction - // We don't settle here to preserve callback and storage interactions associated with calling "settle" - lotData[lotId_].conclusion = uint48(block.timestamp); + // If the new filled capacity is less than the lot capacity, the auction continues + if (newFilledCapacity < lotCapacity) { + return bidId; } + + // If partial fill, then calculate new payout and refund + if (newFilledCapacity > lotCapacity) { + // Store the partial fill + _lotPartialFill[lotId_] = _calculatePartialFill( + bidId, lotCapacity, newFilledCapacity, amount96, baseScale, data.price + ); + + // Decrement the total bid amount by the refund + data.totalBidAmount -= _lotPartialFill[lotId_].refund; + } + + // End the auction + // We don't settle here to preserve callback and storage interactions associated with calling "settle" + lotData[lotId_].conclusion = uint48(block.timestamp); + + return bidId; } + /// @inheritdoc BatchAuctionModule + /// @dev This function performs the following: + /// - Marks the bid as claimed + /// - Returns the amount to be refunded + /// + /// This function assumes: + /// - The lot ID has been validated + /// - The bid ID has been validated + /// - The caller has been authorized + /// - The auction is active + /// - The bid has not been refunded function _refundBid( uint96 lotId_, uint64 bidId_, @@ -193,6 +231,21 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { refund = bid.amount; } + /// @inheritdoc BatchAuctionModule + /// @dev This function performs the following: + /// - Validates the bid + /// - Marks the bid as claimed + /// - Calculates the payout and refund + /// + /// This function assumes: + /// - The lot ID has been validated + /// - The caller has been authorized + /// - The auction has concluded + /// - The auction is not settled + /// + /// This function reverts if: + /// - The bid ID is invalid + /// - The bid has already been claimed function _claimBids( uint96 lotId_, uint64[] calldata bidIds_ @@ -216,6 +269,21 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // ========== SETTLEMENT ========== // + /// @inheritdoc BatchAuctionModule + /// @dev This function performs the following: + /// - Sets the auction status to settled + /// - Calculates the filled capacity + /// - If the filled capacity is less than the minimum filled capacity, the auction does not clear + /// - If the filled capacity is greater than or equal to the minimum filled capacity, the auction clears + /// - Returns the total in, total out, and whether the auction is finished + /// + /// This function assumes: + /// - The lot ID has been validated + /// - The auction has concluded + /// - The auction is not settled + /// + /// This function reverts if: + /// - None function _settle( uint96 lotId_, uint256 @@ -251,6 +319,17 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { finished_ = true; } + /// @inheritdoc BatchAuctionModule + /// @dev This function performs the following: + /// - Sets the auction status to Settled + /// + /// This function assumes: + /// - The lot ID has been validated + /// - The auction is not settled + /// - The dedicated settle period has not passed + /// + /// This function reverts if: + /// - None function _abort(uint96 lotId_) internal override { // Set the auction status to settled _auctionData[lotId_].status = LotStatus.Settled; From 1fb71e59474095565fae49361197fb38a0860561 Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 20 May 2024 14:47:05 -0500 Subject: [PATCH 05/13] test: port tests from baseline-v2 branch --- test/modules/auctions/FPB/FPBTest.sol | 250 ++++++++ test/modules/auctions/FPB/auction.t.sol | 121 ++++ test/modules/auctions/FPB/bid.t.sol | 563 ++++++++++++++++++ test/modules/auctions/FPB/cancelAuction.t.sol | 118 ++++ test/modules/auctions/FPB/claimBids.t.sol | 290 +++++++++ test/modules/auctions/FPB/refundBid.t.sol | 177 ++++++ test/modules/auctions/FPB/settle.t.sol | 184 ++++++ 7 files changed, 1703 insertions(+) create mode 100644 test/modules/auctions/FPB/FPBTest.sol create mode 100644 test/modules/auctions/FPB/auction.t.sol create mode 100644 test/modules/auctions/FPB/bid.t.sol create mode 100644 test/modules/auctions/FPB/cancelAuction.t.sol create mode 100644 test/modules/auctions/FPB/claimBids.t.sol create mode 100644 test/modules/auctions/FPB/refundBid.t.sol create mode 100644 test/modules/auctions/FPB/settle.t.sol diff --git a/test/modules/auctions/FPB/FPBTest.sol b/test/modules/auctions/FPB/FPBTest.sol new file mode 100644 index 00000000..519499ea --- /dev/null +++ b/test/modules/auctions/FPB/FPBTest.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {FixedPointMathLib as Math} from "lib/solmate/src/utils/FixedPointMathLib.sol"; + +// Mocks +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +// Modules +import {BatchAuctionHouse} from "src/BatchAuctionHouse.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; +import {FixedPriceBatch} from "src/modules/auctions/batch/FPB.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +abstract contract FpbTest is Test, Permit2User { + uint256 internal constant _BASE_SCALE = 1e18; + + address internal constant _PROTOCOL = address(0x2); + address internal constant _BIDDER = address(0x3); + address internal constant _REFERRER = address(0x4); + + uint256 internal constant _LOT_CAPACITY = 10e18; + uint48 internal constant _DURATION = 1 days; + uint24 internal constant _MIN_FILL_PERCENT = 5e4; // 50% + uint256 internal constant _PRICE = 2e18; + + BatchAuctionHouse internal _auctionHouse; + FixedPriceBatch internal _module; + + // Input parameters (modified by modifiers) + uint48 internal _start; + uint96 internal _lotId = type(uint96).max; + IAuction.AuctionParams internal _auctionParams; + FixedPriceBatch.AuctionDataParams internal _fpbParams; + + uint8 internal _quoteTokenDecimals = 18; + uint8 internal _baseTokenDecimals = 18; + + function setUp() external { + vm.warp(1_000_000); + + _auctionHouse = new BatchAuctionHouse(address(this), _PROTOCOL, _permit2Address); + _module = new FixedPriceBatch(address(_auctionHouse)); + + _start = uint48(block.timestamp) + 1; + + _fpbParams = + IFixedPriceBatch.AuctionDataParams({price: _PRICE, minFillPercent: _MIN_FILL_PERCENT}); + + _auctionParams = IAuction.AuctionParams({ + start: _start, + duration: _DURATION, + capacityInQuote: false, + capacity: _LOT_CAPACITY, + implParams: abi.encode(_fpbParams) + }); + } + + // ========== MODIFIERS ========== // + + function _setQuoteTokenDecimals(uint8 decimals_) internal { + _quoteTokenDecimals = decimals_; + + _fpbParams.price = _scaleQuoteTokenAmount(_PRICE); + + _auctionParams.implParams = abi.encode(_fpbParams); + + if (_auctionParams.capacityInQuote) { + _auctionParams.capacity = _scaleQuoteTokenAmount(_LOT_CAPACITY); + } + } + + modifier givenQuoteTokenDecimals(uint8 decimals_) { + _setQuoteTokenDecimals(decimals_); + _; + } + + function _setBaseTokenDecimals(uint8 decimals_) internal { + _baseTokenDecimals = decimals_; + + if (!_auctionParams.capacityInQuote) { + _auctionParams.capacity = _scaleBaseTokenAmount(_LOT_CAPACITY); + } + } + + modifier givenBaseTokenDecimals(uint8 decimals_) { + _setBaseTokenDecimals(decimals_); + _; + } + + function _setCapacity(uint256 capacity_) internal { + _auctionParams.capacity = capacity_; + } + + modifier givenLotCapacity(uint256 capacity_) { + _setCapacity(capacity_); + _; + } + + modifier givenStartTimestamp(uint48 start_) { + _auctionParams.start = start_; + _; + } + + modifier givenDuration(uint48 duration_) { + _auctionParams.duration = duration_; + _; + } + + function _createAuctionLot() internal { + vm.prank(address(_auctionHouse)); + _module.auction(_lotId, _auctionParams, _quoteTokenDecimals, _baseTokenDecimals); + } + + modifier givenLotIsCreated() { + _createAuctionLot(); + _; + } + + function _setPrice(uint256 price_) internal { + _fpbParams.price = price_; + _auctionParams.implParams = abi.encode(_fpbParams); + } + + modifier givenPrice(uint256 price_) { + _setPrice(price_); + _; + } + + function _setMinFillPercent(uint24 minFillPercent_) internal { + _fpbParams.minFillPercent = minFillPercent_; + _auctionParams.implParams = abi.encode(_fpbParams); + } + + modifier givenMinFillPercent(uint24 minFillPercent_) { + _setMinFillPercent(minFillPercent_); + _; + } + + function _concludeLot() internal { + vm.warp(_start + _DURATION + 1); + } + + modifier givenLotHasConcluded() { + _concludeLot(); + _; + } + + function _startLot() internal { + vm.warp(_start + 1); + } + + modifier givenLotHasStarted() { + _startLot(); + _; + } + + function _cancelAuctionLot() internal { + vm.prank(address(_auctionHouse)); + _module.cancelAuction(_lotId); + } + + modifier givenLotIsCancelled() { + _cancelAuctionLot(); + _; + } + + function _createBid(uint256 amount_) internal { + vm.prank(address(_auctionHouse)); + _module.bid(_lotId, _BIDDER, _REFERRER, amount_, abi.encode("")); + } + + modifier givenBidIsCreated(uint256 amount_) { + _createBid(amount_); + _; + } + + modifier givenLotIsAborted() { + vm.prank(address(_auctionHouse)); + _module.abort(_lotId); + _; + } + + function _settleLot() internal { + vm.prank(address(_auctionHouse)); + _module.settle(_lotId, 100_000); + } + + modifier givenLotIsSettled() { + _settleLot(); + _; + } + + function _warpAfterSettlePeriod() internal { + vm.warp(_start + _DURATION + _module.dedicatedSettlePeriod()); + } + + modifier givenLotSettlePeriodHasPassed() { + _warpAfterSettlePeriod(); + _; + } + + modifier givenDuringLotSettlePeriod() { + vm.warp(_start + _DURATION + _module.dedicatedSettlePeriod() - 1); + _; + } + + function _refundBid(uint64 bidId_) internal returns (uint256 refundAmount) { + vm.prank(address(_auctionHouse)); + return _module.refundBid(_lotId, bidId_, 0, _BIDDER); + } + + modifier givenBidIsRefunded(uint64 bidId_) { + _refundBid(bidId_); + _; + } + + function _claimBid(uint64 bidId_) + internal + returns (IBatchAuction.BidClaim[] memory bidClaims, bytes memory auctionOutput) + { + uint64[] memory bidIds = new uint64[](1); + bidIds[0] = bidId_; + + vm.prank(address(_auctionHouse)); + return _module.claimBids(_lotId, bidIds); + } + + modifier givenBidIsClaimed(uint64 bidId_) { + uint64[] memory bidIds = new uint64[](1); + bidIds[0] = bidId_; + + vm.prank(address(_auctionHouse)); + _module.claimBids(_lotId, bidIds); + _; + } + + // ======== Internal Functions ======== // + + function _scaleQuoteTokenAmount(uint256 amount_) internal view returns (uint256) { + return Math.mulDivDown(amount_, 10 ** _quoteTokenDecimals, _BASE_SCALE); + } + + function _scaleBaseTokenAmount(uint256 amount_) internal view returns (uint256) { + return Math.mulDivDown(amount_, 10 ** _baseTokenDecimals, _BASE_SCALE); + } +} diff --git a/test/modules/auctions/FPB/auction.t.sol b/test/modules/auctions/FPB/auction.t.sol new file mode 100644 index 00000000..18c72a0a --- /dev/null +++ b/test/modules/auctions/FPB/auction.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; +import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbCreateAuctionTest is FpbTest { + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the start time is in the past + // [X] it reverts + // [X] when the duration is less than the minimum + // [X] it reverts + // [X] when the price is 0 + // [X] it reverts + // [X] when the minimum fill percentage is > 100% + // [X] it reverts + // [X] when the start time is 0 + // [X] it sets it to the current block timestamp + // [X] it sets the price and minFilled + + function test_notParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.auction(_lotId, _auctionParams, _quoteTokenDecimals, _baseTokenDecimals); + } + + function test_startTimeInPast_reverts() + public + givenStartTimestamp(uint48(block.timestamp - 1)) + { + // Expect revert + bytes memory err = abi.encodeWithSelector( + IAuction.Auction_InvalidStart.selector, _auctionParams.start, uint48(block.timestamp) + ); + vm.expectRevert(err); + + // Call the function + _createAuctionLot(); + } + + function test_durationLessThanMinimum_reverts() public givenDuration(uint48(8 hours)) { + // Expect revert + bytes memory err = abi.encodeWithSelector( + IAuction.Auction_InvalidDuration.selector, _auctionParams.duration, uint48(1 days) + ); + vm.expectRevert(err); + + // Call the function + _createAuctionLot(); + } + + function test_priceIsZero_reverts() public givenPrice(0) { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call the function + _createAuctionLot(); + } + + function test_minFillPercentageGreaterThan100_reverts() public givenMinFillPercent(1e5 + 1) { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call the function + _createAuctionLot(); + } + + function test_startTimeIsZero_setsToCurrentBlockTimestamp() public givenStartTimestamp(0) { + // Call the function + _createAuctionLot(); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.start, uint48(block.timestamp), "start"); + assertEq( + lotData.conclusion, uint48(block.timestamp + _auctionParams.duration), "conclusion" + ); + } + + function test_success(uint256 capacity_, uint256 price_, uint24 minFillPercent_) public { + uint256 capacity = bound(capacity_, 1, type(uint256).max); + _setCapacity(capacity); + uint256 price = bound(price_, 1, type(uint256).max); + _setPrice(price); + uint24 minFillPercent = uint24(bound(minFillPercent_, 0, 1e5)); + _setMinFillPercent(minFillPercent); + + // Call the function + _createAuctionLot(); + + // Round up to be conservative + uint256 minFilled = Math.fullMulDivUp(capacity, minFillPercent, 1e5); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, capacity, "capacity"); + assertEq(lotData.capacityInQuote, false, "capacityInQuote"); + assertEq(lotData.quoteTokenDecimals, _quoteTokenDecimals, "quoteTokenDecimals"); + assertEq(lotData.baseTokenDecimals, _baseTokenDecimals, "baseTokenDecimals"); + assertEq(lotData.start, _auctionParams.start, "start"); + assertEq(lotData.conclusion, _auctionParams.start + _auctionParams.duration, "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(auctionData.price, price, "price"); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 1, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); + assertEq(auctionData.totalBidAmount, 0, "totalBidAmount"); + assertEq(auctionData.minFilled, minFilled, "minFilled"); + } +} diff --git a/test/modules/auctions/FPB/bid.t.sol b/test/modules/auctions/FPB/bid.t.sol new file mode 100644 index 00000000..fab1de20 --- /dev/null +++ b/test/modules/auctions/FPB/bid.t.sol @@ -0,0 +1,563 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbBidTest is FpbTest { + uint256 internal constant _BID_AMOUNT = 2e18; + + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the lot has not started + // [X] it reverts + // [X] when the lot has concluded + // [X] it reverts + // [X] when the lot has been cancelled + // [X] it reverts + // [X] when the lot has been aborted + // [X] it reverts + // [X] when the lot has been settled + // [X] it reverts + // [X] when the lot is in the settlement period + // [X] it reverts + // [X] when the bid amount is 0 + // [X] it reverts + // [X] when the bid amount is greater than uint96 max + // [X] it reverts + // [X] when the auction price is very high or very low + // [X] it records the bid accurately + // [X] when the bid amount reaches capacity + // [X] it records the bid and concludes the auction + // [X] when the bid amount is greater than the remaining capacity + // [X] it records the bid, concludes the auction and calculates partial fill + // [X] when the quote token decimals are greater + // [X] it records the bid accurately + // [X] when the quote token decimals are smaller + // [X] it records the bid accurately + // [X] it records the bid + + function test_notParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.bid(_lotId, _BIDDER, _REFERRER, _BID_AMOUNT, abi.encode("")); + } + + function test_invalidLotId_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidLotId.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotNotStarted_reverts() public givenLotIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotConcluded_reverts() public givenLotIsCreated givenLotHasConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotCancelled_reverts() public givenLotIsCreated givenLotIsCancelled { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotAborted_reverts() + public + givenLotIsCreated + givenLotSettlePeriodHasPassed + givenLotIsAborted + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotSettled_reverts() + public + givenLotIsCreated + givenLotHasConcluded + givenLotSettlePeriodHasPassed + givenLotIsSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_lotInSettlementPeriod_reverts() + public + givenLotIsCreated + givenLotHasConcluded + givenDuringLotSettlePeriod + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _createBid(_BID_AMOUNT); + } + + function test_bidAmountIsZero_reverts() public givenLotIsCreated givenLotHasStarted { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call the function + _createBid(0); + } + + function test_bidAmountIsGreaterThanMax_reverts() public givenLotIsCreated givenLotHasStarted { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call the function + _createBid(uint256(type(uint96).max) + 1); + } + + function test_bidsReachCapacity() public givenLotIsCreated givenLotHasStarted { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(10e18); + + // Create a second bid to fill the remaining capacity exactly + _createBid(10e18); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, 20e18, "totalBidAmount"); + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, 10e18, "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, 10e18, "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, false, "hasPartialFill"); + assertEq(partialFill.bidId, 0, "partialFill: bidId"); + assertEq(partialFill.refund, 0, "partialFill: refund"); + assertEq(partialFill.payout, 0, "partialFill: payout"); + } + + function test_bidsReachCapacity_quoteTokenDecimalsLarger() + public + givenQuoteTokenDecimals(21) + givenBaseTokenDecimals(13) + givenLotIsCreated + givenLotHasStarted + { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Create a second bid to fill the remaining capacity exactly + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _scaleBaseTokenAmount(_LOT_CAPACITY), "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, _scaleQuoteTokenAmount(20e18), "totalBidAmount"); + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, false, "hasPartialFill"); + assertEq(partialFill.bidId, 0, "partialFill: bidId"); + assertEq(partialFill.refund, 0, "partialFill: refund"); + assertEq(partialFill.payout, 0, "partialFill: payout"); + } + + function test_bidsReachCapacity_quoteTokenDecimalsSmaller() + public + givenQuoteTokenDecimals(13) + givenBaseTokenDecimals(21) + givenLotIsCreated + givenLotHasStarted + { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Create a second bid to fill the remaining capacity exactly + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _scaleBaseTokenAmount(_LOT_CAPACITY), "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, _scaleQuoteTokenAmount(20e18), "totalBidAmount"); + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, false, "hasPartialFill"); + assertEq(partialFill.bidId, 0, "partialFill: bidId"); + assertEq(partialFill.refund, 0, "partialFill: refund"); + assertEq(partialFill.payout, 0, "partialFill: payout"); + } + + function test_bidsOverCapacity() public givenLotIsCreated givenLotHasStarted { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(10e18); + + // Create a second bid to fill the over the remaining capacity (12/2 = 6) + _createBid(12e18); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, 20e18, "totalBidAmount"); // Excludes refund + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, 10e18, "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, 12e18, "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, true, "hasPartialFill"); + assertEq(partialFill.bidId, 2, "partialFill: bidId"); + assertEq(partialFill.refund, 2e18, "partialFill: refund"); + assertEq(partialFill.payout, 5e18, "partialFill: payout"); + } + + function test_bidsOverCapacity_quoteTokenDecimalsLarger() + public + givenQuoteTokenDecimals(21) + givenBaseTokenDecimals(13) + givenLotIsCreated + givenLotHasStarted + { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Create a second bid to fill the over the remaining capacity (12/2 = 6) + _createBid(_scaleQuoteTokenAmount(12e18)); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _scaleBaseTokenAmount(_LOT_CAPACITY), "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, _scaleQuoteTokenAmount(20e18), "totalBidAmount"); // Excludes refund + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(12e18), "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, true, "hasPartialFill"); + assertEq(partialFill.bidId, 2, "partialFill: bidId"); + assertEq(partialFill.refund, _scaleQuoteTokenAmount(2e18), "partialFill: refund"); + assertEq(partialFill.payout, _scaleBaseTokenAmount(5e18), "partialFill: payout"); + } + + function test_bidsOverCapacity_quoteTokenDecimalsSmaller() + public + givenQuoteTokenDecimals(13) + givenBaseTokenDecimals(21) + givenLotIsCreated + givenLotHasStarted + { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(_scaleQuoteTokenAmount(10e18)); + + // Create a second bid to fill the over the remaining capacity (12/2 = 6) + _createBid(_scaleQuoteTokenAmount(12e18)); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _scaleBaseTokenAmount(_LOT_CAPACITY), "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 3, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, _scaleQuoteTokenAmount(20e18), "totalBidAmount"); // Excludes refund + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(10e18), "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Assert bid two + bidData = _module.getBid(_lotId, 2); + assertEq(bidData.bidder, _BIDDER, "bid two: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid two: referrer"); + assertEq(bidData.amount, _scaleQuoteTokenAmount(12e18), "bid two: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid two: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, true, "hasPartialFill"); + assertEq(partialFill.bidId, 2, "partialFill: bidId"); + assertEq(partialFill.refund, _scaleQuoteTokenAmount(2e18), "partialFill: refund"); + assertEq(partialFill.payout, _scaleBaseTokenAmount(5e18), "partialFill: payout"); + } + + function test_singleBidOverCapacity() public givenLotIsCreated givenLotHasStarted { + // Create a bid to that exceeds capacity (22/2 = 11) + _createBid(22e18); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 2, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, 20e18, "totalBidAmount"); // Excludes refund + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, 22e18, "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, true, "hasPartialFill"); + assertEq(partialFill.bidId, 1, "partialFill: bidId"); + assertEq(partialFill.refund, 2e18, "partialFill: refund"); + assertEq(partialFill.payout, 10e18, "partialFill: payout"); + } + + function test_bidsUnderCapacity() public givenLotIsCreated givenLotHasStarted { + // Create a bid to fill half capacity (10/2 = 5) + _createBid(10e18); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.conclusion, _start + _DURATION, "conclusion"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Created), "status"); + assertEq(auctionData.nextBidId, 2, "nextBidId"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); // Not settled yet + assertEq(auctionData.totalBidAmount, 10e18, "totalBidAmount"); + + // Assert bid one + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(bidData.bidder, _BIDDER, "bid one: bidder"); + assertEq(bidData.referrer, _REFERRER, "bid one: referrer"); + assertEq(bidData.amount, 10e18, "bid one: amount"); + assertEq( + uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "bid one: status" + ); + } + + function test_partialFill_auctionPriceFuzz(uint256 price_) public { + // Given that the capacity is set, there is a maximum value to the price before the bidAmount hits uint96 + // 11e18 * price / 1e18 <= type(uint96).max + // price <= type(uint96).max / 10e18 + uint256 price = bound(price_, 1, type(uint96).max / 10e18); + _setPrice(price); + + // Create the auction + _createAuctionLot(); + + // Warp to start + _startLot(); + + // Calculate a bid amount that would result in a partial fill + uint256 bidAmount = 11e18 * price / (10 ** _baseTokenDecimals); + uint256 maxBidAmount = 10e18 * price / (10 ** _baseTokenDecimals); + + // Create a bid + _createBid(bidAmount); + + // Settle the auction + _warpAfterSettlePeriod(); + _settleLot(); + + // Assert partial fill + (bool hasPartialFill, IFixedPriceBatch.PartialFill memory partialFill) = + _module.getPartialFill(_lotId); + assertEq(hasPartialFill, true, "hasPartialFill"); + assertEq(partialFill.bidId, 1, "partialFill: bidId"); + assertEq(partialFill.refund, bidAmount - maxBidAmount, "partialFill: refund"); + assertEq(partialFill.payout, 10e18, "partialFill: payout"); + } +} diff --git a/test/modules/auctions/FPB/cancelAuction.t.sol b/test/modules/auctions/FPB/cancelAuction.t.sol new file mode 100644 index 00000000..bffaba69 --- /dev/null +++ b/test/modules/auctions/FPB/cancelAuction.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbCancelAuctionTest is FpbTest { + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the auction has concluded + // [X] it reverts + // [X] when the auction has been cancelled + // [X] it reverts + // [X] when the auction has been aborted + // [X] it reverts + // [X] when the auction has been settled + // [X] it reverts + // [X] when the auction has started + // [X] it reverts + // [X] it updates the conclusion, capacity and status + + function test_notParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.cancelAuction(_lotId); + } + + function test_invalidLotId_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidLotId.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_auctionConcluded_reverts(uint48 conclusionElapsed_) public givenLotIsCreated { + uint48 conclusionElapsed = uint48(bound(conclusionElapsed_, 0, 1 days)); + + // Warp to the conclusion + vm.warp(_start + _DURATION + conclusionElapsed); + + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_auctionCancelled_reverts() public givenLotIsCreated givenLotIsCancelled { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_auctionAborted_reverts() + public + givenLotIsCreated + givenLotSettlePeriodHasPassed + givenLotIsAborted + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_auctionSettled_reverts() + public + givenLotIsCreated + givenLotHasConcluded + givenLotIsSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_auctionStarted_reverts() public givenLotIsCreated givenLotHasStarted { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _cancelAuctionLot(); + } + + function test_success() public givenLotIsCreated { + // Call the function + _cancelAuctionLot(); + + // Assert state + IAuction.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.conclusion, uint48(block.timestamp), "conclusion"); + assertEq(lotData.capacity, 0, "capacity"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Settled), "status"); + } +} diff --git a/test/modules/auctions/FPB/claimBids.t.sol b/test/modules/auctions/FPB/claimBids.t.sol new file mode 100644 index 00000000..e5000190 --- /dev/null +++ b/test/modules/auctions/FPB/claimBids.t.sol @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbClaimBidsTest is FpbTest { + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] when any bid id is invalid + // [X] it reverts + // [X] given the lot has not concluded + // [X] it reverts + // [X] given any bid has been claimed + // [X] it reverts + // [X] given it is during the settlement period + // [X] it reverts + // [X] given the lot is not settled + // [X] it reverts + // [X] given the auction was aborted + // [X] it returns the refund amount and updates the bid status + // [X] given the settlement cleared + // [X] given the bid was a partial fill + // [X] it returns the payout and refund amounts and updates the bid status + // [X] it returns the refund amount and updates the bid status + // [X] it returns the refund amount and updates the bid status + // [X] it returns the bid claims for multiple bids + + function test_notParent_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + givenLotIsSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.claimBids(_lotId, new uint64[](1)); + } + + function test_invalidLotId_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidLotId.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_invalidBidId_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenLotHasConcluded + givenLotIsSettled + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IBatchAuction.Auction_InvalidBidId.selector, _lotId, 1); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_lotNotConcluded_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_bidClaimed_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + givenLotIsSettled + { + // Claim the bid + _claimBid(1); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Bid_WrongState.selector, _lotId, 1); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_duringSettlementPeriod_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_lotNotSettled_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + givenLotSettlePeriodHasPassed + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _claimBid(1); + } + + function test_lotAborted() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + givenLotSettlePeriodHasPassed + givenLotIsAborted + { + // Call the function + (IBatchAuction.BidClaim[] memory bidClaims,) = _claimBid(1); + + // Check values + assertEq(bidClaims.length, 1, "bidClaims length"); + + IBatchAuction.BidClaim memory bidClaim = bidClaims[0]; + assertEq(bidClaim.paid, 1e18, "paid"); + assertEq(bidClaim.refund, 1e18, "refund"); + assertEq(bidClaim.payout, 0, "payout"); + + // Assert bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + } + + function test_lotSettlementClears() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(6e18) + givenBidIsCreated(6e18) + givenLotHasConcluded + givenLotIsSettled + { + // Claim the first bid + (IBatchAuction.BidClaim[] memory bidClaims,) = _claimBid(1); + + // Check values + assertEq(bidClaims.length, 1, "bidClaims length"); + + IBatchAuction.BidClaim memory bidClaim = bidClaims[0]; + assertEq(bidClaim.paid, 6e18, "paid"); + assertEq(bidClaim.refund, 0, "refund"); + assertEq(bidClaim.payout, 3e18, "payout"); // 6/2 + + // Check the bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + + IFixedPriceBatch.Bid memory bidDataTwo = _module.getBid(_lotId, 2); + assertEq(uint8(bidDataTwo.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "status"); + } + + function test_lotSettlementDoesNotClear() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(2e18) + givenBidIsCreated(2e18) + givenLotHasConcluded + givenLotIsSettled + { + // Claim the first bid + (IBatchAuction.BidClaim[] memory bidClaims,) = _claimBid(1); + + // Check values + assertEq(bidClaims.length, 1, "bidClaims length"); + + IBatchAuction.BidClaim memory bidClaim = bidClaims[0]; + assertEq(bidClaim.paid, 2e18, "paid"); + assertEq(bidClaim.refund, 2e18, "refund"); + assertEq(bidClaim.payout, 0, "payout"); + + // Check the bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + + IFixedPriceBatch.Bid memory bidDataTwo = _module.getBid(_lotId, 2); + assertEq(uint8(bidDataTwo.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "status"); + } + + function test_lotSettlementClears_partialFill() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(10e18) + givenBidIsCreated(12e18) + givenLotHasConcluded + givenLotIsSettled + { + // Claim the second bid (partial fill) + (IBatchAuction.BidClaim[] memory bidClaims,) = _claimBid(2); + + // Check values + assertEq(bidClaims.length, 1, "bidClaims length"); + + IBatchAuction.BidClaim memory bidClaim = bidClaims[0]; + assertEq(bidClaim.paid, 12e18, "paid"); + assertEq(bidClaim.refund, 2e18, "refund"); + assertEq(bidClaim.payout, 5e18, "payout"); // 10/2 + + // Check the bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Submitted), "status"); + + IFixedPriceBatch.Bid memory bidDataTwo = _module.getBid(_lotId, 2); + assertEq(uint8(bidDataTwo.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + } + + function test_multipleBids() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(6e18) + givenBidIsCreated(8e18) + givenLotHasConcluded + givenLotIsSettled + { + // Claim both bids + uint64[] memory bidIds = new uint64[](2); + bidIds[0] = 1; + bidIds[1] = 2; + + vm.prank(address(_auctionHouse)); + (IBatchAuction.BidClaim[] memory bidClaims,) = _module.claimBids(_lotId, bidIds); + + // Check values + assertEq(bidClaims.length, 2, "bidClaims length"); + + IBatchAuction.BidClaim memory bidClaim = bidClaims[0]; + assertEq(bidClaim.paid, 6e18, "paid"); + assertEq(bidClaim.refund, 0, "refund"); + assertEq(bidClaim.payout, 3e18, "payout"); // 6/2 + + IBatchAuction.BidClaim memory bidClaimTwo = bidClaims[1]; + assertEq(bidClaimTwo.paid, 8e18, "paid"); + assertEq(bidClaimTwo.refund, 0, "refund"); + assertEq(bidClaimTwo.payout, 4e18, "payout"); // 8/2 + + // Check the bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + + IFixedPriceBatch.Bid memory bidDataTwo = _module.getBid(_lotId, 2); + assertEq(uint8(bidDataTwo.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + } +} diff --git a/test/modules/auctions/FPB/refundBid.t.sol b/test/modules/auctions/FPB/refundBid.t.sol new file mode 100644 index 00000000..e5cadb98 --- /dev/null +++ b/test/modules/auctions/FPB/refundBid.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IBatchAuction} from "src/interfaces/modules/IBatchAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbRefundBidTest is FpbTest { + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the bid id is invalid + // [X] it reverts + // [X] when the caller is not the bid owner + // [X] it reverts + // [X] given the bid has been refunded + // [X] it reverts + // [X] given the lot has concluded + // [X] it reverts + // [X] given the lot has been aborted + // [X] it reverts + // [X] given the lot has been settled + // [X] it reverts + // [X] given the lot is in the settlement period + // [X] it reverts + // [X] it returns the refund amount and updates the bid status + + function test_notParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.refundBid(_lotId, 1, 0, _BIDDER); + } + + function test_invalidLotId_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidLotId.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_invalidBidId_reverts() public givenLotIsCreated givenLotHasStarted { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IBatchAuction.Auction_InvalidBidId.selector, _lotId, 1); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_notBidOwner_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.NotPermitted.selector, address(this)); + vm.expectRevert(err); + + // Call the function + vm.prank(address(_auctionHouse)); + _module.refundBid(_lotId, 1, 0, address(this)); + } + + function test_bidRefunded_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + { + // Refund the bid + _refundBid(1); + + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Bid_WrongState.selector, _lotId, 1); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_lotConcluded_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_lotAborted_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotSettlePeriodHasPassed + givenLotIsAborted + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_lotSettled_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenLotHasConcluded + givenLotIsSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_lotSettlePeriod_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenDuringLotSettlePeriod + { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _refundBid(1); + } + + function test_success() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(1e18) + givenBidIsCreated(2e18) + { + // Call the function + uint256 refundAmount = _refundBid(1); + + // Check the refund amount + assertEq(refundAmount, 1e18, "refundAmount"); + + // Check bid state + IFixedPriceBatch.Bid memory bidData = _module.getBid(_lotId, 1); + assertEq(uint8(bidData.status), uint8(IFixedPriceBatch.BidStatus.Claimed), "status"); + + // Check auction data + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(auctionData.totalBidAmount, 2e18, "totalBidAmount"); + } +} diff --git a/test/modules/auctions/FPB/settle.t.sol b/test/modules/auctions/FPB/settle.t.sol new file mode 100644 index 00000000..77c36bad --- /dev/null +++ b/test/modules/auctions/FPB/settle.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Module} from "src/modules/Modules.sol"; +import {IAuction} from "src/interfaces/modules/IAuction.sol"; +import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; + +import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; + +contract FpbSettleTest is FpbTest { + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the lot has not started + // [X] it reverts + // [X] when the lot has not concluded + // [X] it reverts + // [X] when the lot has been cancelled + // [X] it reverts + // [X] when the lot has been aborted + // [X] it reverts + // [X] when the lot has been settled + // [X] it reverts + // [X] when the lot is in the settlement period + // [X] it settles + // [X] when the filled capacity is below the minimum + // [X] it marks the settlement as not cleared and updates the status + // [X] it marks the settlement as cleared, updates the status and returns the total in and out + + function test_notParent_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenLotHasConcluded + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call the function + _module.settle(_lotId, 100_000); + } + + function test_invalidLotId_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_InvalidLotId.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_lotHasNotStarted_reverts() public givenLotIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_lotHasNotConcluded_reverts() public givenLotIsCreated givenLotHasStarted { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_lotHasBeenCancelled_reverts() public givenLotIsCreated givenLotIsCancelled { + // Expect revert + bytes memory err = abi.encodeWithSelector(IAuction.Auction_LotNotActive.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_lotHasBeenAborted_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenLotSettlePeriodHasPassed + givenLotIsAborted + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_lotHasBeenSettled_reverts() + public + givenLotIsCreated + givenLotHasStarted + givenLotHasConcluded + givenLotIsSettled + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(IFixedPriceBatch.Auction_WrongState.selector, _lotId); + vm.expectRevert(err); + + // Call the function + _settleLot(); + } + + function test_duringSettlementPeriod() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(10e18) + givenLotHasConcluded + { + // Call the function + _settleLot(); + + // Assert state + IFixedPriceBatch.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.conclusion, _start + _DURATION, "conclusion"); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.purchased, 10e18, "purchased"); + assertEq(lotData.sold, 5e18, "sold"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Settled), "status"); + assertEq(auctionData.settlementCleared, true, "settlementCleared"); + assertEq(auctionData.totalBidAmount, 10e18, "totalBidAmount"); + } + + function test_settlementClears() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(10e18) + givenLotHasConcluded + givenLotSettlePeriodHasPassed + { + // Call the function + _settleLot(); + + // Assert state + IFixedPriceBatch.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.conclusion, _start + _DURATION, "conclusion"); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.purchased, 10e18, "purchased"); + assertEq(lotData.sold, 5e18, "sold"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Settled), "status"); + assertEq(auctionData.settlementCleared, true, "settlementCleared"); + assertEq(auctionData.totalBidAmount, 10e18, "totalBidAmount"); + } + + function test_belowMinFillPercent() + public + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated(6e18) + givenLotHasConcluded + givenLotSettlePeriodHasPassed + { + // Call the function + _settleLot(); + + // Assert state + IFixedPriceBatch.Lot memory lotData = _module.getLot(_lotId); + assertEq(lotData.conclusion, _start + _DURATION, "conclusion"); + assertEq(lotData.capacity, _LOT_CAPACITY, "capacity"); + assertEq(lotData.purchased, 0, "purchased"); + assertEq(lotData.sold, 0, "sold"); + + IFixedPriceBatch.AuctionData memory auctionData = _module.getAuctionData(_lotId); + assertEq(uint8(auctionData.status), uint8(IFixedPriceBatch.LotStatus.Settled), "status"); + assertEq(auctionData.settlementCleared, false, "settlementCleared"); + assertEq(auctionData.totalBidAmount, 6e18, "totalBidAmount"); + } +} From d34627ed4e40b0f8ca896ad58565390ea19a5276 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 27 May 2024 10:57:22 +0400 Subject: [PATCH 06/13] Add JSON schema (and validation in VSCode) for deployment sequence files --- .vscode/settings.json | 10 ++++++- script/deploy/sequence_schema.json | 43 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 script/deploy/sequence_schema.json diff --git a/.vscode/settings.json b/.vscode/settings.json index a4244592..08ae7b4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,13 @@ ], "[json]": { "editor.tabSize": 2 - } + }, + "json.schemas": [ + { + "fileMatch": [ + "/script/deploy/sequences/*.json" + ], + "url": "/script/deploy/sequence_schema.json" + } + ] } \ No newline at end of file diff --git a/script/deploy/sequence_schema.json b/script/deploy/sequence_schema.json new file mode 100644 index 00000000..490f9e95 --- /dev/null +++ b/script/deploy/sequence_schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://axis.finance/deploy.schema.json", + "title": "Axis Finance Deployment Configuration", + "description": "Configuration for deploying Axis Finance core and modules", + "type": "object", + "properties": { + "sequence": { + "type": "array", + "items": { + "type": "object", + "description": "Describes an individual deployment", + "properties": { + "name": { + "type": "string", + "description": "The name of the module to deploy", + "exclusiveMinimum": 0 + }, + "installAtomicAuctionHouse": { + "type": "boolean", + "description": "Whether to install the module into the Atomic Auction House", + "default": false + }, + "installBatchAuctionHouse": { + "type": "boolean", + "description": "Whether to install the module into the Batch Auction House", + "default": false + }, + "args": { + "type": "object", + "description": "Arguments to pass to the module's deploy function", + "uniqueItems": true, + "additionalProperties": { + "type": ["integer", "string"] + } + } + }, + "required": ["name"] + } + }, + "required": ["sequence"] + } +} \ No newline at end of file From b064b6a8f53c703e1204baa26829add116f0cc38 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 27 May 2024 10:59:37 +0400 Subject: [PATCH 07/13] Add FixedPriceBatch to standard deployment script --- script/deploy/Deploy.s.sol | 34 ++++++++++++++++++++++++++++- script/deploy/sequences/origin.json | 4 ++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index 46b9c461..f21856a8 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -18,6 +18,7 @@ import {Callbacks} from "src/lib/Callbacks.sol"; // Auction modules import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; +import {FixedPriceBatch} from "src/modules/auctions/batch/FPB.sol"; // Derivative modules import {LinearVesting} from "src/modules/derivatives/LinearVesting.sol"; @@ -362,7 +363,7 @@ contract Deploy is Script, WithEnvironment, WithSalts { return (address(batchCatalogue), _PREFIX_AXIS); } - // ========== MODULE DEPLOYMENTS ========== // + // ========== AUCTION MODULE DEPLOYMENTS ========== // function deployEncryptedMarginalPrice(bytes memory) public @@ -428,6 +429,37 @@ contract Deploy is Script, WithEnvironment, WithSalts { return (address(amFps), _PREFIX_AXIS); } + function deployFixedPriceBatch(bytes memory) public virtual returns (address, string memory) { + // No args used + console2.log(""); + console2.log("Deploying FixedPriceBatch"); + + address batchAuctionHouse = _getAddressNotZero("axis.BatchAuctionHouse"); + + // Get the salt + bytes32 salt_ = _getSalt( + "FixedPriceBatch", type(FixedPriceBatch).creationCode, abi.encode(batchAuctionHouse) + ); + + // Deploy the module + FixedPriceBatch amFpb; + if (salt_ == bytes32(0)) { + vm.broadcast(); + amFpb = new FixedPriceBatch(batchAuctionHouse); + } else { + console2.log(" salt:", vm.toString(salt_)); + + vm.broadcast(); + amFpb = new FixedPriceBatch{salt: salt_}(batchAuctionHouse); + } + console2.log(""); + console2.log(" FixedPriceBatch deployed at:", address(amFpb)); + + return (address(amFpb), _PREFIX_AXIS); + } + + // ========== DERIVATIVE MODULE DEPLOYMENTS ========== // + function deployAtomicLinearVesting(bytes memory) public virtual diff --git a/script/deploy/sequences/origin.json b/script/deploy/sequences/origin.json index 53eece35..4137ff21 100644 --- a/script/deploy/sequences/origin.json +++ b/script/deploy/sequences/origin.json @@ -20,6 +20,10 @@ "name": "FixedPriceSale", "installAtomicAuctionHouse": true }, + { + "name": "FixedPriceBatch", + "installBatchAuctionHouse": true + }, { "name": "AtomicLinearVesting", "installAtomicAuctionHouse": true From 0868ba0c2113a183fb5fa7eeb0e4471bfc610943 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 27 May 2024 11:06:39 +0400 Subject: [PATCH 08/13] Add validation of addresses to JSON schema --- script/deploy/sequence_schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/deploy/sequence_schema.json b/script/deploy/sequence_schema.json index 490f9e95..d2f07b5b 100644 --- a/script/deploy/sequence_schema.json +++ b/script/deploy/sequence_schema.json @@ -31,7 +31,8 @@ "description": "Arguments to pass to the module's deploy function", "uniqueItems": true, "additionalProperties": { - "type": ["integer", "string"] + "type": ["integer", "string"], + "pattern": "^0x[0-9a-fA-F]{40}$" } } }, From f154ce127a964bdda4451bdb4df79a19a20d4ab6 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 28 May 2024 10:49:26 -0500 Subject: [PATCH 09/13] script: update deploy scripts to include FPB --- script/deploy/Deploy.s.sol | 30 +++++++++++++++++++ script/deploy/DeployBlast.s.sol | 29 ++++++++++++++++++ script/deploy/sequences/fixed-batch.json | 8 +++++ script/deploy/sequences/origin.json | 14 ++------- src/blast/modules/auctions/batch/BlastFPB.sol | 2 +- 5 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 script/deploy/sequences/fixed-batch.json diff --git a/script/deploy/Deploy.s.sol b/script/deploy/Deploy.s.sol index 46b9c461..edf20f7a 100644 --- a/script/deploy/Deploy.s.sol +++ b/script/deploy/Deploy.s.sol @@ -18,6 +18,7 @@ import {Callbacks} from "src/lib/Callbacks.sol"; // Auction modules import {EncryptedMarginalPrice} from "src/modules/auctions/batch/EMP.sol"; import {FixedPriceSale} from "src/modules/auctions/atomic/FPS.sol"; +import {FixedPriceBatch} from "src/modules/auctions/batch/FPB.sol"; // Derivative modules import {LinearVesting} from "src/modules/derivatives/LinearVesting.sol"; @@ -428,6 +429,35 @@ contract Deploy is Script, WithEnvironment, WithSalts { return (address(amFps), _PREFIX_AXIS); } + function deployFixedPriceBatch(bytes memory) public virtual returns (address, string memory) { + // No args used + console2.log(""); + console2.log("Deploying FixedPriceBatch"); + + address batchAuctionHouse = _getAddressNotZero("axis.BatchAuctionHouse"); + + // Get the salt + bytes32 salt_ = _getSalt( + "FixedPriceBatch", type(FixedPriceBatch).creationCode, abi.encode(batchAuctionHouse) + ); + + // Deploy the module + FixedPriceBatch amFpb; + if (salt_ == bytes32(0)) { + vm.broadcast(); + amFpb = new FixedPriceBatch(batchAuctionHouse); + } else { + console2.log(" salt:", vm.toString(salt_)); + + vm.broadcast(); + amFpb = new FixedPriceBatch{salt: salt_}(batchAuctionHouse); + } + console2.log(""); + console2.log(" FixedPriceBatch deployed at:", address(amFpb)); + + return (address(amFpb), _PREFIX_AXIS); + } + function deployAtomicLinearVesting(bytes memory) public virtual diff --git a/script/deploy/DeployBlast.s.sol b/script/deploy/DeployBlast.s.sol index 1b318253..74fb6b69 100644 --- a/script/deploy/DeployBlast.s.sol +++ b/script/deploy/DeployBlast.s.sol @@ -8,6 +8,7 @@ import {BlastAtomicAuctionHouse} from "src/blast/BlastAtomicAuctionHouse.sol"; import {BlastBatchAuctionHouse} from "src/blast/BlastBatchAuctionHouse.sol"; import {BlastEMP} from "src/blast/modules/auctions/batch/BlastEMP.sol"; import {BlastFPS} from "src/blast/modules/auctions/atomic/BlastFPS.sol"; +import {BlastFPB} from "src/blast/modules/auctions/batch/BlastFPB.sol"; import {BlastLinearVesting} from "src/blast/modules/derivatives/BlastLinearVesting.sol"; import {Deploy} from "script/deploy/Deploy.s.sol"; @@ -153,6 +154,34 @@ contract DeployBlast is Deploy { return (address(amFps), _PREFIX_AXIS); } + function deployFixedPriceBatch(bytes memory) public override returns (address, string memory) { + // No args used + console2.log(""); + console2.log("Deploying BlastFPB (Fixed Price Batch)"); + + address batchAuctionHouse = _getAddressNotZero("axis.BatchAuctionHouse"); + address blast = _getAddressNotZero("blast.blast"); + + // Get the salt + bytes32 salt_ = + _getSalt("BlastFPB", type(BlastFPB).creationCode, abi.encode(batchAuctionHouse, blast)); + + // Deploy the module + BlastFPB amFpb; + if (salt_ == bytes32(0)) { + vm.broadcast(); + amFpb = new BlastFPB(batchAuctionHouse, blast); + } else { + console2.log(" salt:", vm.toString(salt_)); + + vm.broadcast(); + amFpb = new BlastFPB{salt: salt_}(batchAuctionHouse, blast); + } + console2.log(" BlastFPB deployed at:", address(amFpb)); + + return (address(amFpb), _PREFIX_AXIS); + } + function deployAtomicLinearVesting(bytes memory) public override diff --git a/script/deploy/sequences/fixed-batch.json b/script/deploy/sequences/fixed-batch.json new file mode 100644 index 00000000..635187fc --- /dev/null +++ b/script/deploy/sequences/fixed-batch.json @@ -0,0 +1,8 @@ +{ + "sequence": [ + { + "name": "FixedPriceBatch", + "installBatchAuctionHouse": true + } + ] +} \ No newline at end of file diff --git a/script/deploy/sequences/origin.json b/script/deploy/sequences/origin.json index 53eece35..6ed808b7 100644 --- a/script/deploy/sequences/origin.json +++ b/script/deploy/sequences/origin.json @@ -1,14 +1,8 @@ { "sequence": [ - { - "name": "AtomicAuctionHouse" - }, { "name": "BatchAuctionHouse" }, - { - "name": "AtomicCatalogue" - }, { "name": "BatchCatalogue" }, @@ -17,12 +11,8 @@ "installBatchAuctionHouse": true }, { - "name": "FixedPriceSale", - "installAtomicAuctionHouse": true - }, - { - "name": "AtomicLinearVesting", - "installAtomicAuctionHouse": true + "name": "FixedPriceBatch", + "installBatchAuctionHouse": true }, { "name": "BatchLinearVesting", diff --git a/src/blast/modules/auctions/batch/BlastFPB.sol b/src/blast/modules/auctions/batch/BlastFPB.sol index 10776890..2086b6c1 100644 --- a/src/blast/modules/auctions/batch/BlastFPB.sol +++ b/src/blast/modules/auctions/batch/BlastFPB.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {FixedPriceBatch} from "src/modules/auctions/batch/FPB.sol"; import {BlastGas} from "src/blast/modules/BlastGas.sol"; -contract BlastEMP is FixedPriceBatch, BlastGas { +contract BlastFPB is FixedPriceBatch, BlastGas { // ========== CONSTRUCTOR ========== // constructor( From 83ba84b2e2cc05c445070eba48982a62e6610e5f Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 28 May 2024 10:49:47 -0500 Subject: [PATCH 10/13] deploy: FPB to testnets --- deployments/.arbitrum-sepolia-1716910513.json | 3 +++ deployments/.blast-sepolia-1716909826.json | 3 +++ deployments/.mode-sepolia-1716911194.json | 3 +++ script/env.json | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 deployments/.arbitrum-sepolia-1716910513.json create mode 100644 deployments/.blast-sepolia-1716909826.json create mode 100644 deployments/.mode-sepolia-1716911194.json diff --git a/deployments/.arbitrum-sepolia-1716910513.json b/deployments/.arbitrum-sepolia-1716910513.json new file mode 100644 index 00000000..c543754b --- /dev/null +++ b/deployments/.arbitrum-sepolia-1716910513.json @@ -0,0 +1,3 @@ +{ +"axis.FixedPriceBatch": "0xB24D0b6ae015DC6fd279E330db101bB890d8060c" +} diff --git a/deployments/.blast-sepolia-1716909826.json b/deployments/.blast-sepolia-1716909826.json new file mode 100644 index 00000000..fd620feb --- /dev/null +++ b/deployments/.blast-sepolia-1716909826.json @@ -0,0 +1,3 @@ +{ +"axis.FixedPriceBatch": "0xEDa0cC0bbb45D8cd6354755856053d6Ea646E201" +} diff --git a/deployments/.mode-sepolia-1716911194.json b/deployments/.mode-sepolia-1716911194.json new file mode 100644 index 00000000..d75cf37e --- /dev/null +++ b/deployments/.mode-sepolia-1716911194.json @@ -0,0 +1,3 @@ +{ +"axis.FixedPriceBatch": "0xC818f1f000f9C24D014BCe2c5334e14B1360d9CD" +} diff --git a/script/env.json b/script/env.json index 231bea35..ef6c9d44 100644 --- a/script/env.json +++ b/script/env.json @@ -42,6 +42,7 @@ "BatchUniswapV2DirectToLiquidity": "0x0000000000000000000000000000000000000000", "BatchUniswapV3DirectToLiquidity": "0x0000000000000000000000000000000000000000", "EncryptedMarginalPrice": "0x87F2a19FBbf9e557a68bD35D85FAd20dEec40494", + "FixedPriceBatch": "0xB24D0b6ae015DC6fd279E330db101bB890d8060c", "FixedPriceSale": "0x0A0BA689D2D72D3f376293c534AF299B3C6Dac85", "OWNER": "0xB47C8e4bEb28af80eDe5E5bF474927b110Ef2c0e", "PERMIT2": "0x000000000022D473030F116dDEE9F6B43aC78BA3", @@ -134,6 +135,7 @@ "BatchUniswapV2DirectToLiquidity": "0x0000000000000000000000000000000000000000", "BatchUniswapV3DirectToLiquidity": "0x0000000000000000000000000000000000000000", "EncryptedMarginalPrice": "0x96B52Ab3e5CAc0BbF49Be5039F2f9ef5d53bD322", + "FixedPriceBatch": "0xEDa0cC0bbb45D8cd6354755856053d6Ea646E201", "FixedPriceSale": "0x3661B7704F7032103B3122C7796B5E03fAC715b5", "OWNER": "0xB47C8e4bEb28af80eDe5E5bF474927b110Ef2c0e", "PERMIT2": "0x000000000022D473030F116dDEE9F6B43aC78BA3", @@ -197,6 +199,7 @@ "BatchUniswapV2DirectToLiquidity": "0x0000000000000000000000000000000000000000", "BatchUniswapV3DirectToLiquidity": "0x0000000000000000000000000000000000000000", "EncryptedMarginalPrice": "0x4e519eEf63b9e127cFCeCA31C8E5485CdA65D355", + "FixedPriceBatch": "0xC818f1f000f9C24D014BCe2c5334e14B1360d9CD", "FixedPriceSale": "0xacD10C2B4aA625dd00cba40E4466c8Ff07288a16", "OWNER": "0xB47C8e4bEb28af80eDe5E5bF474927b110Ef2c0e", "PERMIT2": "0x000000000022D473030F116dDEE9F6B43aC78BA3", From 8feaa2e636a9bb04a36789504f6813c48e97ffe7 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 28 May 2024 10:54:34 -0500 Subject: [PATCH 11/13] chore: rename deployment files --- ...um-sepolia-1716910513.json => .arbitrum-sepolia-fpb-v3.1.json} | 0 ...blast-sepolia-1716909826.json => .blast-sepolia-fpb-v3.1.json} | 0 ...{.mode-sepolia-1716911194.json => .mode-sepolia-fpb-v3.1.json} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename deployments/{.arbitrum-sepolia-1716910513.json => .arbitrum-sepolia-fpb-v3.1.json} (100%) rename deployments/{.blast-sepolia-1716909826.json => .blast-sepolia-fpb-v3.1.json} (100%) rename deployments/{.mode-sepolia-1716911194.json => .mode-sepolia-fpb-v3.1.json} (100%) diff --git a/deployments/.arbitrum-sepolia-1716910513.json b/deployments/.arbitrum-sepolia-fpb-v3.1.json similarity index 100% rename from deployments/.arbitrum-sepolia-1716910513.json rename to deployments/.arbitrum-sepolia-fpb-v3.1.json diff --git a/deployments/.blast-sepolia-1716909826.json b/deployments/.blast-sepolia-fpb-v3.1.json similarity index 100% rename from deployments/.blast-sepolia-1716909826.json rename to deployments/.blast-sepolia-fpb-v3.1.json diff --git a/deployments/.mode-sepolia-1716911194.json b/deployments/.mode-sepolia-fpb-v3.1.json similarity index 100% rename from deployments/.mode-sepolia-1716911194.json rename to deployments/.mode-sepolia-fpb-v3.1.json From d51dbb244a6a28fcf8b03548534d2ff98a427afa Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 29 May 2024 14:54:39 -0500 Subject: [PATCH 12/13] deploy: base sepolia --- deployments/.base-sepolia-allowlists-v3.1.json | 5 +++++ deployments/.base-sepolia-v3.1.json | 7 +++++++ script/env.json | 12 ++++++++---- script/salts/allowlist/AllowlistSalts.s.sol | 4 ++-- script/salts/salts.json | 6 +++++- 5 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 deployments/.base-sepolia-allowlists-v3.1.json create mode 100644 deployments/.base-sepolia-v3.1.json diff --git a/deployments/.base-sepolia-allowlists-v3.1.json b/deployments/.base-sepolia-allowlists-v3.1.json new file mode 100644 index 00000000..c32e97b3 --- /dev/null +++ b/deployments/.base-sepolia-allowlists-v3.1.json @@ -0,0 +1,5 @@ +{ +"axis.BatchCappedMerkleAllowlist": "0x98316f91B751d1ae41Fd668440db05Dc644b8112", +"axis.BatchMerkleAllowlist": "0x985AFB4b65Fa7f47c53CF4a44a1d6D8ad59Bf6F8", +"axis.BatchTokenAllowlist": "0x986fB5ac838e7DB6857c171d7ac846Fc875c306B" +} diff --git a/deployments/.base-sepolia-v3.1.json b/deployments/.base-sepolia-v3.1.json new file mode 100644 index 00000000..67432c67 --- /dev/null +++ b/deployments/.base-sepolia-v3.1.json @@ -0,0 +1,7 @@ +{ +"axis.BatchAuctionHouse": "0xBA000092028c37fdf4231090D9a0e42B3A983C17", +"axis.BatchCatalogue": "0xD55227c0C37C97Fa2619a9C7F658C173883C1E2a", +"axis.EncryptedMarginalPrice": "0x0599DA010907835037A0beC4525Dc5D600e790EB", +"axis.FixedPriceBatch": "0x71a2946A761FC6ecE1b16cb4517a3E3D7E30Cc92", +"axis.BatchLinearVesting": "0xc20918b09dE9708d2A7997dfFc3c5ACB34d4a15b" +} diff --git a/script/env.json b/script/env.json index 1e718dce..5120e382 100644 --- a/script/env.json +++ b/script/env.json @@ -69,12 +69,16 @@ "AtomicLinearVesting": "0x0000000000000000000000000000000000000000", "AtomicUniswapV2DirectToLiquidity": "0x0000000000000000000000000000000000000000", "AtomicUniswapV3DirectToLiquidity": "0x0000000000000000000000000000000000000000", - "BatchAuctionHouse": "0x0000000000000000000000000000000000000000", - "BatchCatalogue": "0x0000000000000000000000000000000000000000", - "BatchLinearVesting": "0x0000000000000000000000000000000000000000", + "BatchAuctionHouse": "0xBA000092028c37fdf4231090D9a0e42B3A983C17", + "BatchCappedMerkleAllowlist": "0x98316f91B751d1ae41Fd668440db05Dc644b8112", + "BatchCatalogue": "0xD55227c0C37C97Fa2619a9C7F658C173883C1E2a", + "BatchLinearVesting": "0xc20918b09dE9708d2A7997dfFc3c5ACB34d4a15b", + "BatchMerkleAllowlist": "0x985AFB4b65Fa7f47c53CF4a44a1d6D8ad59Bf6F8", + "BatchTokenAllowlist": "0x986fB5ac838e7DB6857c171d7ac846Fc875c306B", "BatchUniswapV2DirectToLiquidity": "0x0000000000000000000000000000000000000000", "BatchUniswapV3DirectToLiquidity": "0x0000000000000000000000000000000000000000", - "EncryptedMarginalPrice": "0x0000000000000000000000000000000000000000", + "EncryptedMarginalPrice": "0x0599DA010907835037A0beC4525Dc5D600e790EB", + "FixedPriceBatch": "0x71a2946A761FC6ecE1b16cb4517a3E3D7E30Cc92", "FixedPriceSale": "0x0000000000000000000000000000000000000000", "OWNER": "0xB47C8e4bEb28af80eDe5E5bF474927b110Ef2c0e", "PERMIT2": "0x000000000022D473030F116dDEE9F6B43aC78BA3", diff --git a/script/salts/allowlist/AllowlistSalts.s.sol b/script/salts/allowlist/AllowlistSalts.s.sol index ff594657..0379373f 100644 --- a/script/salts/allowlist/AllowlistSalts.s.sol +++ b/script/salts/allowlist/AllowlistSalts.s.sol @@ -22,9 +22,9 @@ contract AllowlistSalts is Script, WithEnvironment, WithSalts { _createBytecodeDirectory(); // Cache auction houses - _envAtomicAuctionHouse = _envAddressNotZero("axis.AtomicAuctionHouse"); + _envAtomicAuctionHouse = _envAddress("axis.AtomicAuctionHouse"); console2.log("AtomicAuctionHouse:", _envAtomicAuctionHouse); - _envBatchAuctionHouse = _envAddressNotZero("axis.BatchAuctionHouse"); + _envBatchAuctionHouse = _envAddress("axis.BatchAuctionHouse"); console2.log("BatchAuctionHouse:", _envBatchAuctionHouse); } diff --git a/script/salts/salts.json b/script/salts/salts.json index 0f663619..933349c9 100644 --- a/script/salts/salts.json +++ b/script/salts/salts.json @@ -3,6 +3,7 @@ "0xc34e46cfceb1e62d804e8197ba829a150e216e433ee21a68b96f1edd3abd4dd9": "0x87bc14fda5bd97c7e883ae1227f30462125ebcf6800a34d10f39af4ac46e84b9" }, "BatchAuctionHouse": { + "0x1d8b7b9cfbd8610a556d5e2e85dcdb25a17d6b5407574aef07970a86e4b0e2c3": "0x06374b73456c869d550c6cb45aff40113752ac3c4dd8efb8185b6da5c898aa82", "0x4c4d86f9737bd18dab3f6dc74d2f5c610b7169459c90a457e0e126ed42ae3bba": "0xbe4a9dc1b73685497c6104df59c2a3d2c1c5039bd48b1b25e9c0a029f3744311" }, "BlastAtomicAuctionHouse": { @@ -12,12 +13,14 @@ "0xc640e527fdcd05d6135de917e29082984847300ec6bf4cf38393f8dbfa742b19": "0x1e7690f0ac2409cb3804ffe443d81e9685d882d4d1804c8bfb1056cc624afee8" }, "CappedMerkleAllowlist": { + "0x0ae7777d88dd21a8b9ca3dd9212307295a83aed438fef9dad0b62d57fbaf1025": "0x1de5ae5b126bd2cee8eb4f083080f5c30baa692580cf823fa5f382a7bfc70ac5", "0x7629e3374867d748eaab783ad2eedb4e39634e55428c39713fd82415d41043a3": "0x31246341c7c82c31414f184b807da273fdd6cdf591cb6f997fec41babc279fd9", "0x78ee3a1a70f7e9a784590ca00ef3290f3478105f6338266b9e5f0d10951b4aa9": "0xdb5af3b6d32454d69075509db94196460a19acb9849c5dc0d5ccef5a1b4ab032" }, "MerkleAllowlist": { "0x8dabf7c769c996b5b33b4da1013d93b9daeeb7cf468d179804c9130fd7eaa891": "0x71072df633972dfa166196430a104dbc30b8dc5997a8b965253ca299fd96feaf", - "0xb4128aa3cf1f256d76081a556cbe66c83429e49ce28bca3b2e3b07b926f0bda8": "0xcbac00a3b5465e91d8c65d179de9fa8f52be1c99b8ecfd9ce35b51faa0046232" + "0xb4128aa3cf1f256d76081a556cbe66c83429e49ce28bca3b2e3b07b926f0bda8": "0xcbac00a3b5465e91d8c65d179de9fa8f52be1c99b8ecfd9ce35b51faa0046232", + "0xc8e647c61be9e05b08050eb7d8cfc1102b5328d9540b81891da5783ba25d2978": "0x368c44b5208a15f6292a0ac1246d7d6973d5c919d32cebd2f30da0844356e63a" }, "Test_CappedMerkleAllowlist": { "0x949cc973758fec263c28001984b366da5a91b8c2808ff8a54d0a15d553fbb119": "0x3152e9b2cb8c87f150a66f3e6445095a816f624d33fe0837e565c6cf7dab4365", @@ -50,6 +53,7 @@ "0x205b2d8780f5fa0bb6edef93607aa4f671b524b5d2b2f3278d238d08dfdf900c": "0x52c359e9c03d7e4b2af98f5b70fc386406184608f7f882d2789be69a2743d09a" }, "TokenAllowlist": { + "0x09db47d395a68db033a3b222b9d1a212cec8422b03aeafc313aa4d2813c6dd60": "0x1c4bc5002d793fc427b068abb9dab670aad1864bab04a981d134b7459103f693", "0x30d8cf89cc8c815884740dc27e72a1f8b5dacbbe39f3d1e35deb436176390b20": "0xd3b0a4a73b330c13d6fef29f392e398db9099d4de7cc69ccbe71a2e21d7687dc", "0xcc0cb46859311aa362f48add9ffc284efd8aad3e4ec5a29b68eee7ab1b4cd8c4": "0x38fa3d6714b18bea94ce4f3f8a3491d0873d4fbc8527002cfb153c385c093e72" } From f4233a5543e706333287099da4bbe48c6168379a Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 4 Jun 2024 20:39:38 -0500 Subject: [PATCH 13/13] fix: FPB did not settle due to rounding error (issue 201) --- src/modules/auctions/batch/FPB.sol | 9 +++++++ test/modules/auctions/FPB/settle.t.sol | 37 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/modules/auctions/batch/FPB.sol b/src/modules/auctions/batch/FPB.sol index 31daa650..bf2cfd62 100644 --- a/src/modules/auctions/batch/FPB.sol +++ b/src/modules/auctions/batch/FPB.sol @@ -191,6 +191,15 @@ contract FixedPriceBatch is BatchAuctionModule, IFixedPriceBatch { // Decrement the total bid amount by the refund data.totalBidAmount -= _lotPartialFill[lotId_].refund; + + // Calculate the updated filled capacity + uint256 filledCapacity = Math.fullMulDiv(data.totalBidAmount, baseScale, data.price); + + // Compare this with minimum filled and update if needed + // We do this to ensure that slight rounding errors do not cause + // the auction to not clear when the capacity is actually filled + // This generally can only happen when the min fill is 100% + if (filledCapacity < data.minFilled) data.minFilled = filledCapacity; } // End the auction diff --git a/test/modules/auctions/FPB/settle.t.sol b/test/modules/auctions/FPB/settle.t.sol index 77c36bad..592d78e7 100644 --- a/test/modules/auctions/FPB/settle.t.sol +++ b/test/modules/auctions/FPB/settle.t.sol @@ -6,6 +6,7 @@ import {IAuction} from "src/interfaces/modules/IAuction.sol"; import {IFixedPriceBatch} from "src/interfaces/modules/auctions/IFixedPriceBatch.sol"; import {FpbTest} from "test/modules/auctions/FPB/FPBTest.sol"; +import {console2} from "lib/forge-std/src/console2.sol"; contract FpbSettleTest is FpbTest { // [X] when the caller is not the parent @@ -181,4 +182,40 @@ contract FpbSettleTest is FpbTest { assertEq(auctionData.settlementCleared, false, "settlementCleared"); assertEq(auctionData.totalBidAmount, 6e18, "totalBidAmount"); } + + // Added per ethersky's review (issue 201) to avoid a rounding issue which prevents settlement + function test_settle_doesNotBrick() + public + givenPrice(2e18) + givenMinFillPercent(100e3) + givenLotCapacity(10e18) + givenLotIsCreated + givenLotHasStarted + { + vm.prank(address(_auctionHouse)); + _module.bid(_lotId, _BIDDER, _REFERRER, 2e19 - 1, abi.encode("")); + + IFixedPriceBatch.AuctionData memory auctionData_before = _module.getAuctionData(_lotId); + IAuction.Lot memory lot_before = _module.getLot(_lotId); + console2.log("totalBidAmount before ==> ", auctionData_before.totalBidAmount); + console2.log("conclusion before ==> ", lot_before.conclusion); + + vm.prank(address(_auctionHouse)); + _module.bid(_lotId, _BIDDER, _REFERRER, 1e18 + 1, abi.encode("")); + + IFixedPriceBatch.AuctionData memory auctionData_after = _module.getAuctionData(_lotId); + IAuction.Lot memory lot_after = _module.getLot(_lotId); + + console2.log("totalBidAmount after ==> ", auctionData_after.totalBidAmount); + console2.log("conclusion after ==> ", lot_after.conclusion); + assertLt(lot_after.conclusion, lot_before.conclusion); + + vm.prank(address(_auctionHouse)); + _module.settle(_lotId, 100_000); + + IFixedPriceBatch.AuctionData memory auctionData_final = _module.getAuctionData(_lotId); + + console2.log("settlementCleared final ==> ", auctionData_final.settlementCleared); + assert(auctionData_final.settlementCleared); + } }