From d01ceec2ecbe47adb9c329270d82de4e85d08175 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 11 Jan 2024 12:12:06 -0600 Subject: [PATCH 001/117] feat: off-chain auction settlement first pass --- src/AuctionHouse.sol | 159 +++++++++++++++++++-- src/modules/Auction.sol | 14 +- test/modules/Auction/MockAuctionModule.sol | 8 +- 3 files changed, 161 insertions(+), 20 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 038694f4..54be3028 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -81,8 +81,14 @@ abstract contract Router is FeeManager { // Off-chain auction variant function settle( uint256 id_, - Auction.Bid[] memory bids_ - ) external virtual returns (uint256[] memory amountsOut); + Auction.Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] calldata amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_, + bytes[] calldata approvals_, + bytes[] calldata allowlistProofs_ + ) external virtual; } /// @title AuctionHouse @@ -95,6 +101,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== ERRORS ========== // error AmountLessThanMinimum(); error InvalidHook(); + error InvalidBidder(address bidder_); + error NotAuthorized(); error UnsupportedToken(ERC20 token_); // ========== EVENTS ========== // @@ -112,14 +120,23 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { ERC20 quoteToken_, uint256 amount_ ) internal returns (uint256 totalFees) { + // Calculate fees for purchase + (uint256 toReferrer, uint256 toProtocol) = calculateFees(referrer_, quoteToken_, amount_); + + // Update fee balances if non-zero + if (toReferrer > 0) rewards[referrer_][quoteToken_] += toReferrer; + if (toProtocol > 0) rewards[PROTOCOL][quoteToken_] += toProtocol; + + return toReferrer + toProtocol; + } + + function calculateFees(address referrer_, ERC20 quoteToken_, uint256 amount_) internal view returns (uint256 toReferrer, uint256 toProtocol) { // TODO should protocol and/or referrer be able to charge different fees based on the type of auction being used? // Calculate fees for purchase // 1. Calculate referrer fee // 2. Calculate protocol fee as the total expected fee amount minus the referrer fee // to avoid issues with rounding from separate fee calculations - uint256 toReferrer; - uint256 toProtocol; if (referrer_ == address(0)) { // There is no referrer toProtocol = (amount_ * protocolFee) / FEE_DECIMALS; @@ -127,9 +144,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 referrerFee = referrerFees[referrer_]; // reduce to single SLOAD if (referrerFee == 0) { // There is a referrer, but they have not set a fee - // If protocol fee is zero, return zero - // Otherwise, calcualte protocol fee - if (protocolFee == 0) return 0; toProtocol = (amount_ * protocolFee) / FEE_DECIMALS; } else { // There is a referrer and they have set a fee @@ -137,12 +151,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { toProtocol = ((amount_ * (protocolFee + referrerFee)) / FEE_DECIMALS) - toReferrer; } } - - // Update fee balances if non-zero - if (toReferrer > 0) rewards[referrer_][quoteToken_] += toReferrer; - if (toProtocol > 0) rewards[PROTOCOL][quoteToken_] += toProtocol; - - return toReferrer + toProtocol; } function purchase( @@ -160,6 +168,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Load routing data for the lot Routing memory routing = lotRouting[id_]; + // Check that sender is on the allowlist, if there is one + // TODO + + + // Calculate fees for purchase uint256 totalFees = allocateFees(referrer_, routing.quoteToken, amount_); // Send purchase to auction house and get payout plus any extra output @@ -183,6 +196,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { emit Purchase(id_, msg.sender, referrer_, amount_, payout); } + // TODO need a delegated execution function for purchase and bid because we check allowlist on the caller in the normal functions + function bid( address recipient_, address referrer_, @@ -200,11 +215,125 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Off-chain auction variant + // Lots of parameters, likely need to consolidate function settle( uint256 id_, - Auction.Bid[] memory bids_ - ) external override returns (uint256[] memory amountsOut) { + Auction.Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] calldata amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_, + bytes[] calldata approvals_, + bytes[] calldata allowlistProofs_ + ) external override { + // Load routing data for the lot + Routing memory routing = lotRouting[id_]; + + // Validate that sender is authorized to settle the auction // TODO + + // Validate array lengths all match + uint256 len = winningBids_.length; + if (len != bidSignatures_.length || len != amountsIn_.length || len != amountsOut_.length || len != approvals_.length || len != allowlistProofs_.length) revert InvalidParams(); + + // Bid-level validation and fee calculations + uint256[] memory amountsInLessFees = new uint256[](len); + uint256 totalProtocolFee; + uint256 totalAmountInLessFees; + uint256 totalAmountOut; + for (uint256 i; i < len; i++) { + // If there is an allowlist, validate that the winners are on the allowlist + if (address(routing.allowlist) != address(0)) { + if (!routing.allowlist.isAllowed(winningBids_[i].bidder, allowlistProofs_[i])) revert InvalidBidder(winningBids_[i].bidder); + } + + // Check that the amounts out are at least the minimum specified by the bidder + // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder + // If so, we need to adjust the minAmountOut proportionally for the slippage check + // We also verify that the amountIn is not more than the bidder specified + uint256 minAmountOut = winningBids_[i].minAmountOut; + if (amountsIn_[i] > winningBids_[i].amount) { + revert InvalidParams(); + } else if (amountsIn_[i] < winningBids_[i].amount) { + minAmountOut = (minAmountOut * amountsIn_[i]) / winningBids_[i].amount; // TODO need to think about scaling and rounding here + } + if (amountsOut_[i] < minAmountOut) revert AmountLessThanMinimum(); + + // Calculate fees from bid amount + (uint256 toReferrer, uint256 toProtocol) = calculateFees(winningBids_[i].referrer, routing.quoteToken, amountsIn_[i]); + amountsInLessFees[i] = amountsIn_[i] - toReferrer - toProtocol; + + // Update referrer fee balances if non-zero and increment the total protocol fee + if (toReferrer > 0) rewards[winningBids_[i].referrer][routing.quoteToken] += toReferrer; + totalProtocolFee += toProtocol; + + // Increment total amount out + totalAmountInLessFees += amountsInLessFees[i]; + totalAmountOut += amountsOut_[i]; + } + + // Update protocol fee if not zero + if (totalProtocolFee > 0) rewards[PROTOCOL][routing.quoteToken] += totalProtocolFee; + + // Send auction inputs to auction module to validate settlement + // We do this because the format of the bids and signatures is specific to the auction module + // Some common things to check: + // 1. Total of amounts out is not greater than capacity + // 2. Minimum price is enforced + // 3. Minimum bid size is enforced + // 4. Minimum capacity sold is enforced + AuctionModule module = _getModuleForId(id_); + + // TODO update auction module interface and base function to handle these inputs, and perhaps others + bytes memory auctionOutput = module.settle(id_, winningBids_, bidSignatures_, amountsInLessFees, amountsOut_, validityProof_); + + // Iterate through bids, handling transfers and payouts + // Have to transfer to auction house first since fee is in quote token + // Check balance before and after to ensure full amount received, revert if not + // Handles edge cases like fee-on-transfer tokens (which are not supported) + for (uint256 i; i < len; i++) { + // TODO use permit2 approvals if provided + + uint256 quoteBalance = routing.quoteToken.balanceOf(address(this)); + routing.quoteToken.safeTransferFrom(msg.sender, address(this), amountsIn_[i]); + if (routing.quoteToken.balanceOf(address(this)) < quoteBalance + amountsIn_[i]) { + revert UnsupportedToken(routing.quoteToken); + } + } + + // If hooks address supplied, transfer tokens from auction house to hooks contract, + // then execute the hook function, and ensure proper amount of tokens transferred in. + if (address(routing.hooks) != address(0)) { + // Send quote token to callback (transferred in first to allow use during callback) + routing.quoteToken.safeTransfer(address(routing.hooks), totalAmountInLessFees); + + // Call the callback function to receive payout tokens for payout + uint256 baseBalance = routing.baseToken.balanceOf(address(this)); + routing.hooks.mid(id_, totalAmountInLessFees, totalAmountOut); + + // Check to ensure that the callback sent the requested amount of payout tokens back to the teller + if (routing.baseToken.balanceOf(address(this)) < (baseBalance + totalAmountOut)) { + revert InvalidHook(); + } + } else { + // If no hook is provided, transfer tokens from auction owner to this contract + // for payout. + // Check balance before and after to ensure full amount received, revert if not + // Handles edge cases like fee-on-transfer tokens (which are not supported) + uint256 baseBalance = routing.baseToken.balanceOf(address(this)); + routing.baseToken.safeTransferFrom(routing.owner, address(this), totalAmountOut); + if (routing.baseToken.balanceOf(address(this)) < (baseBalance + totalAmountOut)) { + revert UnsupportedToken(routing.baseToken); + } + + routing.quoteToken.safeTransfer(routing.owner, totalAmountInLessFees); + } + + // Handle payouts to bidders + for (uint256 i; i < len; i++) { + // Handle payout to user, including creation of derivative tokens + _handlePayout(routing, winningBids_[i].bidder, amountsOut_[i], auctionOutput); + } } // ============ INTERNAL EXECUTION FUNCTIONS ========== // diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 8e4ba3e1..7944f325 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -39,11 +39,15 @@ abstract contract Auction { uint256 purchased; // quote tokens in } + // TODO pack if we anticipate on-chain auction variants struct Bid { + uint256 lotId; address bidder; + address recipient; + address referrer; uint256 amount; uint256 minAmountOut; - bytes32 param; // optional implementation-specific parameter for the bid + bytes32 auctionParam; // optional implementation-specific parameter for the bid } struct AuctionParams { @@ -89,8 +93,12 @@ abstract contract Auction { // TODO use solady data packing library to make bids smaller on the actual module to store? function settle( uint256 id_, - Bid[] memory bids_ - ) external virtual returns (uint256[] memory amountsOut); + Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] memory amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_ + ) external virtual returns (bytes memory); // ========== AUCTION MANAGEMENT ========== // diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index dbae60b2..2d846fa8 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -49,8 +49,12 @@ contract MockAuctionModule is AuctionModule { function settle( uint256 id_, - Bid[] memory bids_ - ) external virtual override returns (uint256[] memory amountsOut) {} + Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] memory amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_ + ) external virtual override returns (bytes memory) {} function payoutFor( uint256 id_, From 50ddf9468a38665b3d23d90d29d987d74f1c03a1 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 11 Jan 2024 12:42:35 -0600 Subject: [PATCH 002/117] refactor: make settle params into struct --- src/AuctionHouse.sol | 64 ++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 54be3028..4aaa9aa9 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -20,6 +20,18 @@ abstract contract FeeManager { } abstract contract Router is FeeManager { + // ========== DATA STRUCTURES ========== // + struct Settlement { + Auction.Bid[] winningBids; + bytes[] bidSignatures; + uint256[] amountsIn; + uint256[] amountsOut; + bytes validityProof; + bytes[] approvals; // optional, permit 2 token approvals + bytes[] allowlistProofs; // optional, allowlist proofs + } + + // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -81,13 +93,7 @@ abstract contract Router is FeeManager { // Off-chain auction variant function settle( uint256 id_, - Auction.Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] calldata amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_, - bytes[] calldata approvals_, - bytes[] calldata allowlistProofs_ + Settlement memory settlement_ ) external virtual; } @@ -121,7 +127,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 amount_ ) internal returns (uint256 totalFees) { // Calculate fees for purchase - (uint256 toReferrer, uint256 toProtocol) = calculateFees(referrer_, quoteToken_, amount_); + (uint256 toReferrer, uint256 toProtocol) = calculateFees(referrer_, amount_); // Update fee balances if non-zero if (toReferrer > 0) rewards[referrer_][quoteToken_] += toReferrer; @@ -130,7 +136,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return toReferrer + toProtocol; } - function calculateFees(address referrer_, ERC20 quoteToken_, uint256 amount_) internal view returns (uint256 toReferrer, uint256 toProtocol) { + function calculateFees(address referrer_, uint256 amount_) internal view returns (uint256 toReferrer, uint256 toProtocol) { // TODO should protocol and/or referrer be able to charge different fees based on the type of auction being used? // Calculate fees for purchase @@ -218,13 +224,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Lots of parameters, likely need to consolidate function settle( uint256 id_, - Auction.Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] calldata amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_, - bytes[] calldata approvals_, - bytes[] calldata allowlistProofs_ + Settlement memory settlement_ ) external override { // Load routing data for the lot Routing memory routing = lotRouting[id_]; @@ -233,8 +233,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO // Validate array lengths all match - uint256 len = winningBids_.length; - if (len != bidSignatures_.length || len != amountsIn_.length || len != amountsOut_.length || len != approvals_.length || len != allowlistProofs_.length) revert InvalidParams(); + uint256 len = settlement_.winningBids.length; + if (len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length || len != settlement_.amountsOut.length || len != settlement_.approvals.length || len != settlement_.allowlistProofs.length) revert InvalidParams(); // Bid-level validation and fee calculations uint256[] memory amountsInLessFees = new uint256[](len); @@ -244,32 +244,32 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { for (uint256 i; i < len; i++) { // If there is an allowlist, validate that the winners are on the allowlist if (address(routing.allowlist) != address(0)) { - if (!routing.allowlist.isAllowed(winningBids_[i].bidder, allowlistProofs_[i])) revert InvalidBidder(winningBids_[i].bidder); + if (!routing.allowlist.isAllowed(settlement_.winningBids[i].bidder, settlement_.allowlistProofs[i])) revert InvalidBidder(settlement_.winningBids[i].bidder); } // Check that the amounts out are at least the minimum specified by the bidder // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder // If so, we need to adjust the minAmountOut proportionally for the slippage check // We also verify that the amountIn is not more than the bidder specified - uint256 minAmountOut = winningBids_[i].minAmountOut; - if (amountsIn_[i] > winningBids_[i].amount) { + uint256 minAmountOut = settlement_.winningBids[i].minAmountOut; + if (settlement_.amountsIn[i] > settlement_.winningBids[i].amount) { revert InvalidParams(); - } else if (amountsIn_[i] < winningBids_[i].amount) { - minAmountOut = (minAmountOut * amountsIn_[i]) / winningBids_[i].amount; // TODO need to think about scaling and rounding here + } else if (settlement_.amountsIn[i] < settlement_.winningBids[i].amount) { + minAmountOut = (minAmountOut * settlement_.amountsIn[i]) / settlement_.winningBids[i].amount; // TODO need to think about scaling and rounding here } - if (amountsOut_[i] < minAmountOut) revert AmountLessThanMinimum(); + if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); // Calculate fees from bid amount - (uint256 toReferrer, uint256 toProtocol) = calculateFees(winningBids_[i].referrer, routing.quoteToken, amountsIn_[i]); - amountsInLessFees[i] = amountsIn_[i] - toReferrer - toProtocol; + (uint256 toReferrer, uint256 toProtocol) = calculateFees(settlement_.winningBids[i].referrer, settlement_.amountsIn[i]); + amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; // Update referrer fee balances if non-zero and increment the total protocol fee - if (toReferrer > 0) rewards[winningBids_[i].referrer][routing.quoteToken] += toReferrer; + if (toReferrer > 0) rewards[settlement_.winningBids[i].referrer][routing.quoteToken] += toReferrer; totalProtocolFee += toProtocol; // Increment total amount out totalAmountInLessFees += amountsInLessFees[i]; - totalAmountOut += amountsOut_[i]; + totalAmountOut += settlement_.amountsOut[i]; } // Update protocol fee if not zero @@ -285,7 +285,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { AuctionModule module = _getModuleForId(id_); // TODO update auction module interface and base function to handle these inputs, and perhaps others - bytes memory auctionOutput = module.settle(id_, winningBids_, bidSignatures_, amountsInLessFees, amountsOut_, validityProof_); + bytes memory auctionOutput = module.settle(id_, settlement_.winningBids, settlement_.bidSignatures, amountsInLessFees, settlement_.amountsOut, settlement_.validityProof); // Iterate through bids, handling transfers and payouts // Have to transfer to auction house first since fee is in quote token @@ -295,8 +295,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO use permit2 approvals if provided uint256 quoteBalance = routing.quoteToken.balanceOf(address(this)); - routing.quoteToken.safeTransferFrom(msg.sender, address(this), amountsIn_[i]); - if (routing.quoteToken.balanceOf(address(this)) < quoteBalance + amountsIn_[i]) { + routing.quoteToken.safeTransferFrom(msg.sender, address(this), settlement_.amountsIn[i]); + if (routing.quoteToken.balanceOf(address(this)) < quoteBalance + settlement_.amountsIn[i]) { revert UnsupportedToken(routing.quoteToken); } } @@ -332,7 +332,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders for (uint256 i; i < len; i++) { // Handle payout to user, including creation of derivative tokens - _handlePayout(routing, winningBids_[i].bidder, amountsOut_[i], auctionOutput); + _handlePayout(routing, settlement_.winningBids[i].bidder, settlement_.amountsOut[i], auctionOutput); } } From e58207746d93da3e2f4593c0e8fc8b922ec32e5d Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 11 Jan 2024 12:42:48 -0600 Subject: [PATCH 003/117] chore: run linter --- src/AuctionHouse.sol | 61 +++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 4aaa9aa9..f5715dd9 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -31,7 +31,6 @@ abstract contract Router is FeeManager { bytes[] allowlistProofs; // optional, allowlist proofs } - // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -91,10 +90,7 @@ abstract contract Router is FeeManager { function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); // Off-chain auction variant - function settle( - uint256 id_, - Settlement memory settlement_ - ) external virtual; + function settle(uint256 id_, Settlement memory settlement_) external virtual; } /// @title AuctionHouse @@ -136,7 +132,10 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return toReferrer + toProtocol; } - function calculateFees(address referrer_, uint256 amount_) internal view returns (uint256 toReferrer, uint256 toProtocol) { + function calculateFees( + address referrer_, + uint256 amount_ + ) internal view returns (uint256 toReferrer, uint256 toProtocol) { // TODO should protocol and/or referrer be able to charge different fees based on the type of auction being used? // Calculate fees for purchase @@ -177,7 +176,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Check that sender is on the allowlist, if there is one // TODO - // Calculate fees for purchase uint256 totalFees = allocateFees(referrer_, routing.quoteToken, amount_); @@ -222,10 +220,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Off-chain auction variant // Lots of parameters, likely need to consolidate - function settle( - uint256 id_, - Settlement memory settlement_ - ) external override { + function settle(uint256 id_, Settlement memory settlement_) external override { // Load routing data for the lot Routing memory routing = lotRouting[id_]; @@ -234,7 +229,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Validate array lengths all match uint256 len = settlement_.winningBids.length; - if (len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length || len != settlement_.amountsOut.length || len != settlement_.approvals.length || len != settlement_.allowlistProofs.length) revert InvalidParams(); + if ( + len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length + || len != settlement_.amountsOut.length || len != settlement_.approvals.length + || len != settlement_.allowlistProofs.length + ) revert InvalidParams(); // Bid-level validation and fee calculations uint256[] memory amountsInLessFees = new uint256[](len); @@ -244,7 +243,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { for (uint256 i; i < len; i++) { // If there is an allowlist, validate that the winners are on the allowlist if (address(routing.allowlist) != address(0)) { - if (!routing.allowlist.isAllowed(settlement_.winningBids[i].bidder, settlement_.allowlistProofs[i])) revert InvalidBidder(settlement_.winningBids[i].bidder); + if ( + !routing.allowlist.isAllowed( + settlement_.winningBids[i].bidder, settlement_.allowlistProofs[i] + ) + ) revert InvalidBidder(settlement_.winningBids[i].bidder); } // Check that the amounts out are at least the minimum specified by the bidder @@ -255,16 +258,20 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { if (settlement_.amountsIn[i] > settlement_.winningBids[i].amount) { revert InvalidParams(); } else if (settlement_.amountsIn[i] < settlement_.winningBids[i].amount) { - minAmountOut = (minAmountOut * settlement_.amountsIn[i]) / settlement_.winningBids[i].amount; // TODO need to think about scaling and rounding here + minAmountOut = + (minAmountOut * settlement_.amountsIn[i]) / settlement_.winningBids[i].amount; // TODO need to think about scaling and rounding here } if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); // Calculate fees from bid amount - (uint256 toReferrer, uint256 toProtocol) = calculateFees(settlement_.winningBids[i].referrer, settlement_.amountsIn[i]); + (uint256 toReferrer, uint256 toProtocol) = + calculateFees(settlement_.winningBids[i].referrer, settlement_.amountsIn[i]); amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; // Update referrer fee balances if non-zero and increment the total protocol fee - if (toReferrer > 0) rewards[settlement_.winningBids[i].referrer][routing.quoteToken] += toReferrer; + if (toReferrer > 0) { + rewards[settlement_.winningBids[i].referrer][routing.quoteToken] += toReferrer; + } totalProtocolFee += toProtocol; // Increment total amount out @@ -285,8 +292,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { AuctionModule module = _getModuleForId(id_); // TODO update auction module interface and base function to handle these inputs, and perhaps others - bytes memory auctionOutput = module.settle(id_, settlement_.winningBids, settlement_.bidSignatures, amountsInLessFees, settlement_.amountsOut, settlement_.validityProof); - + bytes memory auctionOutput = module.settle( + id_, + settlement_.winningBids, + settlement_.bidSignatures, + amountsInLessFees, + settlement_.amountsOut, + settlement_.validityProof + ); + // Iterate through bids, handling transfers and payouts // Have to transfer to auction house first since fee is in quote token // Check balance before and after to ensure full amount received, revert if not @@ -296,12 +310,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 quoteBalance = routing.quoteToken.balanceOf(address(this)); routing.quoteToken.safeTransferFrom(msg.sender, address(this), settlement_.amountsIn[i]); - if (routing.quoteToken.balanceOf(address(this)) < quoteBalance + settlement_.amountsIn[i]) { + if ( + routing.quoteToken.balanceOf(address(this)) + < quoteBalance + settlement_.amountsIn[i] + ) { revert UnsupportedToken(routing.quoteToken); } } - // If hooks address supplied, transfer tokens from auction house to hooks contract, + // If hooks address supplied, transfer tokens from auction house to hooks contract, // then execute the hook function, and ensure proper amount of tokens transferred in. if (address(routing.hooks) != address(0)) { // Send quote token to callback (transferred in first to allow use during callback) @@ -332,7 +349,9 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders for (uint256 i; i < len; i++) { // Handle payout to user, including creation of derivative tokens - _handlePayout(routing, settlement_.winningBids[i].bidder, settlement_.amountsOut[i], auctionOutput); + _handlePayout( + routing, settlement_.winningBids[i].bidder, settlement_.amountsOut[i], auctionOutput + ); } } From 209414d9321081a91caf9658a7ed2675fad834e3 Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 17 Jan 2024 09:43:25 -0600 Subject: [PATCH 004/117] wip: separate external and local settlement --- src/AuctionHouse.sol | 27 ++-- src/modules/auctions/EBA.SOL | 3 + src/modules/auctions/bases/BatchAuction.sol | 170 +++++++++++--------- 3 files changed, 112 insertions(+), 88 deletions(-) create mode 100644 src/modules/auctions/EBA.SOL diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index f5715dd9..bbfa7968 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -21,12 +21,19 @@ abstract contract FeeManager { abstract contract Router is FeeManager { // ========== DATA STRUCTURES ========== // - struct Settlement { - Auction.Bid[] winningBids; - bytes[] bidSignatures; - uint256[] amountsIn; - uint256[] amountsOut; - bytes validityProof; + struct ExternalSettlement { + Auction.Bid[] bids; // user bids submitted externally + bytes[] bidSignatures; // user signatures for bids submitted externally + bytes[] approvals; // optional, permit 2 token approvals + bytes[] allowlistProofs; // optional, allowlist proofs + uint256[] amountsIn; // actual amount in for the corresponding bids + uint256[] amountsOut; // actual amount out for the corresponding bids + bytes validityProof; // optional, provide proof of settlement validity to be verified by module + } + + struct LocalSettlement { + Auction.Bid[] bids; // user bids submitted externally + bytes[] bidSignatures; // user signatures for bids submitted externally bytes[] approvals; // optional, permit 2 token approvals bytes[] allowlistProofs; // optional, allowlist proofs } @@ -218,9 +225,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO } - // Off-chain auction variant - // Lots of parameters, likely need to consolidate - function settle(uint256 id_, Settlement memory settlement_) external override { + // External submission and local evaluation + function settle(uint256 id_, LocalSettlement memory settlement_) external override {} + + // External submission and evaluation + function settle(uint256 id_, ExternalSettlement memory settlement_) external override { // Load routing data for the lot Routing memory routing = lotRouting[id_]; diff --git a/src/modules/auctions/EBA.SOL b/src/modules/auctions/EBA.SOL new file mode 100644 index 00000000..b293390e --- /dev/null +++ b/src/modules/auctions/EBA.SOL @@ -0,0 +1,3 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 6c029897..f1edffac 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -1,108 +1,120 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -// import "src/modules/Auction.sol"; +import "src/modules/Auction.sol"; -// // Spec -// // - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token -// // - Purchasers will submit orders off-chain that will be batched and submitted at the end of the auction by a Teller. All Tellers should be able to execute batches of orders? -// // - The issuer will provide all relevant information for the running of the batch auction to this contract. Some parameters for derivatives of the payout will be passed onto and processed by the Teller. -// // - The issuer should be able to auction different variables in the purchase. -// // I need to determine if this should be handled by different batch auctioneers. -// // - There are some overlap with the variables used in Live Auctions, so those should be abstracted and inherited so we don't repeat ourselves. -// // - Data needed for a batch auction: -// // - capacity - amount of tokens being sold (or bought?) -// // - quote token -// // - payout token -// // - teller -// // - teller params -// // - duration (start & conclusion) -// // - allowlist -// // - amount sold & amount purchased - do we need to track this since it is just for historical purposes? can we emit the data in an event? -// // - minimum value to settle auction - minimum value for whatever parameter is being auctioned. -// // need to think if we need to have a maximum value option, but it can probably just use an inverse. -// // - info to tell the teller what the auctioned value is and how to settle the auction. need to think on this more +// Spec +// - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token +// - Purchasers will submit orders off-chain that will be batched and submitted at the end of the auction by a Teller. All Tellers should be able to execute batches of orders? +// - The issuer will provide all relevant information for the running of the batch auction to this contract. Some parameters for derivatives of the payout will be passed onto and processed by the Teller. +// - The issuer should be able to auction different variables in the purchase. +// I need to determine if this should be handled by different batch auctioneers. +// - There are some overlap with the variables used in Live Auctions, so those should be abstracted and inherited so we don't repeat ourselves. +// - Data needed for a batch auction: +// - capacity - amount of tokens being sold (or bought?) +// - quote token +// - payout token +// - teller +// - teller params +// - duration (start & conclusion) +// - allowlist +// - amount sold & amount purchased - do we need to track this since it is just for historical purposes? can we emit the data in an event? +// - minimum value to settle auction - minimum value for whatever parameter is being auctioned. +// need to think if we need to have a maximum value option, but it can probably just use an inverse. +// - info to tell the teller what the auctioned value is and how to settle the auction. need to think on this more -// abstract contract BatchAuction { -// error BatchAuction_NotConcluded(); +abstract contract BatchAuction { + error BatchAuction_NotConcluded(); -// // ========== STATE VARIABLES ========== // + -// mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; + // ========== AUCTION INFORMATION ========== // -// // ========== AUCTION INFORMATION ========== // + // TODO add batch auction specific getters +} -// // TODO add batch auction specific getters -// } +abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { -// abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { + // ========== STATE VARIABLES ========== // -// function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { -// // TODO -// // Validate inputs + mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; -// // Execute user approval if provided? + function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + // TODO + // Validate inputs -// // Call implementation specific bid logic + // Execute user approval if provided? -// // Store bid data -// } + // Call implementation specific bid logic -// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { -// // TODO -// // Validate inputs + // Store bid data + } -// // Call implementation specific settle logic + function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + // TODO + // Validate inputs -// // Store settle data -// } + // Call implementation specific settle logic -// function settle(uint256 id_, Auction.Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { -// revert Auction_NotImplemented(); -// } -// } + // Store settle data + } -// abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { + function settle(uint256 id_, Auction.Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { + revert Auction_NotImplemented(); + } +} -// // ========== AUCTION EXECUTION ========== // +abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { -// function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { -// revert Auction_NotImplemented(); -// } + // ========== AUCTION EXECUTION ========== // -// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { -// revert Auction_NotImplemented(); -// } + function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + revert Auction_NotImplemented(); + } -// /// @notice Settle a batch auction with the provided bids -// function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { -// Lot storage lot = lotData[id_]; + function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + revert Auction_NotImplemented(); + } -// // Must be past the conclusion time to settle -// if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); + /// @notice Settle a batch auction with the provided bids + function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { + Lot storage lot = lotData[id_]; -// // Bids must not be greater than the capacity -// uint256 len = bids_.length; -// uint256 sum; -// if (lot.capacityInQuote) { -// for (uint256 i; i < len; i++) { -// sum += bids_[i].amount; -// } -// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); -// } else { -// for (uint256 i; i < len; i++) { -// sum += bids_[i].minAmountOut; -// } -// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); -// } + // Must be past the conclusion time to settle + if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); -// // TODO other generic validation? -// // Check approvals in the Auctioneer since it handles token transfers + // Bids must not be greater than the capacity + uint256 len = bids_.length; + uint256 sum; + if (lot.capacityInQuote) { + for (uint256 i; i < len; i++) { + sum += bids_[i].amount; + } + if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); + } else { + for (uint256 i; i < len; i++) { + sum += bids_[i].minAmountOut; + } + if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); + } -// // Get amounts out from implementation-specific auction logic -// amountsOut = _settle(id_, bids_); -// } + // Get amounts out from implementation-specific auction logic + amountsOut = _settle(id_, bids_); + } -// function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); + function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); -// } +} + +abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { + function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + revert Auction_NotImplemented(); + } + + function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + revert Auction_NotImplemented(); + } + + function settle(uint256 id_, ) + +} From ff5319d1961f71af77bd6ae637462dcbb0617f42 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 15:23:27 +0400 Subject: [PATCH 005/117] Use pre-defined transfer functions. Linting. Compile fixes. --- src/AuctionHouse.sol | 81 ++++++------------- src/modules/auctions/bases/BatchAuction.sol | 74 +++++++++++++---- .../Auction/MockAtomicAuctionModule.sol | 13 ++- .../Auction/MockBatchAuctionModule.sol | 11 ++- 4 files changed, 102 insertions(+), 77 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 3e794f66..64dae4ad 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -130,7 +130,9 @@ abstract contract Router is FeeManager { function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); // Off-chain auction variant - function settle(uint256 id_, Settlement memory settlement_) external virtual; + function settle(uint256 id_, LocalSettlement memory settlement_) external virtual; + + function settle(uint256 id_, ExternalSettlement memory settlement_) external virtual; // ========== FEE MANAGEMENT ========== // @@ -192,7 +194,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Update fee balances if non-zero if (toReferrer > 0) rewards[referrer_][quoteToken_] += toReferrer; - if (toProtocol > 0) rewards[PROTOCOL][quoteToken_] += toProtocol; + if (toProtocol > 0) rewards[_PROTOCOL][quoteToken_] += toProtocol; return toReferrer + toProtocol; } @@ -330,7 +332,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO // Validate array lengths all match - uint256 len = settlement_.winningBids.length; + uint256 len = settlement_.bids.length; if ( len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length || len != settlement_.amountsOut.length || len != settlement_.approvals.length @@ -347,32 +349,32 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { if (address(routing.allowlist) != address(0)) { if ( !routing.allowlist.isAllowed( - settlement_.winningBids[i].bidder, settlement_.allowlistProofs[i] + settlement_.bids[i].bidder, settlement_.allowlistProofs[i] ) - ) revert InvalidBidder(settlement_.winningBids[i].bidder); + ) revert InvalidBidder(settlement_.bids[i].bidder); } // Check that the amounts out are at least the minimum specified by the bidder // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder // If so, we need to adjust the minAmountOut proportionally for the slippage check // We also verify that the amountIn is not more than the bidder specified - uint256 minAmountOut = settlement_.winningBids[i].minAmountOut; - if (settlement_.amountsIn[i] > settlement_.winningBids[i].amount) { + uint256 minAmountOut = settlement_.bids[i].minAmountOut; + if (settlement_.amountsIn[i] > settlement_.bids[i].amount) { revert InvalidParams(); - } else if (settlement_.amountsIn[i] < settlement_.winningBids[i].amount) { + } else if (settlement_.amountsIn[i] < settlement_.bids[i].amount) { minAmountOut = - (minAmountOut * settlement_.amountsIn[i]) / settlement_.winningBids[i].amount; // TODO need to think about scaling and rounding here + (minAmountOut * settlement_.amountsIn[i]) / settlement_.bids[i].amount; // TODO need to think about scaling and rounding here } if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); // Calculate fees from bid amount (uint256 toReferrer, uint256 toProtocol) = - calculateFees(settlement_.winningBids[i].referrer, settlement_.amountsIn[i]); + calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; // Update referrer fee balances if non-zero and increment the total protocol fee if (toReferrer > 0) { - rewards[settlement_.winningBids[i].referrer][routing.quoteToken] += toReferrer; + rewards[settlement_.bids[i].referrer][routing.quoteToken] += toReferrer; } totalProtocolFee += toProtocol; @@ -382,7 +384,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Update protocol fee if not zero - if (totalProtocolFee > 0) rewards[PROTOCOL][routing.quoteToken] += totalProtocolFee; + if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; // Send auction inputs to auction module to validate settlement // We do this because the format of the bids and signatures is specific to the auction module @@ -396,63 +398,26 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO update auction module interface and base function to handle these inputs, and perhaps others bytes memory auctionOutput = module.settle( id_, - settlement_.winningBids, + settlement_.bids, settlement_.bidSignatures, amountsInLessFees, settlement_.amountsOut, settlement_.validityProof ); - // Iterate through bids, handling transfers and payouts - // Have to transfer to auction house first since fee is in quote token - // Check balance before and after to ensure full amount received, revert if not - // Handles edge cases like fee-on-transfer tokens (which are not supported) - for (uint256 i; i < len; i++) { - // TODO use permit2 approvals if provided - - uint256 quoteBalance = routing.quoteToken.balanceOf(address(this)); - routing.quoteToken.safeTransferFrom(msg.sender, address(this), settlement_.amountsIn[i]); - if ( - routing.quoteToken.balanceOf(address(this)) - < quoteBalance + settlement_.amountsIn[i] - ) { - revert UnsupportedToken(address(routing.quoteToken)); - } - } - - // If hooks address supplied, transfer tokens from auction house to hooks contract, - // then execute the hook function, and ensure proper amount of tokens transferred in. - if (address(routing.hooks) != address(0)) { - // Send quote token to callback (transferred in first to allow use during callback) - routing.quoteToken.safeTransfer(address(routing.hooks), totalAmountInLessFees); + // Assumes that payment has already been collected for each bid - // Call the callback function to receive payout tokens for payout - uint256 baseBalance = routing.baseToken.balanceOf(address(this)); - routing.hooks.mid(id_, totalAmountInLessFees, totalAmountOut); + // Send payment in bulk to auction owner + _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); - // Check to ensure that the callback sent the requested amount of payout tokens back to the teller - if (routing.baseToken.balanceOf(address(this)) < (baseBalance + totalAmountOut)) { - revert InvalidHook(); - } - } else { - // If no hook is provided, transfer tokens from auction owner to this contract - // for payout. - // Check balance before and after to ensure full amount received, revert if not - // Handles edge cases like fee-on-transfer tokens (which are not supported) - uint256 baseBalance = routing.baseToken.balanceOf(address(this)); - routing.baseToken.safeTransferFrom(routing.owner, address(this), totalAmountOut); - if (routing.baseToken.balanceOf(address(this)) < (baseBalance + totalAmountOut)) { - revert UnsupportedToken(address(routing.baseToken)); - } - - routing.quoteToken.safeTransfer(routing.owner, totalAmountInLessFees); - } + // Collect payout in bulk from the auction owner + _collectPayout(id_, totalAmountInLessFees, totalAmountOut, routing); // Handle payouts to bidders for (uint256 i; i < len; i++) { - // Handle payout to user, including creation of derivative tokens - _handlePayout( - routing, settlement_.winningBids[i].bidder, settlement_.amountsOut[i], auctionOutput + // Send payout to each bidder + _sendPayout( + id_, settlement_.bids[i].bidder, settlement_.amountsOut[i], routing, auctionOutput ); } } diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index f1edffac..10efed98 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -26,20 +26,25 @@ import "src/modules/Auction.sol"; abstract contract BatchAuction { error BatchAuction_NotConcluded(); - - // ========== AUCTION INFORMATION ========== // // TODO add batch auction specific getters } abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { - // ========== STATE VARIABLES ========== // mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external onlyParent { // TODO // Validate inputs @@ -50,7 +55,12 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // Store bid data } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + function settle(uint256 id_) + external + override + onlyParent + returns (uint256[] memory amountsOut) + { // TODO // Validate inputs @@ -59,25 +69,43 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // Store settle data } - function settle(uint256 id_, Auction.Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { + function settle( + uint256 id_, + Auction.Bid[] memory bids_ + ) external onlyParent returns (uint256[] memory amountsOut) { revert Auction_NotImplemented(); } } abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { - // ========== AUCTION EXECUTION ========== // - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external onlyParent { revert Auction_NotImplemented(); } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + function settle(uint256 id_) + external + override + onlyParent + returns (uint256[] memory amountsOut) + { revert Auction_NotImplemented(); } /// @notice Settle a batch auction with the provided bids - function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { + function settle( + uint256 id_, + Bid[] memory bids_ + ) external onlyParent returns (uint256[] memory amountsOut) { Lot storage lot = lotData[id_]; // Must be past the conclusion time to settle @@ -102,19 +130,33 @@ abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { amountsOut = _settle(id_, bids_); } - function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); - + function _settle( + uint256 id_, + Bid[] memory bids_ + ) internal virtual returns (uint256[] memory amountsOut); } abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external onlyParent { revert Auction_NotImplemented(); } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { + function settle(uint256 id_) + external + override + onlyParent + returns (uint256[] memory amountsOut) + { revert Auction_NotImplemented(); } - function settle(uint256 id_, ) - + // function settle(uint256 id_, ) } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index af8f01ba..031df90c 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -53,7 +53,7 @@ contract MockAtomicAuctionModule is AuctionModule { if (payoutData[id_] == 0) { payout = amount_; } else { - payout = payoutData[id_] * amount_ / 1e5; + payout = (payoutData[id_] * amount_) / 1e5; } Output memory output = Output({multiplier: 1}); @@ -78,7 +78,7 @@ contract MockAtomicAuctionModule is AuctionModule { function settle( uint256 id_, Bid[] memory bids_ - ) external virtual override returns (uint256[] memory amountsOut) {} + ) external virtual returns (uint256[] memory amountsOut) {} function payoutFor( uint256 id_, @@ -93,4 +93,13 @@ contract MockAtomicAuctionModule is AuctionModule { function maxPayout(uint256 id_) public view virtual override returns (uint256) {} function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + + function settle( + uint256 id_, + Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] memory amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_ + ) external virtual override returns (bytes memory) {} } diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 4a6f837e..01225eb7 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -47,7 +47,7 @@ contract MockBatchAuctionModule is AuctionModule { function settle( uint256 id_, Bid[] memory bids_ - ) external virtual override returns (uint256[] memory amountsOut) {} + ) external virtual returns (uint256[] memory amountsOut) {} function payoutFor( uint256 id_, @@ -62,4 +62,13 @@ contract MockBatchAuctionModule is AuctionModule { function maxPayout(uint256 id_) public view virtual override returns (uint256) {} function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + + function settle( + uint256 id_, + Bid[] calldata winningBids_, + bytes[] calldata bidSignatures_, + uint256[] memory amountsIn_, + uint256[] calldata amountsOut_, + bytes calldata validityProof_ + ) external virtual override returns (bytes memory) {} } From 6d388ad6eef62bff78e0f0bee63e144eff972920 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 16:17:45 +0400 Subject: [PATCH 006/117] Rename internal function --- src/AuctionHouse.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 64dae4ad..d48038d5 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -190,7 +190,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 amount_ ) internal returns (uint256 totalFees) { // Calculate fees for purchase - (uint256 toReferrer, uint256 toProtocol) = calculateFees(referrer_, amount_); + (uint256 toReferrer, uint256 toProtocol) = _calculateFees(referrer_, amount_); // Update fee balances if non-zero if (toReferrer > 0) rewards[referrer_][quoteToken_] += toReferrer; @@ -199,7 +199,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return toReferrer + toProtocol; } - function calculateFees( + function _calculateFees( address referrer_, uint256 amount_ ) internal view returns (uint256 toReferrer, uint256 toProtocol) { @@ -369,7 +369,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Calculate fees from bid amount (uint256 toReferrer, uint256 toProtocol) = - calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); + _calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; // Update referrer fee balances if non-zero and increment the total protocol fee From 82b2ef57f5e421fc6690719ae20cb07d6cc982c9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 16:18:11 +0400 Subject: [PATCH 007/117] Comment improvements --- src/AuctionHouse.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index d48038d5..4c13480d 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -47,20 +47,18 @@ abstract contract Router is FeeManager { bytes[] allowlistProofs; // optional, allowlist proofs } - // ========== STRUCTS ========== // - /// @notice Parameters used by the purchase function /// @dev This reduces the number of variables in scope for the purchase function /// /// @param recipient Address to receive payout /// @param referrer Address of referrer - /// @param approvalDeadline Deadline for approval signature + /// @param approvalDeadline Deadline for Permit2 approval signature /// @param lotId Lot ID /// @param amount Amount of quoteToken to purchase with (in native decimals) /// @param minAmountOut Minimum amount of baseToken to receive - /// @param approvalNonce Nonce for permit approval signature + /// @param approvalNonce Nonce for permit Permit2 approval signature /// @param auctionData Custom data used by the auction module - /// @param approvalSignature Permit approval signature for the quoteToken + /// @param approvalSignature Permit2 approval signature for the quoteToken /// @param allowlistProof Proof of allowlist inclusion struct PurchaseParams { address recipient; @@ -107,7 +105,7 @@ abstract contract Router is FeeManager { // ========== ATOMIC AUCTIONS ========== // - /// @notice Purchase a lot from an auction + /// @notice Purchase a lot from an atomic auction /// @notice Permit2 is utilised to simplify token transfers /// /// @param params_ Purchase parameters From 3605cdee97fce0d2dda4e788b94a1f9fc593396e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 16:35:14 +0400 Subject: [PATCH 008/117] Extract shared functions. Partial implementation of AuctionHouse.bid() --- src/AuctionHouse.sol | 112 +++++++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 4c13480d..1824b415 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -17,6 +17,7 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; import {Veecode, fromVeecode, WithModules} from "src/modules/Modules.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; +import {IAllowlist} from "src/interfaces/IAllowlist.sol"; // TODO define purpose abstract contract FeeManager { @@ -73,6 +74,32 @@ abstract contract Router is FeeManager { bytes allowlistProof; } + /// @notice Parameters used by the bid function + /// @dev This reduces the number of variables in scope for the bid function + /// + /// @param recipient Address to receive payout + /// @param referrer Address of referrer + /// @param approvalDeadline Deadline for Permit2 approval signature + /// @param lotId Lot ID + /// @param amount Amount of quoteToken to purchase with (in native decimals) + /// @param minAmountOut Minimum amount of baseToken to receive + /// @param approvalNonce Nonce for Permit2 approval signature + /// @param auctionData Custom data used by the auction module + /// @param approvalSignature Permit2 approval signature for the quoteToken + /// @param allowlistProof Proof of allowlist inclusion + struct BidParams { + address recipient; + address referrer; + uint48 approvalDeadline; + uint256 lotId; + uint256 amount; + uint256 minAmountOut; + uint256 approvalNonce; + bytes auctionData; + bytes approvalSignature; + bytes allowlistProof; + } + // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -114,16 +141,16 @@ abstract contract Router is FeeManager { // ========== BATCH AUCTIONS ========== // - // On-chain auction variant - function bid( - address recipient_, - address referrer_, - uint256 id_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external virtual; + /// @notice Bid on a lot in a batch auction + /// @notice The implementing function must perform the following: + /// 1. Validate the bid + /// 2. Store the bid + /// 3. Transfer the amount of quote token from the bidder + /// + /// @param params_ Bid parameters + function bid(BidParams memory params_) external virtual; + + // TODO is a separate bid function needed to support SBA? function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); @@ -132,6 +159,8 @@ abstract contract Router is FeeManager { function settle(uint256 id_, ExternalSettlement memory settlement_) external virtual; + // TODO bid refunds + // ========== FEE MANAGEMENT ========== // function setProtocolFee(uint48 protocolFee_) external { @@ -223,6 +252,27 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } + /// @notice Determines if `caller_` is allowed to purchase/bid on a lot. + /// If no allowlist is defined, this function will return true. + /// + /// @param allowlist_ Allowlist contract + /// @param lotId_ Lot ID + /// @param caller_ Address of caller + /// @param allowlistProof_ Proof of allowlist inclusion + /// @return bool True if caller is allowed to purchase/bid on the lot + function _isAllowed( + IAllowlist allowlist_, + uint256 lotId_, + address caller_, + bytes memory allowlistProof_ + ) internal view returns (bool) { + if (address(allowlist_) == address(0)) { + return true; + } else { + return allowlist_.isAllowed(lotId_, caller_, allowlistProof_); + } + } + // ========== ATOMIC AUCTIONS ========== // /// @inheritdoc Router @@ -253,10 +303,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { Routing memory routing = lotRouting[params_.lotId]; // Check if the purchaser is on the allowlist - if (address(routing.allowlist) != address(0)) { - if (!routing.allowlist.isAllowed(params_.lotId, msg.sender, params_.allowlistProof)) { - revert NotAuthorized(); - } + if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { + revert NotAuthorized(); } uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); @@ -302,16 +350,32 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== BATCH AUCTIONS ========== // - function bid( - address recipient_, - address referrer_, - uint256 id_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external override { - // TODO + /// @inheritdoc Router + function bid(BidParams memory params_) external override isValidLot(params_.lotId) { + // Load routing data for the lot + Routing memory routing = lotRouting[params_.lotId]; + + // Determine if the bidder is authorized to bid + if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { + revert NotAuthorized(); + } + + // Transfer the quote token from the bidder + _collectPayment( + params_.lotId, + params_.amount, + routing.quoteToken, + routing.hooks, + params_.approvalDeadline, + params_.approvalNonce, + params_.approvalSignature + ); + + // Record the bid on the auction module + // The module will determine if the bid is valid - minimum bid size, minimum price, etc + // TODO add additional parameters to the bid function? e.g. bidder, referrer, recipient + AuctionModule module = _getModuleForId(params_.lotId); + module.bid(params_.lotId, params_.amount, params_.minAmountOut, params_.auctionData); } function settle(uint256 id_) external override returns (uint256[] memory amountsOut) { From 9ea9196e6d164a782a9eac02044eff941cf7bc5a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 16:53:09 +0400 Subject: [PATCH 009/117] Reconcile signatures for bid functions --- src/AuctionHouse.sol | 13 ++++++++++--- src/modules/Auction.sol | 5 ++++- src/modules/auctions/bases/BatchAuction.sol | 6 +++--- test/modules/Auction/MockAtomicAuctionModule.sol | 10 +++++++++- test/modules/Auction/MockAuctionModule.sol | 5 ++++- test/modules/Auction/MockBatchAuctionModule.sol | 10 +++++++++- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 1824b415..5a13495d 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -34,7 +34,7 @@ abstract contract Router is FeeManager { struct ExternalSettlement { Auction.Bid[] bids; // user bids submitted externally bytes[] bidSignatures; // user signatures for bids submitted externally - bytes[] approvals; // optional, permit 2 token approvals + bytes[] approvals; // optional, permit 2 token approvals // TODO this requires an approval deadline, nonce and signatures bytes[] allowlistProofs; // optional, allowlist proofs uint256[] amountsIn; // actual amount in for the corresponding bids uint256[] amountsOut; // actual amount out for the corresponding bids @@ -373,9 +373,16 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Record the bid on the auction module // The module will determine if the bid is valid - minimum bid size, minimum price, etc - // TODO add additional parameters to the bid function? e.g. bidder, referrer, recipient AuctionModule module = _getModuleForId(params_.lotId); - module.bid(params_.lotId, params_.amount, params_.minAmountOut, params_.auctionData); + module.bid( + params_.recipient, + params_.referrer, + params_.lotId, + params_.amount, + params_.minAmountOut, + params_.auctionData, + bytes("") + ); } function settle(uint256 id_) external override returns (uint256[] memory amountsOut) { diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a691c7fd..4b2286e9 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -81,10 +81,13 @@ abstract contract Auction { // On-chain auction variant function bid( + address recipient_, + address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, - bytes calldata auctionData_ + bytes calldata auctionData_, + bytes calldata approval_ ) external virtual; function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 10efed98..3cfe8f48 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -44,7 +44,7 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ - ) external onlyParent { + ) external override onlyParent { // TODO // Validate inputs @@ -88,7 +88,7 @@ abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ - ) external onlyParent { + ) external override onlyParent { revert Auction_NotImplemented(); } @@ -145,7 +145,7 @@ abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ - ) external onlyParent { + ) external override onlyParent { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 031df90c..9eae7761 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -69,7 +69,15 @@ contract MockAtomicAuctionModule is AuctionModule { purchaseReverts = reverts_; } - function bid(uint256, uint256, uint256, bytes calldata) external virtual override { + function bid( + address, + address, + uint256, + uint256, + uint256, + bytes calldata, + bytes calldata + ) external virtual override { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 2d846fa8..e91d7df1 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -39,10 +39,13 @@ contract MockAuctionModule is AuctionModule { ) external virtual override returns (uint256 payout, bytes memory auctionOutput) {} function bid( + address recipient_, + address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, - bytes calldata auctionData_ + bytes calldata auctionData_, + bytes calldata approval_ ) external virtual override {} function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 01225eb7..99605fde 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -40,7 +40,15 @@ contract MockBatchAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function bid(uint256, uint256, uint256, bytes calldata) external virtual override {} + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external virtual override {} function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} From 7bc9988909ca30885b8e835ae164460a85870386 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 17:04:59 +0400 Subject: [PATCH 010/117] Add settle function for local storage, external settlement --- src/AuctionHouse.sol | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 5a13495d..fbd066ac 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -48,6 +48,19 @@ abstract contract Router is FeeManager { bytes[] allowlistProofs; // optional, allowlist proofs } + /// @notice Parameters used by the settle function for on-chain bid storage and external settlement + /// + /// @param bidIds IDs of bids to settle + /// @param amountsIn Actual amounts in for the corresponding bids + /// @param amountsOut Actual amounts out for the corresponding bids + /// @param validityProof Optional, provide proof of settlement validity to be verified by module + struct LocalStorageExternalSettlement { + uint256[] bidIds; + uint256[] amountsIn; + uint256[] amountsOut; + bytes validityProof; + } + /// @notice Parameters used by the purchase function /// @dev This reduces the number of variables in scope for the purchase function /// @@ -150,13 +163,19 @@ abstract contract Router is FeeManager { /// @param params_ Bid parameters function bid(BidParams memory params_) external virtual; - // TODO is a separate bid function needed to support SBA? - + // On-chain bid storage, local settlement function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); - // Off-chain auction variant + // On-chain bid storage, external settlement + function settle( + uint256 id_, + LocalStorageExternalSettlement memory settlement_ + ) external virtual; + + // Off-chain bid storage, local settlement function settle(uint256 id_, LocalSettlement memory settlement_) external virtual; + // Off-chain bid storage, external settlement function settle(uint256 id_, ExternalSettlement memory settlement_) external virtual; // TODO bid refunds @@ -389,8 +408,17 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // TODO } + function settle( + uint256 id_, + LocalStorageExternalSettlement memory settlement_ + ) external virtual override { + // TODO + } + // External submission and local evaluation - function settle(uint256 id_, LocalSettlement memory settlement_) external override {} + function settle(uint256 id_, LocalSettlement memory settlement_) external override { + // TODO + } // External submission and evaluation function settle(uint256 id_, ExternalSettlement memory settlement_) external override { From be3c092b8fda45aee95ea52bef3bef312468b6c5 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 17:12:04 +0400 Subject: [PATCH 011/117] Shift to _isAllowed() function --- src/AuctionHouse.sol | 21 +++++++++++---------- test/AuctionHouse/purchase.t.sol | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index fbd066ac..5c05e18a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -204,8 +204,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { error AmountLessThanMinimum(); - error NotAuthorized(); - error UnsupportedToken(address token_); error InvalidHook(); @@ -323,7 +321,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Check if the purchaser is on the allowlist if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { - revert NotAuthorized(); + revert InvalidBidder(msg.sender); } uint256 totalFees = _allocateFees(params_.referrer, routing.quoteToken, params_.amount); @@ -376,7 +374,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Determine if the bidder is authorized to bid if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { - revert NotAuthorized(); + revert InvalidBidder(msg.sender); } // Transfer the quote token from the bidder @@ -443,12 +441,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 totalAmountOut; for (uint256 i; i < len; i++) { // If there is an allowlist, validate that the winners are on the allowlist - if (address(routing.allowlist) != address(0)) { - if ( - !routing.allowlist.isAllowed( - settlement_.bids[i].bidder, settlement_.allowlistProofs[i] - ) - ) revert InvalidBidder(settlement_.bids[i].bidder); + if ( + !_isAllowed( + routing.allowlist, + id_, + settlement_.bids[i].bidder, + settlement_.allowlistProofs[i] + ) + ) { + revert InvalidBidder(settlement_.bids[i].bidder); } // Check that the amounts out are at least the minimum specified by the bidder diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 1f412238..4a82548e 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -355,7 +355,7 @@ contract PurchaseTest is Test, Permit2User { function test_givenCallerNotOnAllowlist() external givenAuctionHasAllowlist { // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.NotAuthorized.selector); + bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidBidder.selector, alice); vm.expectRevert(err); // Purchase From b9f34791697ca7854fb39e098579a2d53de00e87 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 18 Jan 2024 17:17:41 +0400 Subject: [PATCH 012/117] Shift fee implementation --- src/AuctionHouse.sol | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 5c05e18a..334f95d3 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -113,6 +113,8 @@ abstract contract Router is FeeManager { bytes allowlistProof; } + // TODO consolidate PurchaseParams and BidParams if there are no further changes + // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -182,15 +184,11 @@ abstract contract Router is FeeManager { // ========== FEE MANAGEMENT ========== // - function setProtocolFee(uint48 protocolFee_) external { - // TOOD make this permissioned - protocolFee = protocolFee_; - } + /// @notice Sets the fee for the protocol + function setProtocolFee(uint48 protocolFee_) external virtual; - function setReferrerFee(address referrer_, uint48 referrerFee_) external { - // TOOD make this permissioned - referrerFees[referrer_] = referrerFee_; - } + /// @notice Sets the fee for a referrer + function setReferrerFee(address referrer_, uint48 referrerFee_) external virtual; } /// @title AuctionHouse @@ -794,4 +792,16 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { revert UnsupportedToken(address(token_)); } } + + // ========== FEE MANAGEMENT ========== // + + /// @inheritdoc Router + function setProtocolFee(uint48 protocolFee_) external override onlyOwner { + protocolFee = protocolFee_; + } + + /// @inheritdoc Router + function setReferrerFee(address referrer_, uint48 referrerFee_) external override onlyOwner { + referrerFees[referrer_] = referrerFee_; + } } From 824eb90df98d811783f0bae7142e256e36bf4039 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 19 Jan 2024 15:10:36 +0400 Subject: [PATCH 013/117] Adjust bid/settle signatures. Remove unused functions. --- src/AuctionHouse.sol | 394 ++++++++++-------- src/modules/Auction.sol | 35 +- src/modules/auctions/bases/BatchAuction.sol | 180 ++++---- .../Auction/MockAtomicAuctionModule.sol | 15 +- test/modules/Auction/MockAuctionModule.sol | 21 +- .../Auction/MockBatchAuctionModule.sol | 15 +- 6 files changed, 341 insertions(+), 319 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 334f95d3..bcd0b4f4 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -31,34 +31,11 @@ abstract contract FeeManager { abstract contract Router is FeeManager { // ========== DATA STRUCTURES ========== // - struct ExternalSettlement { - Auction.Bid[] bids; // user bids submitted externally - bytes[] bidSignatures; // user signatures for bids submitted externally - bytes[] approvals; // optional, permit 2 token approvals // TODO this requires an approval deadline, nonce and signatures - bytes[] allowlistProofs; // optional, allowlist proofs - uint256[] amountsIn; // actual amount in for the corresponding bids - uint256[] amountsOut; // actual amount out for the corresponding bids - bytes validityProof; // optional, provide proof of settlement validity to be verified by module - } - - struct LocalSettlement { - Auction.Bid[] bids; // user bids submitted externally - bytes[] bidSignatures; // user signatures for bids submitted externally - bytes[] approvals; // optional, permit 2 token approvals - bytes[] allowlistProofs; // optional, allowlist proofs - } - - /// @notice Parameters used by the settle function for on-chain bid storage and external settlement - /// - /// @param bidIds IDs of bids to settle - /// @param amountsIn Actual amounts in for the corresponding bids - /// @param amountsOut Actual amounts out for the corresponding bids - /// @param validityProof Optional, provide proof of settlement validity to be verified by module - struct LocalStorageExternalSettlement { - uint256[] bidIds; - uint256[] amountsIn; - uint256[] amountsOut; - bytes validityProof; + /// @notice Parameters used for Permit2 approvals + struct Permit2Approval { + uint48 deadline; + uint256 nonce; + bytes signature; } /// @notice Parameters used by the purchase function @@ -87,34 +64,6 @@ abstract contract Router is FeeManager { bytes allowlistProof; } - /// @notice Parameters used by the bid function - /// @dev This reduces the number of variables in scope for the bid function - /// - /// @param recipient Address to receive payout - /// @param referrer Address of referrer - /// @param approvalDeadline Deadline for Permit2 approval signature - /// @param lotId Lot ID - /// @param amount Amount of quoteToken to purchase with (in native decimals) - /// @param minAmountOut Minimum amount of baseToken to receive - /// @param approvalNonce Nonce for Permit2 approval signature - /// @param auctionData Custom data used by the auction module - /// @param approvalSignature Permit2 approval signature for the quoteToken - /// @param allowlistProof Proof of allowlist inclusion - struct BidParams { - address recipient; - address referrer; - uint48 approvalDeadline; - uint256 lotId; - uint256 amount; - uint256 minAmountOut; - uint256 approvalNonce; - bytes auctionData; - bytes approvalSignature; - bytes allowlistProof; - } - - // TODO consolidate PurchaseParams and BidParams if there are no further changes - // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -162,24 +111,45 @@ abstract contract Router is FeeManager { /// 2. Store the bid /// 3. Transfer the amount of quote token from the bidder /// - /// @param params_ Bid parameters - function bid(BidParams memory params_) external virtual; - - // On-chain bid storage, local settlement - function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); + /// @param lotId_ Lot ID + /// @param recipient_ Address to receive payout + /// @param referrer_ Address of referrer + /// @param amount_ Amount of quoteToken to purchase with (in native decimals) + /// @param auctionData_ Custom data used by the auction module + /// @param allowlistProof_ Allowlist proof + /// @param permit2Data_ Permit2 data + function bid( + uint96 lotId_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_, + bytes calldata allowlistProof_, + bytes calldata permit2Data_ + ) external virtual; - // On-chain bid storage, external settlement + /// @notice Settle a batch auction with the provided bids + /// @notice This function is used for on-chain storage of bids and external settlement + /// + /// @notice The implementing function must perform the following: + /// 1. Validate that the caller is authorized to settle the auction + /// 2. Calculate fees + /// 3. Pass the bids to the auction module to validate the settlement + /// 4. Send payment to the auction owner + /// 5. Collect payout from the auction owner + /// 6. Send payout to each bidder + /// + /// @param lotId_ Lot ID + /// @param winningBids_ Winning bids + /// @param settlementProof_ Proof of settlement validity + /// @param settlementData_ Settlement data function settle( - uint256 id_, - LocalStorageExternalSettlement memory settlement_ + uint96 lotId_, + Auction.Bid[] calldata winningBids_, + bytes calldata settlementProof_, + bytes calldata settlementData_ ) external virtual; - // Off-chain bid storage, local settlement - function settle(uint256 id_, LocalSettlement memory settlement_) external virtual; - - // Off-chain bid storage, external settlement - function settle(uint256 id_, ExternalSettlement memory settlement_) external virtual; - // TODO bid refunds // ========== FEE MANAGEMENT ========== // @@ -366,122 +336,61 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== BATCH AUCTIONS ========== // /// @inheritdoc Router - function bid(BidParams memory params_) external override isValidLot(params_.lotId) { + function bid( + uint96 lotId_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_, + bytes calldata allowlistProof_, + bytes calldata permit2Data_ + ) external override isValidLot(lotId_) { // Load routing data for the lot - Routing memory routing = lotRouting[params_.lotId]; + Routing memory routing = lotRouting[lotId_]; // Determine if the bidder is authorized to bid - if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { + if (!_isAllowed(routing.allowlist, lotId_, msg.sender, allowlistProof_)) { revert InvalidBidder(msg.sender); } // Transfer the quote token from the bidder + Permit2Approval memory permit2Approval = abi.decode(permit2Data_, (Permit2Approval)); _collectPayment( - params_.lotId, - params_.amount, + lotId_, + amount_, routing.quoteToken, routing.hooks, - params_.approvalDeadline, - params_.approvalNonce, - params_.approvalSignature + permit2Approval.deadline, + permit2Approval.nonce, + permit2Approval.signature ); // Record the bid on the auction module // The module will determine if the bid is valid - minimum bid size, minimum price, etc - AuctionModule module = _getModuleForId(params_.lotId); + AuctionModule module = _getModuleForId(lotId_); module.bid( - params_.recipient, - params_.referrer, - params_.lotId, - params_.amount, - params_.minAmountOut, - params_.auctionData, - bytes("") + lotId_, + recipient_, + referrer_, + amount_, + auctionData_, + bytes("") // TODO approval param ); } - function settle(uint256 id_) external override returns (uint256[] memory amountsOut) { - // TODO - } - + /// @inheritdoc Router function settle( - uint256 id_, - LocalStorageExternalSettlement memory settlement_ - ) external virtual override { - // TODO - } - - // External submission and local evaluation - function settle(uint256 id_, LocalSettlement memory settlement_) external override { - // TODO - } - - // External submission and evaluation - function settle(uint256 id_, ExternalSettlement memory settlement_) external override { + uint96 lotId_, + Auction.Bid[] calldata winningBids_, + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external override isValidLot(lotId_) { // Load routing data for the lot - Routing memory routing = lotRouting[id_]; + Routing memory routing = lotRouting[lotId_]; // Validate that sender is authorized to settle the auction // TODO - // Validate array lengths all match - uint256 len = settlement_.bids.length; - if ( - len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length - || len != settlement_.amountsOut.length || len != settlement_.approvals.length - || len != settlement_.allowlistProofs.length - ) revert InvalidParams(); - - // Bid-level validation and fee calculations - uint256[] memory amountsInLessFees = new uint256[](len); - uint256 totalProtocolFee; - uint256 totalAmountInLessFees; - uint256 totalAmountOut; - for (uint256 i; i < len; i++) { - // If there is an allowlist, validate that the winners are on the allowlist - if ( - !_isAllowed( - routing.allowlist, - id_, - settlement_.bids[i].bidder, - settlement_.allowlistProofs[i] - ) - ) { - revert InvalidBidder(settlement_.bids[i].bidder); - } - - // Check that the amounts out are at least the minimum specified by the bidder - // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder - // If so, we need to adjust the minAmountOut proportionally for the slippage check - // We also verify that the amountIn is not more than the bidder specified - uint256 minAmountOut = settlement_.bids[i].minAmountOut; - if (settlement_.amountsIn[i] > settlement_.bids[i].amount) { - revert InvalidParams(); - } else if (settlement_.amountsIn[i] < settlement_.bids[i].amount) { - minAmountOut = - (minAmountOut * settlement_.amountsIn[i]) / settlement_.bids[i].amount; // TODO need to think about scaling and rounding here - } - if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); - - // Calculate fees from bid amount - (uint256 toReferrer, uint256 toProtocol) = - _calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); - amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; - - // Update referrer fee balances if non-zero and increment the total protocol fee - if (toReferrer > 0) { - rewards[settlement_.bids[i].referrer][routing.quoteToken] += toReferrer; - } - totalProtocolFee += toProtocol; - - // Increment total amount out - totalAmountInLessFees += amountsInLessFees[i]; - totalAmountOut += settlement_.amountsOut[i]; - } - - // Update protocol fee if not zero - if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; - // Send auction inputs to auction module to validate settlement // We do this because the format of the bids and signatures is specific to the auction module // Some common things to check: @@ -489,17 +398,41 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // 2. Minimum price is enforced // 3. Minimum bid size is enforced // 4. Minimum capacity sold is enforced - AuctionModule module = _getModuleForId(id_); - - // TODO update auction module interface and base function to handle these inputs, and perhaps others - bytes memory auctionOutput = module.settle( - id_, - settlement_.bids, - settlement_.bidSignatures, - amountsInLessFees, - settlement_.amountsOut, - settlement_.validityProof - ); + uint256[] memory amountsOut; + bytes memory auctionOutput; + { + AuctionModule module = _getModuleForId(lotId_); + (amountsOut, auctionOutput) = + module.settle(lotId_, winningBids_, settlementProof_, settlementData_); + } + + // Calculate fees + uint256 totalAmountInLessFees; + uint256 totalAmountOut; + { + uint256 bidCount = winningBids_.length; + uint256 totalProtocolFee; + for (uint256 i; i < bidCount; i++) { + // No need to check if the bid amount is greater than the amount out because it is checked in `bid()` + + // Calculate fees from bid amount + (uint256 toReferrer, uint256 toProtocol) = + _calculateFees(winningBids_[i].referrer, winningBids_[i].amount); + + // Update referrer fee balances if non-zero and increment the total protocol fee + if (toReferrer > 0) { + rewards[winningBids_[i].referrer][routing.quoteToken] += toReferrer; + } + totalProtocolFee += toProtocol; + + // Increment total amount out + totalAmountInLessFees += winningBids_[i].amount - toReferrer - toProtocol; + totalAmountOut += amountsOut[i]; + } + + // Update protocol fee if not zero + if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; + } // Assumes that payment has already been collected for each bid @@ -507,17 +440,120 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); // Collect payout in bulk from the auction owner - _collectPayout(id_, totalAmountInLessFees, totalAmountOut, routing); + _collectPayout(lotId_, totalAmountInLessFees, totalAmountOut, routing); // Handle payouts to bidders - for (uint256 i; i < len; i++) { - // Send payout to each bidder - _sendPayout( - id_, settlement_.bids[i].bidder, settlement_.amountsOut[i], routing, auctionOutput - ); + { + uint256 bidCount = winningBids_.length; + for (uint256 i; i < bidCount; i++) { + // Send payout to each bidder + _sendPayout(lotId_, winningBids_[i].bidder, amountsOut[i], routing, auctionOutput); + } } } + // // External submission and evaluation + // function settle(uint256 id_, ExternalSettlement memory settlement_) external override { + // // Load routing data for the lot + // Routing memory routing = lotRouting[id_]; + + // // Validate that sender is authorized to settle the auction + // // TODO + + // // Validate array lengths all match + // uint256 len = settlement_.bids.length; + // if ( + // len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length + // || len != settlement_.amountsOut.length || len != settlement_.approvals.length + // || len != settlement_.allowlistProofs.length + // ) revert InvalidParams(); + + // // Bid-level validation and fee calculations + // uint256[] memory amountsInLessFees = new uint256[](len); + // uint256 totalProtocolFee; + // uint256 totalAmountInLessFees; + // uint256 totalAmountOut; + // for (uint256 i; i < len; i++) { + // // If there is an allowlist, validate that the winners are on the allowlist + // if ( + // !_isAllowed( + // routing.allowlist, + // id_, + // settlement_.bids[i].bidder, + // settlement_.allowlistProofs[i] + // ) + // ) { + // revert InvalidBidder(settlement_.bids[i].bidder); + // } + + // // Check that the amounts out are at least the minimum specified by the bidder + // // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder + // // If so, we need to adjust the minAmountOut proportionally for the slippage check + // // We also verify that the amountIn is not more than the bidder specified + // uint256 minAmountOut = settlement_.bids[i].minAmountOut; + // if (settlement_.amountsIn[i] > settlement_.bids[i].amount) { + // revert InvalidParams(); + // } else if (settlement_.amountsIn[i] < settlement_.bids[i].amount) { + // minAmountOut = + // (minAmountOut * settlement_.amountsIn[i]) / settlement_.bids[i].amount; // TODO need to think about scaling and rounding here + // } + // if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); + + // // Calculate fees from bid amount + // (uint256 toReferrer, uint256 toProtocol) = + // _calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); + // amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; + + // // Update referrer fee balances if non-zero and increment the total protocol fee + // if (toReferrer > 0) { + // rewards[settlement_.bids[i].referrer][routing.quoteToken] += toReferrer; + // } + // totalProtocolFee += toProtocol; + + // // Increment total amount out + // totalAmountInLessFees += amountsInLessFees[i]; + // totalAmountOut += settlement_.amountsOut[i]; + // } + + // // Update protocol fee if not zero + // if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; + + // // Send auction inputs to auction module to validate settlement + // // We do this because the format of the bids and signatures is specific to the auction module + // // Some common things to check: + // // 1. Total of amounts out is not greater than capacity + // // 2. Minimum price is enforced + // // 3. Minimum bid size is enforced + // // 4. Minimum capacity sold is enforced + // AuctionModule module = _getModuleForId(id_); + + // // TODO update auction module interface and base function to handle these inputs, and perhaps others + // bytes memory auctionOutput = module.settle( + // id_, + // settlement_.bids, + // settlement_.bidSignatures, + // amountsInLessFees, + // settlement_.amountsOut, + // settlement_.validityProof + // ); + + // // Assumes that payment has already been collected for each bid + + // // Send payment in bulk to auction owner + // _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); + + // // Collect payout in bulk from the auction owner + // _collectPayout(id_, totalAmountInLessFees, totalAmountOut, routing); + + // // Handle payouts to bidders + // for (uint256 i; i < len; i++) { + // // Send payout to each bidder + // _sendPayout( + // id_, settlement_.bids[i].bidder, settlement_.amountsOut[i], routing, auctionOutput + // ); + // } + // } + // ========== TOKEN TRANSFERS ========== // /// @notice Collects payment of the quote token from the user diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 4b2286e9..cc259f71 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -79,29 +79,38 @@ abstract contract Auction { // ========== BATCH AUCTIONS ========== // - // On-chain auction variant + /// @notice Bid on an auction lot + /// + /// @param lotId_ The lot id + /// @param recipient_ The recipient of the purchased tokens + /// @param referrer_ The referrer of the bid + /// @param amount_ The amount of quote tokens to bid + /// @param auctionData_ The auction-specific data + /// @param approval_ The user approval data function bid( + uint96 lotId_, address recipient_, address referrer_, - uint256 id_, uint256 amount_, - uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ ) external virtual; - function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); - - // Off-chain auction variant - // TODO use solady data packing library to make bids smaller on the actual module to store? + /// @notice Settle a batch auction with the provided bids + /// @notice This function is used for on-chain storage of bids and external settlement + /// + /// @param lotId_ Lot id + /// @param winningBids_ Winning bids + /// @param settlementProof_ Proof of settlement validity + /// @param settlementData_ Settlement data + /// @return amountsOut Amount out for each bid + /// @return auctionOutput Auction-specific output function settle( - uint256 id_, + uint96 lotId_, Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] memory amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_ - ) external virtual returns (bytes memory); + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external virtual returns (uint256[] memory amountsOut, bytes memory auctionOutput); // ========== AUCTION MANAGEMENT ========== // diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 3cfe8f48..19a4259d 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -36,12 +36,12 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; + /// @inheritdoc Auction function bid( + uint96 lotId_, address recipient_, address referrer_, - uint256 id_, uint256 amount_, - uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ ) external override onlyParent { @@ -55,11 +55,17 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // Store bid data } - function settle(uint256 id_) + /// @inheritdoc Auction + function settle( + uint96 lotId, + Auction.Bid[] memory winningBids_, + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external override onlyParent - returns (uint256[] memory amountsOut) + returns (uint256[] memory amountsOut, bytes memory auctionOutput) { // TODO // Validate inputs @@ -68,95 +74,81 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // Store settle data } - - function settle( - uint256 id_, - Auction.Bid[] memory bids_ - ) external onlyParent returns (uint256[] memory amountsOut) { - revert Auction_NotImplemented(); - } } -abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { - // ========== AUCTION EXECUTION ========== // - - function bid( - address recipient_, - address referrer_, - uint256 id_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external override onlyParent { - revert Auction_NotImplemented(); - } - - function settle(uint256 id_) - external - override - onlyParent - returns (uint256[] memory amountsOut) - { - revert Auction_NotImplemented(); - } - - /// @notice Settle a batch auction with the provided bids - function settle( - uint256 id_, - Bid[] memory bids_ - ) external onlyParent returns (uint256[] memory amountsOut) { - Lot storage lot = lotData[id_]; - - // Must be past the conclusion time to settle - if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); - - // Bids must not be greater than the capacity - uint256 len = bids_.length; - uint256 sum; - if (lot.capacityInQuote) { - for (uint256 i; i < len; i++) { - sum += bids_[i].amount; - } - if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); - } else { - for (uint256 i; i < len; i++) { - sum += bids_[i].minAmountOut; - } - if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); - } - - // Get amounts out from implementation-specific auction logic - amountsOut = _settle(id_, bids_); - } - - function _settle( - uint256 id_, - Bid[] memory bids_ - ) internal virtual returns (uint256[] memory amountsOut); -} - -abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { - function bid( - address recipient_, - address referrer_, - uint256 id_, - uint256 amount_, - uint256 minAmountOut_, - bytes calldata auctionData_, - bytes calldata approval_ - ) external override onlyParent { - revert Auction_NotImplemented(); - } - - function settle(uint256 id_) - external - override - onlyParent - returns (uint256[] memory amountsOut) - { - revert Auction_NotImplemented(); - } - - // function settle(uint256 id_, ) -} +// abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { +// // ========== AUCTION EXECUTION ========== // + +// function bid( +// uint96 lotId_, +// address recipient_, +// address referrer_, +// uint256 amount_, +// bytes calldata auctionData_, +// bytes calldata approval_ +// ) external override onlyParent { +// revert Auction_NotImplemented(); +// } + +// /// @notice Settle a batch auction with the provided bids +// function settle( +// uint256 id_, +// Bid[] memory bids_ +// ) external onlyParent returns (uint256[] memory amountsOut) { +// Lot storage lot = lotData[id_]; + +// // Must be past the conclusion time to settle +// if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); + +// // Bids must not be greater than the capacity +// uint256 len = bids_.length; +// uint256 sum; +// if (lot.capacityInQuote) { +// for (uint256 i; i < len; i++) { +// sum += bids_[i].amount; +// } +// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); +// } else { +// for (uint256 i; i < len; i++) { +// sum += bids_[i].minAmountOut; +// } +// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); +// } + +// // Get amounts out from implementation-specific auction logic +// amountsOut = _settle(id_, bids_); +// } + +// function _settle( +// uint256 id_, +// Bid[] memory bids_ +// ) internal virtual returns (uint256[] memory amountsOut); +// } + +// abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { +// function bid( +// uint96 lotId_, +// address recipient_, +// address referrer_, +// uint256 amount_, +// bytes calldata auctionData_, +// bytes calldata approval_ +// ) external override onlyParent { +// revert Auction_NotImplemented(); +// } + +// /// @inheritdoc Auction +// function settle( +// uint96 lotId, +// Auction.Bid[] memory winningBids_, +// bytes calldata settlementProof_, +// bytes calldata settlementData_ +// ) +// external +// override +// onlyParent +// returns (uint256[] memory amountsOut) +// { +// revert Auction_NotImplemented(); +// } +// } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 9eae7761..c5251fed 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -70,19 +70,16 @@ contract MockAtomicAuctionModule is AuctionModule { } function bid( + uint96, address, address, uint256, - uint256, - uint256, bytes calldata, bytes calldata ) external virtual override { revert Auction_NotImplemented(); } - function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} - function settle( uint256 id_, Bid[] memory bids_ @@ -103,11 +100,9 @@ contract MockAtomicAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} function settle( - uint256 id_, + uint96 lotId_, Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] memory amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_ - ) external virtual override returns (bytes memory) {} + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index e91d7df1..3b6e837f 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -39,26 +39,14 @@ contract MockAuctionModule is AuctionModule { ) external virtual override returns (uint256 payout, bytes memory auctionOutput) {} function bid( + uint96 id_, address recipient_, address referrer_, - uint256 id_, uint256 amount_, - uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ ) external virtual override {} - function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} - - function settle( - uint256 id_, - Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] memory amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_ - ) external virtual override returns (bytes memory) {} - function payoutFor( uint256 id_, uint256 amount_ @@ -72,6 +60,13 @@ contract MockAuctionModule is AuctionModule { function maxPayout(uint256 id_) public view virtual override returns (uint256) {} function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + + function settle( + uint96 lotId_, + Bid[] calldata winningBids_, + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 99605fde..a083ef91 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -41,17 +41,14 @@ contract MockBatchAuctionModule is AuctionModule { } function bid( + uint96 id_, address recipient_, address referrer_, - uint256 id_, uint256 amount_, - uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_ ) external virtual override {} - function settle(uint256 id_) external virtual override returns (uint256[] memory amountsOut) {} - function settle( uint256 id_, Bid[] memory bids_ @@ -72,11 +69,9 @@ contract MockBatchAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} function settle( - uint256 id_, + uint96 lotId_, Bid[] calldata winningBids_, - bytes[] calldata bidSignatures_, - uint256[] memory amountsIn_, - uint256[] calldata amountsOut_, - bytes calldata validityProof_ - ) external virtual override returns (bytes memory) {} + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} } From c7b701cbc4f8d3ae54b83a528837bd4c744fcbc4 Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 19 Jan 2024 16:58:01 -0600 Subject: [PATCH 014/117] wip: initial LSBBA implementation (not complete) --- src/lib/RSA.sol | 269 ++++++++++++++++++++ src/modules/Auction.sol | 1 - src/modules/auctions/LSBBA.sol | 209 +++++++++++++++ src/modules/auctions/SBB.sol | 4 - src/modules/auctions/bases/BatchAuction.sol | 4 +- 5 files changed, 481 insertions(+), 6 deletions(-) create mode 100644 src/lib/RSA.sol create mode 100644 src/modules/auctions/LSBBA.sol delete mode 100644 src/modules/auctions/SBB.sol diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol new file mode 100644 index 00000000..754fb4bb --- /dev/null +++ b/src/lib/RSA.sol @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +/// @title RSA-OAEP Encryption Library +/// @notice Library that implements RSA encryption and decryption using EME-OAEP encoding scheme as defined in PKCS#1: RSA Cryptography Specification Version 2.2 +/// @author Oighty +// TODO Need to add tests for this library +library RSAOAEP { + function modexp(bytes memory base, bytes memory exponent, bytes memory modulus) + public + view + returns (bytes memory) + { + (bool success, bytes memory output) = address(0x05).staticcall( + abi.encodePacked(base.length, exponent.length, modulus.length, base, exponent, modulus) + ); + + if (!success) revert("modexp failed."); + + return output; + } + + function decrypt(bytes memory cipherText, bytes memory d, bytes memory n, bytes memory label) + internal + view + returns (bytes memory) + { + // Implements 7.1.2 RSAES-OAEP-DECRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 + // Error messages are intentionally vague to prevent oracle attacks + + // 1. Input length validation + // 1. a. If the length of L is greater than the input limitation + // for the hash function, output "decryption error" and stop. + // SHA2-256 has a limit of (2^64 - 1) / 8 bytes, which is far more than can be held in memory in the EVM. + + // 1. b. If the length of the ciphertext is not the length of the private key, + // output "decryption error" and stop. + // 1. c. Private key must be greater than twice the length of the hash function output plus 2 bytes + uint256 cLen = cipherText.length; + { + uint256 dLen = d.length; + if (cLen != dLen || dLen < 66) revert("decryption error"); // 66 = 2*32 + 2 where 32 is the length of the output of the sha256 hash function + } + + // 2. RSA decryption + // 2. a. Convert ciphertext to integer (can skip since modexp does this for us) + // 2. b. Apply modexp decryption using the private key and modulus from the public key + // 2. c. Convert result from integer to bytes (can skip since modexp does this for us) + bytes memory encoded = modexp(cipherText, d, n); + // Require that the encoded length be the same as the ciphertext length + if (cLen != encoded.length) revert("decryption error"); + + // 3. EME-OAEP decoding + // 3. a. Calculate the hash of the provided label + bytes32 lhash = sha256(label); + + // 3. b. Separate encoded message into Y (1 byte) | maskedSeed (32 bytes) | maskedDB (cLen - 32 - 1) + bytes1 y = bytes1(encoded); + bytes32 maskedSeed; + uint256 words = (cLen - 33) / 32 + ((cLen - 33) % 32 == 0 ? 0 : 1); + bytes memory maskedDb = new bytes(cLen - 33); + + assembly { + // Load a word from the encoded string starting at the 2nd byte (also have to account for length stored in first slot) + maskedSeed := mload(add(encoded, 0x21)) + + // Store the remaining bytes into the maskedDb + for { let i := 0 } lt(i, words) { i := add(i, 1) } { + mstore(add(add(maskedDb, 0x20), mul(i, 0x20)), mload(add(add(encoded, 0x41), mul(i, 0x20)))) + } + } + + // 3. c. Calculate seed mask + // 3. d. Calculate seed + bytes32 seed; + { + bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); + seed = maskedSeed ^ seedMask; + } + + // 3. e. Calculate DB mask + bytes memory dbMask = _mgf(abi.encodePacked(seed), cLen - 33); + + // 3. f. Calculate DB + bytes memory db = _xor(maskedDb, dbMask); + uint256 dbWords = db.length / 32 + db.length % 32 == 0 ? 0 : 1; + + // 3. g. Separate DB into an octet string lHash' of length hLen, a + // (possibly empty) padding string PS consisting of octets + // with hexadecimal value 0x00, and a message M as + + // DB = lHash' || PS || 0x01 || M. + + // If there is no octet with hexadecimal value 0x01 to + // separate PS from M, if lHash does not equal lHash', or if + // Y is nonzero, output "decryption error" and stop. + bytes32 recoveredHash = bytes32(db); + bytes1 one; + bytes memory message; + assembly { + // Iterate over bytes after the label hash until hitting a non-zero byte + // Skip the first word since it is the recovered hash + // Identify the start index of the message within the db byte string + let m := 0 + for { let w := 1 } lt(w, dbWords) { w := add(w, 1) } { + let word := mload(add(db, add(0x20, mul(w, 0x20)))) + // Iterate over bytes in the word + for { let i := 0 } lt(i, 0x20) { i := 0x20 } { + switch byte(i, word) + case 0x00 { continue } + case 0x01 { + one := 0x01 + m := add(add(i, 1), mul(sub(w, 1), 0x20)) + break + } + default { + // Non-zero entry found before 0x01, revert + let p := mload(0x40) + mstore(p, "decryption error") + revert(p, 0x10) + } + } + + // If the 0x01 byte has been found, exit the outer loop + switch one + case 0x01 { break } + } + + // Check that m is not zero, otherwise revert + switch m + case 0x00 { + let p := mload(0x40) + mstore(p, "decryption error") + revert(p, 0x10) + } + + // Copy the message from the db bytes string + let len := sub(mload(db), m) + let wrds := div(len, 0x20) + switch mod(len, 0x20) + case 0x00 {} + default { wrds := add(wrds, 1) } + for { let w := 0 } lt(w, wrds) { w := add(w, 1) } { + let c := mload(add(db, add(m, mul(w, 0x20)))) + let i := add(message, mul(w, 0x20)) + mstore(i, c) + } + } + + if (one != 0x01 || lhash != recoveredHash || y != 0x00) revert("decryption error"); + + // 4. Return the message + return message; + } + + function encrypt(bytes memory message, bytes memory label, bytes memory e, bytes memory n, uint256 seed) + internal + view + returns (bytes memory) + { + // Implements 7.1.1. RSAES-OAEP-ENCRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 + + // 1. a. Check that the label length is less than the max for sha256 + // This check is probably not necessary given that the EVM cannot store this much data in memory. + if (label.length > type(uint64).max / 8) revert("label too long"); + + // 1. b. Check length of message against OAEP equation + uint256 mLen = message.length; + uint256 nLen = n.length; + if (mLen > nLen - 66) revert("message too long"); // 66 = 2 * 32 - 2 where 32 is the output size of the hash function in bytes + + // 2. a. Hash the label + bytes32 labelHash = sha256(label); + + // 2. b. Generate padding string + bytes memory padding = new bytes(nLen - 66 - mLen); + + // 2. c. Concatenate inputs into data block for encoding + // DB = labelHash | padding | 0x01 | message + bytes memory db = abi.encodePacked(labelHash, padding, bytes1(0x01), message); + + // 2. d. Generate random byte string the same length as the hash function + bytes32 rand = sha256(abi.encodePacked(seed)); + + // 2. e. Let dbMask = MGF(seed, k - hLen - 1). + bytes memory dbMask = _mgf(abi.encodePacked(rand), nLen - 33); + + // 2. f. Let maskedDB = DB \xor dbMask. + bytes memory maskedDb = _xor(db, dbMask); + + // 2. g. Let seedMask = MGF(maskedDB, hLen). + bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); + + // 2. h. Let maskedSeed = seed \xor seedMask. + bytes32 maskedSeed = rand ^ seedMask; + + // 2. i. Concatenate a single octet with hexadecimal value 0x00, + // maskedSeed, and maskedDB to form an encoded message EM of + // length k octets as + // EM = 0x00 || maskedSeed || maskedDB. + bytes memory encoded = abi.encodePacked(bytes1(0x00), maskedSeed, maskedDb); + + // 3. RSA encryption: + // a. Convert the encoded message EM to an integer message + // representative m (see Section 4.2): + // m = OS2IP (EM). + // b. Apply the RSAEP encryption primitive (Section 5.1.1) to + // the RSA public key (n, e) and the message representative m + // to produce an integer ciphertext representative c: + // c = RSAEP ((n, e), m). + // c. Convert the ciphertext representative c to a ciphertext C + // of length k octets (see Section 4.1): + // C = I2OSP (c, k). + // 4. Output the ciphertext C. + return modexp(encoded, e, n); + } + + function _mgf(bytes memory seed, uint256 maskLen) internal pure returns (bytes memory) { + // Implements 8.2.1 MGF1 as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 + + // 1. Check that the mask length is not greater than 2^32 * hash length (32 bytes in this case) + if (maskLen > 2 ** 32 * 32) revert("mask too long"); + + // 2. Let T be the empty octet string + // Need to initialize to the maskLen here since we cannot resize + bytes memory t = new bytes(maskLen); + + // 3. For counter from 0 to \ceil (maskLen / hLen) - 1, do the + // following: + // A. Convert counter to an octet string C of length 4 octets (see + // Section 4.1): + // C = I2OSP (counter, 4) . + // B. Concatenate the hash of the seed mgfSeed and C to the octet + // string T: + // T = T || Hash(mgfSeed || C) . + + uint256 count = maskLen / 32 + (maskLen % 32 == 0 ? 0 : 1); + for (uint256 c; c < count; c++) { + bytes32 h = sha256(abi.encodePacked(seed, c)); + assembly { + let p := add(add(t, 0x20), mul(c, 0x20)) + mstore(p, h) + } + } + + // 4. Output the leading maskLen octets of T as the octet string mask. + return t; + } + + function _xor(bytes memory first, bytes memory second) internal pure returns (bytes memory) { + uint256 fLen = first.length; + uint256 sLen = second.length; + if (fLen != sLen) revert("xor: different lengths"); + + uint256 words = (fLen / 32) + (fLen % 32 == 0 ? 0 : 1); + bytes memory result = new bytes(fLen); + + // Iterate through words in the byte strings and xor them one at a time, storing the result + assembly { + for { let i := 0 } lt(i, words) { i := add(i, 1) } { + let f := mload(add(first, mul(i, 0x20))) + let s := mload(add(second, mul(i, 0x20))) + mstore(add(add(result, 0x20), mul(i, 0x20)), xor(f, s)) + } + } + + return result; + } +} diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index cc259f71..45654d26 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -41,7 +41,6 @@ abstract contract Auction { // TODO pack if we anticipate on-chain auction variants struct Bid { - uint256 lotId; address bidder; address recipient; address referrer; diff --git a/src/modules/auctions/LSBBA.sol b/src/modules/auctions/LSBBA.sol new file mode 100644 index 00000000..e2d5cb17 --- /dev/null +++ b/src/modules/auctions/LSBBA.sol @@ -0,0 +1,209 @@ +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +// import "src/modules/auctions/bases/BatchAuction.sol"; +import {AuctionModule} from "src/modules/Auction.sol"; +import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; +import {RSAOAEP} from "src/lib/RSA.sol"; + +// A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends +// The auction occurs in three phases: +// 1. Bidding - bidders submit encrypted bids +// 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting +// 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred +// TODO abstract since not everything is implemented here +abstract contract LocalSealedBidBatchAuction is AuctionModule { + + // ========== ERRORS ========== // + error Auction_BidDoesNotExist(); + error Auction_NotBidder(); + error Auction_AlreadyCancelled(); + error Auction_WrongState(); + error Auction_NotLive(); + error Auction_NotConcluded(); + error Auction_InvalidDecrypt(); + + // ========== DATA STRUCTURES ========== // + + enum AuctionStatus { + Created, + KeyRevealed, + Decrypted, + Settled + } + + enum BidStatus { + Submitted, + Cancelled, + Decrypted, + Settled, + Refunded + } + + struct EncryptedBid { + BidStatus status; + address bidder; + address recipient; + address referrer; + uint256 amount; + bytes encryptedAmountOut; + } + + struct Decrypt { + uint256 amountOut; + uint256 seed; + } + + struct AuctionData { + AuctionStatus status; + bytes publicKeyModulus; + bytes privateKey; + uint256 minimumPrice; + uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price + uint256 nextDecryptIndex; + } + + // ========== STATE VARIABLES ========== // + + uint256 public constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas + uint256 public constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up + + mapping(uint96 lotId => AuctionData) public auctionData; + mapping(uint96 lotId => EncryptedBid[] bids) public lotBids; + + // ========== SETUP ========== // + + constructor(address auctionHouse_) AuctionModule(auctionHouse_) { + } + + function VEECODE() public pure override returns (Veecode) { + return toVeecode("01LSBBA"); + } + + function TYPE() public pure override returns (Type) { + return Type.Auction; + } + + // =========== BID =========== // + function bid(uint96 lotId_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_) external onlyInternal returns (uint256 bidId) { + // Check that bids are allowed to be submitted for the lot + if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion) revert Auction_NotLive(); + + // Validate inputs + // Amount at least minimum bid size for lot + if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); + + // Store bid data + // Auction data should just be the encrypted amount out (no decoding required) + EncryptedBid memory userBid; + userBid.bidder = msg.sender; + userBid.recipient = recipient_; + userBid.referrer = referrer_; + userBid.amount = amount_; + userBid.encryptedAmountOut = auctionData_; + userBid.status = BidStatus.Submitted; + + // Bid ID is the next index in the lot's bid array + bidId = lotBids[lotId_].length; + + // Add bid to lot + lotBids[lotId_].push(userBid); + } + + function cancelBid(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { + + // Validate inputs + // Auction for lot must still be live + if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion) revert Auction_NotLive(); + + // Bid ID must be less than number of bids for lot + if (bidId_ >= lotBids[lotId_].length) revert Auction_BidDoesNotExist(); + + // Sender must be bidder + if (sender_ != lotBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); + + // Bid is not already cancelled + if (lotBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_AlreadyCancelled(); + + // Set bid status to cancelled + lotBids[lotId_][bidId_].status = BidStatus.Cancelled; + } + + // =========== DECRYPTION =========== // + + function revealKey(uint96 lotId_, bytes calldata privateKey_) external onlyInternal { + // Check that the auction is in the right state for reveal + if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_NotConcluded(); + + // Validate inputs + // We should be able to generate the public key from the private key + // TODO trickier than it seems + + // Store key and set auction status to key revealed + auctionData[lotId_].status = AuctionStatus.KeyRevealed; + auctionData[lotId_].privateKey = privateKey_; + } + + function decryptBids(uint96 lotId_, Decrypt[] memory decrypts_) external { + // Check that auction is in the right state for decryption + if (auctionData[lotId_].status != AuctionStatus.KeyRevealed) revert Auction_WrongState(); + + // Load next decrypt index + uint256 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; + uint256 len = decrypts_.length; + + // Iterate over decrypts, validate that they match the stored encrypted bids, then store them in the sorted bid queue + for (uint256 i; i < len; i++) { + // Re-encrypt the decrypt to confirm that it matches the stored encrypted bid + bytes memory ciphertext = _encrypt(lotId_, decrypts_[i]); + + // Load encrypted bid + EncryptedBid storage encBid = lotBids[lotId_][nextDecryptIndex + i]; + + // Check that the encrypted bid matches the re-encrypted decrypt by hashing both + if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) revert Auction_InvalidDecrypt(); + + // Derive price from bid amount and decrypt amount out + uint256 price = (encBid.amount * SCALE) / decrypts_[i].amountOut; + + // Store the decrypt in the sorted bid queue + // TODO need to determine which data structure to use for the queue + + // Set bid status to decrypted + encBid.status = BidStatus.Decrypted; + } + + // Increment next decrypt index + auctionData[lotId_].nextDecryptIndex += len; + + // If all bids have been decrypted, set auction status to decrypted + if (auctionData[lotId_].nextDecryptIndex == lotBids[lotId_].length) auctionData[lotId_].status = AuctionStatus.Decrypted; + } + + function _encrypt(uint96 lotId_, Decrypt memory decrypt_) internal view returns (bytes memory) { + return RSAOAEP.encrypt(abi.encodePacked(decrypt_.amountOut), bytes(""), abi.encodePacked(PUB_KEY_EXPONENT), auctionData[lotId_].publicKeyModulus, decrypt_.seed); + } + + + // =========== SETTLEMENT =========== // + + function settle(uint96 lotId_) external onlyInternal returns (Bid[] memory winningBids_) { + // Check that auction is in the right state for settlement + if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); + + // Iterate over bid queue to calculate the marginal clearing price of the auction + + // Create winning bid array using marginal price to set amounts out + + // Set auction status to settled + + // Return winning bids + } + + + // =========== AUCTION MANAGEMENT ========== // + + // TODO auction creation + // TODO auction cancellation? + +} diff --git a/src/modules/auctions/SBB.sol b/src/modules/auctions/SBB.sol deleted file mode 100644 index 8320f0e2..00000000 --- a/src/modules/auctions/SBB.sol +++ /dev/null @@ -1,4 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// sealed bid batch auction diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 19a4259d..79966f8b 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -1,7 +1,9 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import "src/modules/Auction.sol"; +// TODO may not need this file. Lot of implementation specifics. + +import {Auction, AuctionModule} from "src/modules/Auction.sol"; // Spec // - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token From 0506ec15ca89a179eb7f22aea783ecfe22da8342 Mon Sep 17 00:00:00 2001 From: Oighty Date: Sat, 20 Jan 2024 15:55:44 -0600 Subject: [PATCH 015/117] feat: LSBBA updates --- src/lib/RSA.sol | 7 ++---- src/modules/auctions/LSBBA.sol | 39 ++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index 754fb4bb..b52d2237 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -23,7 +23,7 @@ library RSAOAEP { function decrypt(bytes memory cipherText, bytes memory d, bytes memory n, bytes memory label) internal view - returns (bytes memory) + returns (bytes memory message, bytes32 seed) { // Implements 7.1.2 RSAES-OAEP-DECRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 // Error messages are intentionally vague to prevent oracle attacks @@ -72,7 +72,6 @@ library RSAOAEP { // 3. c. Calculate seed mask // 3. d. Calculate seed - bytes32 seed; { bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); seed = maskedSeed ^ seedMask; @@ -96,7 +95,6 @@ library RSAOAEP { // Y is nonzero, output "decryption error" and stop. bytes32 recoveredHash = bytes32(db); bytes1 one; - bytes memory message; assembly { // Iterate over bytes after the label hash until hitting a non-zero byte // Skip the first word since it is the recovered hash @@ -149,8 +147,7 @@ library RSAOAEP { if (one != 0x01 || lhash != recoveredHash || y != 0x00) revert("decryption error"); - // 4. Return the message - return message; + // 4. Return the message and seed used for encryption } function encrypt(bytes memory message, bytes memory label, bytes memory e, bytes memory n, uint256 seed) diff --git a/src/modules/auctions/LSBBA.sol b/src/modules/auctions/LSBBA.sol index e2d5cb17..fa3a418b 100644 --- a/src/modules/auctions/LSBBA.sol +++ b/src/modules/auctions/LSBBA.sol @@ -27,7 +27,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { enum AuctionStatus { Created, - KeyRevealed, Decrypted, Settled } @@ -57,7 +56,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { struct AuctionData { AuctionStatus status; bytes publicKeyModulus; - bytes privateKey; uint256 minimumPrice; uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price uint256 nextDecryptIndex; @@ -131,22 +129,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== DECRYPTION =========== // - function revealKey(uint96 lotId_, bytes calldata privateKey_) external onlyInternal { - // Check that the auction is in the right state for reveal - if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_NotConcluded(); - - // Validate inputs - // We should be able to generate the public key from the private key - // TODO trickier than it seems - - // Store key and set auction status to key revealed - auctionData[lotId_].status = AuctionStatus.KeyRevealed; - auctionData[lotId_].privateKey = privateKey_; - } - - function decryptBids(uint96 lotId_, Decrypt[] memory decrypts_) external { + function decryptAndSortBids(uint96 lotId_, Decrypt[] memory decrypts_) external { // Check that auction is in the right state for decryption - if (auctionData[lotId_].status != AuctionStatus.KeyRevealed) revert Auction_WrongState(); + if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_WrongState(); // Load next decrypt index uint256 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; @@ -181,7 +166,25 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } function _encrypt(uint96 lotId_, Decrypt memory decrypt_) internal view returns (bytes memory) { - return RSAOAEP.encrypt(abi.encodePacked(decrypt_.amountOut), bytes(""), abi.encodePacked(PUB_KEY_EXPONENT), auctionData[lotId_].publicKeyModulus, decrypt_.seed); + return RSAOAEP.encrypt(abi.encodePacked(decrypt_.amountOut), abi.encodePacked(lotId_), abi.encodePacked(PUB_KEY_EXPONENT), auctionData[lotId_].publicKeyModulus, decrypt_.seed); + } + + /// @notice View function that can be used to obtain the amount out and seed for a given bid by providing the private key + /// @dev This function can be used to decrypt bids off-chain if you know the private key + function decryptBid(uint96 lotId_, uint96 bidId_, bytes memory privateKey_) external view returns (Decrypt memory) { + // Load encrypted bid + EncryptedBid memory encBid = lotBids[lotId_][bidId_]; + + // Decrypt the encrypted amount out + (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt(encBid.encryptedAmountOut, abi.encodePacked(lotId_), privateKey_, auctionData[lotId_].publicKeyModulus); + + // Cast the decrypted values + Decrypt memory decrypt; + decrypt.amountOut = abi.decode(amountOut, (uint256)); + decrypt.seed = uint256(seed); + + // Return the decrypt + return decrypt; } From 4b97dcb47ddfb3bad3119d9563095e396daccd58 Mon Sep 17 00:00:00 2001 From: Oighty Date: Sat, 20 Jan 2024 16:40:17 -0600 Subject: [PATCH 016/117] feat: add priority queue implementation to LSSBA --- src/modules/auctions/{ => LSBBA}/LSBBA.sol | 23 ++-- .../auctions/LSBBA/MinPriorityQueue.sol | 124 ++++++++++++++++++ 2 files changed, 137 insertions(+), 10 deletions(-) rename src/modules/auctions/{ => LSBBA}/LSBBA.sol (90%) create mode 100644 src/modules/auctions/LSBBA/MinPriorityQueue.sol diff --git a/src/modules/auctions/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol similarity index 90% rename from src/modules/auctions/LSBBA.sol rename to src/modules/auctions/LSBBA/LSBBA.sol index fa3a418b..2a28b920 100644 --- a/src/modules/auctions/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.19; import {AuctionModule} from "src/modules/Auction.sol"; import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; +import {MinPriorityQueue} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; // A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends // The auction occurs in three phases: @@ -13,6 +14,7 @@ import {RSAOAEP} from "src/lib/RSA.sol"; // 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred // TODO abstract since not everything is implemented here abstract contract LocalSealedBidBatchAuction is AuctionModule { + using MinPriorityQueue for MinPriorityQueue.Queue; // ========== ERRORS ========== // error Auction_BidDoesNotExist(); @@ -55,19 +57,20 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { struct AuctionData { AuctionStatus status; - bytes publicKeyModulus; + uint96 nextDecryptIndex; uint256 minimumPrice; uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price - uint256 nextDecryptIndex; + bytes publicKeyModulus; } // ========== STATE VARIABLES ========== // - uint256 public constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas + uint256 public constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas, but 65537 is probably more secure uint256 public constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => EncryptedBid[] bids) public lotBids; + mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; // TODO must create and call `initialize` on it during auction creation // ========== SETUP ========== // @@ -134,11 +137,14 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_WrongState(); // Load next decrypt index - uint256 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; - uint256 len = decrypts_.length; + uint96 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; + uint96 len = uint96(decrypts_.length); + + // Check that the number of decrypts is less than or equal to the number of bids remaining to be decrypted + if (len > lotBids[lotId_].length - nextDecryptIndex) revert Auction_InvalidDecrypt(); // Iterate over decrypts, validate that they match the stored encrypted bids, then store them in the sorted bid queue - for (uint256 i; i < len; i++) { + for (uint96 i; i < len; i++) { // Re-encrypt the decrypt to confirm that it matches the stored encrypted bid bytes memory ciphertext = _encrypt(lotId_, decrypts_[i]); @@ -147,12 +153,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Check that the encrypted bid matches the re-encrypted decrypt by hashing both if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) revert Auction_InvalidDecrypt(); - - // Derive price from bid amount and decrypt amount out - uint256 price = (encBid.amount * SCALE) / decrypts_[i].amountOut; // Store the decrypt in the sorted bid queue - // TODO need to determine which data structure to use for the queue + lotSortedBids[lotId_].insert(nextDecryptIndex + i, encBid.amount, decrypts_[i].amountOut); // Set bid status to decrypted encBid.status = BidStatus.Decrypted; diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol new file mode 100644 index 00000000..889d1ae3 --- /dev/null +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -0,0 +1,124 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +struct Bid { + uint96 bidId; // ID in queue + uint96 encId; // ID of encrypted bid to reference on settlement + uint256 amountIn; + uint256 minAmountOut; +} + +/// @notice a min priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MinPQ.java.html +/// @notice adapted from FrankieIsLost's implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MinPriorityQueue.sol +/// @author FrankieIsLost +/// @author Oighty (edits) +library MinPriorityQueue { + + struct Queue { + ///@notice incrementing bid id + uint96 nextBidId; + ///@notice array backing priority queue + uint96[] bidIdList; + ///@notice total number of bids in queue + uint96 numBids; + //@notice map bid ids to bids + mapping(uint96 => Bid) bidIdToBidMap; + } + + ///@notice initialize must be called before using queue. + function initialize(Queue storage self) public { + self.bidIdList.push(0); + self.nextBidId = 1; + } + + function isEmpty(Queue storage self) public view returns (bool) { + return self.numBids == 0; + } + + function getNumBids(Queue storage self) public view returns (uint256) { + return self.numBids; + } + + ///@notice view min bid + function getMin(Queue storage self) public view returns (Bid storage) { + require(!isEmpty(self), "nothing to return"); + uint96 minId = self.bidIdList[1]; + return self.bidIdToBidMap[minId]; + } + + ///@notice view bid by index + function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { + require(!isEmpty(self), "nothing to return"); + require(index <= self.numBids, "bid does not exist"); + return self.bidIdToBidMap[self.bidIdList[index]]; + } + + ///@notice move bid up heap + function swim(Queue storage self, uint96 k) private { + while(k > 1 && isGreater(self, k/2, k)) { + exchange(self, k, k/2); + k = k/2; + } + } + + ///@notice move bid down heap + function sink(Queue storage self, uint96 k) private { + while(2 * k <= self.numBids) { + uint96 j = 2 * k; + if(j < self.numBids && isGreater(self, j, j+1)) { + j++; + } + if (!isGreater(self, k, j)) { + break; + } + exchange(self, k, j); + k = j; + } + } + + ///@notice insert bid in heap + function insert(Queue storage self, uint96 encId, uint256 amountIn, uint256 minAmountOut) public { + insert(self, Bid(self.nextBidId++, encId, amountIn, minAmountOut)); + } + + ///@notice insert bid in heap + function insert(Queue storage self, Bid memory bid) private { + self.bidIdList.push(bid.bidId); + self.bidIdToBidMap[bid.bidId] = bid; + self.numBids += 1; + swim(self, self.numBids); + } + + ///@notice delete min bid from heap and return + function delMin(Queue storage self) public returns (Bid memory) { + require(!isEmpty(self), "nothing to delete"); + Bid memory min = self.bidIdToBidMap[self.bidIdList[1]]; + exchange(self, 1, self.numBids--); + self.bidIdList.pop(); + delete self.bidIdToBidMap[min.bidId]; + sink(self, 1); + return min; + } + + ///@notice helper function to determine ordering. When two bids have the same price, give priority + ///to the lower bid ID (inserted earlier) + function isGreater(Queue storage self, uint256 i, uint256 j) private view returns (bool) { + uint96 iId = self.bidIdList[i]; + uint96 jId = self.bidIdList[j]; + Bid memory bidI = self.bidIdToBidMap[iId]; + Bid memory bidJ = self.bidIdToBidMap[jId]; + uint256 relI = bidI.amountIn * bidJ.minAmountOut; + uint256 relJ = bidJ.amountIn * bidI.minAmountOut; + if(relI == relJ) { + return iId < jId; + } + return relI > relJ; + } + + ///@notice helper function to exchange to bids in the heap + function exchange(Queue storage self, uint256 i, uint256 j) private { + uint96 tempId = self.bidIdList[i]; + self.bidIdList[i] = self.bidIdList[j]; + self.bidIdList[j] = tempId; + } +} \ No newline at end of file From eb9c1aec5f2a6662b31c7c23a3f8a397e4dcf061 Mon Sep 17 00:00:00 2001 From: Oighty Date: Sat, 20 Jan 2024 16:59:24 -0600 Subject: [PATCH 017/117] feat: add settlement function to LSBBA --- src/modules/auctions/LSBBA/LSBBA.sol | 73 +++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 2a28b920..aa91f45c 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.19; import {AuctionModule} from "src/modules/Auction.sol"; import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; -import {MinPriorityQueue} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; // A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends // The auction occurs in three phases: @@ -59,6 +59,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { AuctionStatus status; uint96 nextDecryptIndex; uint256 minimumPrice; + uint256 minFilled; // minimum amount of capacity that must be filled to settle the auction uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price bytes publicKeyModulus; } @@ -197,13 +198,83 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Check that auction is in the right state for settlement if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); + // Cache capacity + uint256 capacity = lotData[lotId_].capacity; + // Iterate over bid queue to calculate the marginal clearing price of the auction + MinPriorityQueue.Queue storage queue = lotSortedBids[lotId_]; + uint256 marginalPrice; + uint256 totalAmountIn; + uint256 winningBidIndex; + for (uint256 i = 1; i <= queue.numBids; i++) { + // Load bid + QueueBid storage qBid = queue.getBid(i); + + // Calculate bid price + uint256 price = (qBid.amountIn * SCALE) / qBid.minAmountOut; + + // Increment total amount in + totalAmountIn += qBid.amountIn; + + // Determine total capacity expended at this price + uint256 expended = (totalAmountIn * SCALE) / price; + + // If total capacity expended is greater than or equal to the capacity, we have found the marginal price + if (expended >= capacity) { + marginalPrice = price; + winningBidIndex = i; + break; + } + + // If we have reached the end of the queue, we have found the marginal price and the maximum capacity that can be filled + if (i == queue.numBids) { + // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) + if (expended < auctionData[lotId_].minFilled) { + auctionData[lotId_].status = AuctionStatus.Settled; + return winningBids_; + } else { + marginalPrice = price; + winningBidIndex = i; + } + } + + } + // Check if the minimum price for the auction was reached + // If not, mark as settled and return no winning bids (so users can claim refunds) + if (marginalPrice < auctionData[lotId_].minimumPrice) { + auctionData[lotId_].status = AuctionStatus.Settled; + return winningBids_; + } + + // Auction can be settled at the marginal price if we reach this point // Create winning bid array using marginal price to set amounts out + winningBids_ = new Bid[](winningBidIndex); + for (uint256 i; i < winningBidIndex; i++) { + // Load bid + QueueBid memory qBid = queue.delMin(); + + // Calculate amount out + uint256 amountOut = (qBid.amountIn * SCALE) / marginalPrice; + + // Create winning bid from encrypted bid and calculated amount out + EncryptedBid memory encBid = lotBids[lotId_][qBid.encId]; + Bid memory winningBid; + winningBid.bidder = encBid.bidder; + winningBid.recipient = encBid.recipient; + winningBid.referrer = encBid.referrer; + winningBid.amount = encBid.amount; + winningBid.minAmountOut = amountOut; + + // Add winning bid to array + winningBids_[i] = winningBid; + } // Set auction status to settled + auctionData[lotId_].status = AuctionStatus.Settled; // Return winning bids + return winningBids_; } From 55acd28253378bf9a2eb9b7abe56b48ac2b638e0 Mon Sep 17 00:00:00 2001 From: Oighty Date: Sat, 20 Jan 2024 21:18:04 -0600 Subject: [PATCH 018/117] feat: add auction, cancelAuction, and claimRefund functions to LSBBA --- src/modules/Auction.sol | 18 +++-- src/modules/auctions/LSBBA/LSBBA.sol | 108 +++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 24 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 45654d26..c582d562 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -95,6 +95,8 @@ abstract contract Auction { bytes calldata approval_ ) external virtual; + function cancelBid(uint96 lotId_, uint96 bidId_) external virtual; + /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement /// @@ -116,9 +118,9 @@ abstract contract Auction { // TODO NatSpec comments // TODO validate function - function auction(uint256 id_, AuctionParams memory params_) external virtual; + function auction(uint96 id_, AuctionParams memory params_) external virtual; - function cancel(uint256 id_) external virtual; + function cancelAuction(uint96 id_) external virtual; // ========== AUCTION INFORMATION ========== // @@ -151,7 +153,7 @@ abstract contract AuctionModule is Auction, Module { /// - the duration is less than the minimum /// /// @param lotId_ The lot id - function auction(uint256 lotId_, AuctionParams memory params_) external override onlyParent { + function auction(uint96 lotId_, AuctionParams memory params_) external override onlyParent { // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidStart(params_.start, uint48(block.timestamp)); @@ -178,10 +180,10 @@ abstract contract AuctionModule is Auction, Module { /// @dev implementation-specific auction creation logic can be inserted by overriding this function function _auction( - uint256 id_, + uint96 lotId_, Lot memory lot_, bytes memory params_ - ) internal virtual returns (uint256); + ) internal virtual; /// @notice Cancel an auction lot /// @dev Owner is stored in the Routing information on the AuctionHouse, so we check permissions there @@ -191,7 +193,7 @@ abstract contract AuctionModule is Auction, Module { /// - the lot is not active /// /// @param lotId_ The lot id - function cancel(uint256 lotId_) external override onlyParent { + function cancelAuction(uint96 lotId_) external override onlyParent { Lot storage lot = lotData[lotId_]; // Invalid lot @@ -204,10 +206,10 @@ abstract contract AuctionModule is Auction, Module { lot.capacity = 0; // Call internal closeAuction function to update any other required parameters - _cancel(lotId_); + _cancelAuction(lotId_); } - function _cancel(uint256 id_) internal virtual; + function _cancelAuction(uint96 id_) internal virtual; // ========== AUCTION INFORMATION ========== // diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index aa91f45c..0f2c0ceb 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -37,7 +37,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { Submitted, Cancelled, Decrypted, - Settled, + Won, Refunded } @@ -66,11 +66,13 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== STATE VARIABLES ========== // - uint256 public constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas, but 65537 is probably more secure - uint256 public constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up + uint256 internal constant MIN_BID_PERCENT = 1_000; // 1% + uint256 internal constant ONE_HUNDRED_PERCENT = 100_000; + uint256 internal constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas, but 65537 is probably more secure + uint256 internal constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; - mapping(uint96 lotId => EncryptedBid[] bids) public lotBids; + mapping(uint96 lotId => EncryptedBid[] bids) public lotEncryptedBids; mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; // TODO must create and call `initialize` on it during auction creation // ========== SETUP ========== // @@ -106,10 +108,10 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { userBid.status = BidStatus.Submitted; // Bid ID is the next index in the lot's bid array - bidId = lotBids[lotId_].length; + bidId = lotEncryptedBids[lotId_].length; // Add bid to lot - lotBids[lotId_].push(userBid); + lotEncryptedBids[lotId_].push(userBid); } function cancelBid(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { @@ -119,16 +121,42 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion) revert Auction_NotLive(); // Bid ID must be less than number of bids for lot - if (bidId_ >= lotBids[lotId_].length) revert Auction_BidDoesNotExist(); + if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); // Sender must be bidder - if (sender_ != lotBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); + if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); // Bid is not already cancelled - if (lotBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_AlreadyCancelled(); + if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_AlreadyCancelled(); // Set bid status to cancelled - lotBids[lotId_][bidId_].status = BidStatus.Cancelled; + lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; + } + + // TODO need a top-level function on the Auction House that actually sends the funds to the recipient + function claimRefund(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { + // Validate inputs + // Sender must be bidder + if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); + + // Auction for must have settled to claim refund + // User must not have won the auction or claimed a refund already + // TODO should we allow cancel bids to claim earlier? + // Might allow legit users to change their bids + // But also allows a malicious user to use the same funds to create + // multiple bids in an attempt to grief the settlement + BidStatus bidStatus = lotEncryptedBids[lotId_][bidId_].status; + if ( + auctionData[lotId_].status != AuctionStatus.Settled || + bidStatus == BidStatus.Refunded || + bidStatus == BidStatus.Won + ) revert Auction_WrongState(); + + // Bid ID must be less than number of bids for lot + if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); + + // Set bid status to refunded + lotEncryptedBids[lotId_][bidId_].status = BidStatus.Refunded; } // =========== DECRYPTION =========== // @@ -142,7 +170,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { uint96 len = uint96(decrypts_.length); // Check that the number of decrypts is less than or equal to the number of bids remaining to be decrypted - if (len > lotBids[lotId_].length - nextDecryptIndex) revert Auction_InvalidDecrypt(); + if (len > lotEncryptedBids[lotId_].length - nextDecryptIndex) revert Auction_InvalidDecrypt(); // Iterate over decrypts, validate that they match the stored encrypted bids, then store them in the sorted bid queue for (uint96 i; i < len; i++) { @@ -150,11 +178,15 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { bytes memory ciphertext = _encrypt(lotId_, decrypts_[i]); // Load encrypted bid - EncryptedBid storage encBid = lotBids[lotId_][nextDecryptIndex + i]; + EncryptedBid storage encBid = lotEncryptedBids[lotId_][nextDecryptIndex + i]; // Check that the encrypted bid matches the re-encrypted decrypt by hashing both if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) revert Auction_InvalidDecrypt(); + // If the bid has been cancelled, it shouldn't be added to the queue + // TODO should this just check != Submitted? + if (encBid.status == BidStatus.Cancelled) continue; + // Store the decrypt in the sorted bid queue lotSortedBids[lotId_].insert(nextDecryptIndex + i, encBid.amount, decrypts_[i].amountOut); @@ -166,7 +198,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { auctionData[lotId_].nextDecryptIndex += len; // If all bids have been decrypted, set auction status to decrypted - if (auctionData[lotId_].nextDecryptIndex == lotBids[lotId_].length) auctionData[lotId_].status = AuctionStatus.Decrypted; + if (auctionData[lotId_].nextDecryptIndex == lotEncryptedBids[lotId_].length) auctionData[lotId_].status = AuctionStatus.Decrypted; } function _encrypt(uint96 lotId_, Decrypt memory decrypt_) internal view returns (bytes memory) { @@ -177,7 +209,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { /// @dev This function can be used to decrypt bids off-chain if you know the private key function decryptBid(uint96 lotId_, uint96 bidId_, bytes memory privateKey_) external view returns (Decrypt memory) { // Load encrypted bid - EncryptedBid memory encBid = lotBids[lotId_][bidId_]; + EncryptedBid memory encBid = lotEncryptedBids[lotId_][bidId_]; // Decrypt the encrypted amount out (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt(encBid.encryptedAmountOut, abi.encodePacked(lotId_), privateKey_, auctionData[lotId_].publicKeyModulus); @@ -258,7 +290,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { uint256 amountOut = (qBid.amountIn * SCALE) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out - EncryptedBid memory encBid = lotBids[lotId_][qBid.encId]; + EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.encId]; Bid memory winningBid; winningBid.bidder = encBid.bidder; winningBid.recipient = encBid.recipient; @@ -266,6 +298,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { winningBid.amount = encBid.amount; winningBid.minAmountOut = amountOut; + // Set bid status to won + encBid.status = BidStatus.Won; + // Add winning bid to array winningBids_[i] = winningBid; } @@ -281,6 +316,47 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== AUCTION MANAGEMENT ========== // // TODO auction creation - // TODO auction cancellation? + function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal override { + // Decode implementation params + ( + uint256 minimumPrice, + uint256 minFillPercent, + uint256 minBidPercent, + bytes memory publicKeyModulus + ) = abi.decode(params_, (uint256, uint256, uint256, bytes)); + + // Validate params + // Capacity must be in base token for this auction type + if (lot_.capacityInQuote) revert Auction_InvalidParams(); + + // minFillPercent must be less than or equal to 100% + // TODO should there be a minimum? + if (minFillPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + + // minBidPercent must be greater than or equal to the global min and less than or equal to 100% + // TODO should we cap this below 100%? + if (minBidPercent < MIN_BID_PERCENT || minBidPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + + // publicKeyModulus must be 1024 bits (128 bytes) + if (publicKeyModulus.length != 128) revert Auction_InvalidParams(); + + // Store auction data + AuctionData storage data = auctionData[lotId_]; + data.minimumPrice = minimumPrice; + data.minFilled = (lot_.capacity * minFillPercent) / ONE_HUNDRED_PERCENT; + data.minBidSize = (lot_.capacity * minBidPercent) / ONE_HUNDRED_PERCENT; + data.publicKeyModulus = publicKeyModulus; + + // Initialize sorted bid queue + lotSortedBids[lotId_].initialize(); + } + + function _cancelAuction(uint96 lotId_) internal override { + // Auction cannot be cancelled once it has concluded + if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_WrongState(); + + // Set auction status to settled so that bids can be refunded + auctionData[lotId_].status = AuctionStatus.Settled; + } } From 5fb4ca4bdfe37d1a1e833222401460ce63fff75a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 11:22:28 +0400 Subject: [PATCH 019/117] Notes --- src/AuctionHouse.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index bcd0b4f4..5a969c4f 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -123,10 +123,10 @@ abstract contract Router is FeeManager { address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, + bytes calldata auctionData_, // sequential hash of bids, minimum amount out encrypted with auction public key bytes calldata allowlistProof_, bytes calldata permit2Data_ - ) external virtual; + ) external virtual returns (uint256 bidId); /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement @@ -407,6 +407,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Calculate fees + // TODO extract this to a function uint256 totalAmountInLessFees; uint256 totalAmountOut; { From 89e47c2f2e9fde9a9a996466233859f3d3c2ed1b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 12:22:22 +0400 Subject: [PATCH 020/117] chore: linting --- src/lib/RSA.sol | 38 +++--- src/modules/Auction.sol | 6 +- src/modules/auctions/LSBBA/LSBBA.sol | 109 ++++++++++++------ .../auctions/LSBBA/MinPriorityQueue.sol | 38 +++--- 4 files changed, 120 insertions(+), 71 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index b52d2237..1caca93c 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -6,11 +6,11 @@ pragma solidity 0.8.19; /// @author Oighty // TODO Need to add tests for this library library RSAOAEP { - function modexp(bytes memory base, bytes memory exponent, bytes memory modulus) - public - view - returns (bytes memory) - { + function modexp( + bytes memory base, + bytes memory exponent, + bytes memory modulus + ) public view returns (bytes memory) { (bool success, bytes memory output) = address(0x05).staticcall( abi.encodePacked(base.length, exponent.length, modulus.length, base, exponent, modulus) ); @@ -20,11 +20,12 @@ library RSAOAEP { return output; } - function decrypt(bytes memory cipherText, bytes memory d, bytes memory n, bytes memory label) - internal - view - returns (bytes memory message, bytes32 seed) - { + function decrypt( + bytes memory cipherText, + bytes memory d, + bytes memory n, + bytes memory label + ) internal view returns (bytes memory message, bytes32 seed) { // Implements 7.1.2 RSAES-OAEP-DECRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 // Error messages are intentionally vague to prevent oracle attacks @@ -66,7 +67,10 @@ library RSAOAEP { // Store the remaining bytes into the maskedDb for { let i := 0 } lt(i, words) { i := add(i, 1) } { - mstore(add(add(maskedDb, 0x20), mul(i, 0x20)), mload(add(add(encoded, 0x41), mul(i, 0x20)))) + mstore( + add(add(maskedDb, 0x20), mul(i, 0x20)), + mload(add(add(encoded, 0x41), mul(i, 0x20))) + ) } } @@ -150,11 +154,13 @@ library RSAOAEP { // 4. Return the message and seed used for encryption } - function encrypt(bytes memory message, bytes memory label, bytes memory e, bytes memory n, uint256 seed) - internal - view - returns (bytes memory) - { + function encrypt( + bytes memory message, + bytes memory label, + bytes memory e, + bytes memory n, + uint256 seed + ) internal view returns (bytes memory) { // Implements 7.1.1. RSAES-OAEP-ENCRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 // 1. a. Check that the label length is less than the max for sha256 diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index c582d562..def91a47 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -179,11 +179,7 @@ abstract contract AuctionModule is Auction, Module { } /// @dev implementation-specific auction creation logic can be inserted by overriding this function - function _auction( - uint96 lotId_, - Lot memory lot_, - bytes memory params_ - ) internal virtual; + function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal virtual; /// @notice Cancel an auction lot /// @dev Owner is stored in the Routing information on the AuctionHouse, so we check permissions there diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 0f2c0ceb..7d4ae611 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -66,9 +66,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== STATE VARIABLES ========== // - uint256 internal constant MIN_BID_PERCENT = 1_000; // 1% + uint256 internal constant MIN_BID_PERCENT = 1000; // 1% uint256 internal constant ONE_HUNDRED_PERCENT = 100_000; - uint256 internal constant PUB_KEY_EXPONENT = 65537; // TODO can be 3 to save gas, but 65537 is probably more secure + uint256 internal constant PUB_KEY_EXPONENT = 65_537; // TODO can be 3 to save gas, but 65537 is probably more secure uint256 internal constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; @@ -77,8 +77,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== SETUP ========== // - constructor(address auctionHouse_) AuctionModule(auctionHouse_) { - } + constructor(address auctionHouse_) AuctionModule(auctionHouse_) {} function VEECODE() public pure override returns (Veecode) { return toVeecode("01LSBBA"); @@ -89,10 +88,20 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } // =========== BID =========== // - function bid(uint96 lotId_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_) external onlyInternal returns (uint256 bidId) { + function bid( + uint96 lotId_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_ + ) external onlyInternal returns (uint256 bidId) { // Check that bids are allowed to be submitted for the lot - if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion) revert Auction_NotLive(); - + if ( + auctionData[lotId_].status != AuctionStatus.Created + || block.timestamp < lotData[lotId_].start + || block.timestamp >= lotData[lotId_].conclusion + ) revert Auction_NotLive(); + // Validate inputs // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); @@ -115,10 +124,13 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } function cancelBid(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { - // Validate inputs // Auction for lot must still be live - if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion) revert Auction_NotLive(); + if ( + auctionData[lotId_].status != AuctionStatus.Created + || block.timestamp < lotData[lotId_].start + || block.timestamp >= lotData[lotId_].conclusion + ) revert Auction_NotLive(); // Bid ID must be less than number of bids for lot if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); @@ -127,7 +139,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); // Bid is not already cancelled - if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_AlreadyCancelled(); + if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { + revert Auction_AlreadyCancelled(); + } // Set bid status to cancelled lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; @@ -143,13 +157,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // User must not have won the auction or claimed a refund already // TODO should we allow cancel bids to claim earlier? // Might allow legit users to change their bids - // But also allows a malicious user to use the same funds to create + // But also allows a malicious user to use the same funds to create // multiple bids in an attempt to grief the settlement BidStatus bidStatus = lotEncryptedBids[lotId_][bidId_].status; if ( - auctionData[lotId_].status != AuctionStatus.Settled || - bidStatus == BidStatus.Refunded || - bidStatus == BidStatus.Won + auctionData[lotId_].status != AuctionStatus.Settled || bidStatus == BidStatus.Refunded + || bidStatus == BidStatus.Won ) revert Auction_WrongState(); // Bid ID must be less than number of bids for lot @@ -163,14 +176,19 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { function decryptAndSortBids(uint96 lotId_, Decrypt[] memory decrypts_) external { // Check that auction is in the right state for decryption - if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_WrongState(); - + if ( + auctionData[lotId_].status != AuctionStatus.Created + || block.timestamp < lotData[lotId_].conclusion + ) revert Auction_WrongState(); + // Load next decrypt index uint96 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; uint96 len = uint96(decrypts_.length); // Check that the number of decrypts is less than or equal to the number of bids remaining to be decrypted - if (len > lotEncryptedBids[lotId_].length - nextDecryptIndex) revert Auction_InvalidDecrypt(); + if (len > lotEncryptedBids[lotId_].length - nextDecryptIndex) { + revert Auction_InvalidDecrypt(); + } // Iterate over decrypts, validate that they match the stored encrypted bids, then store them in the sorted bid queue for (uint96 i; i < len; i++) { @@ -181,14 +199,18 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { EncryptedBid storage encBid = lotEncryptedBids[lotId_][nextDecryptIndex + i]; // Check that the encrypted bid matches the re-encrypted decrypt by hashing both - if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) revert Auction_InvalidDecrypt(); - + if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) { + revert Auction_InvalidDecrypt(); + } + // If the bid has been cancelled, it shouldn't be added to the queue // TODO should this just check != Submitted? if (encBid.status == BidStatus.Cancelled) continue; // Store the decrypt in the sorted bid queue - lotSortedBids[lotId_].insert(nextDecryptIndex + i, encBid.amount, decrypts_[i].amountOut); + lotSortedBids[lotId_].insert( + nextDecryptIndex + i, encBid.amount, decrypts_[i].amountOut + ); // Set bid status to decrypted encBid.status = BidStatus.Decrypted; @@ -198,21 +220,41 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { auctionData[lotId_].nextDecryptIndex += len; // If all bids have been decrypted, set auction status to decrypted - if (auctionData[lotId_].nextDecryptIndex == lotEncryptedBids[lotId_].length) auctionData[lotId_].status = AuctionStatus.Decrypted; + if (auctionData[lotId_].nextDecryptIndex == lotEncryptedBids[lotId_].length) { + auctionData[lotId_].status = AuctionStatus.Decrypted; + } } - function _encrypt(uint96 lotId_, Decrypt memory decrypt_) internal view returns (bytes memory) { - return RSAOAEP.encrypt(abi.encodePacked(decrypt_.amountOut), abi.encodePacked(lotId_), abi.encodePacked(PUB_KEY_EXPONENT), auctionData[lotId_].publicKeyModulus, decrypt_.seed); + function _encrypt( + uint96 lotId_, + Decrypt memory decrypt_ + ) internal view returns (bytes memory) { + return RSAOAEP.encrypt( + abi.encodePacked(decrypt_.amountOut), + abi.encodePacked(lotId_), + abi.encodePacked(PUB_KEY_EXPONENT), + auctionData[lotId_].publicKeyModulus, + decrypt_.seed + ); } /// @notice View function that can be used to obtain the amount out and seed for a given bid by providing the private key /// @dev This function can be used to decrypt bids off-chain if you know the private key - function decryptBid(uint96 lotId_, uint96 bidId_, bytes memory privateKey_) external view returns (Decrypt memory) { + function decryptBid( + uint96 lotId_, + uint96 bidId_, + bytes memory privateKey_ + ) external view returns (Decrypt memory) { // Load encrypted bid EncryptedBid memory encBid = lotEncryptedBids[lotId_][bidId_]; // Decrypt the encrypted amount out - (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt(encBid.encryptedAmountOut, abi.encodePacked(lotId_), privateKey_, auctionData[lotId_].publicKeyModulus); + (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt( + encBid.encryptedAmountOut, + abi.encodePacked(lotId_), + privateKey_, + auctionData[lotId_].publicKeyModulus + ); // Cast the decrypted values Decrypt memory decrypt; @@ -223,7 +265,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { return decrypt; } - // =========== SETTLEMENT =========== // function settle(uint96 lotId_) external onlyInternal returns (Bid[] memory winningBids_) { @@ -269,7 +310,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { winningBidIndex = i; } } - } // Check if the minimum price for the auction was reached @@ -312,7 +352,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { return winningBids_; } - // =========== AUCTION MANAGEMENT ========== // // TODO auction creation @@ -330,12 +369,14 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { if (lot_.capacityInQuote) revert Auction_InvalidParams(); // minFillPercent must be less than or equal to 100% - // TODO should there be a minimum? + // TODO should there be a minimum? if (minFillPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); // minBidPercent must be greater than or equal to the global min and less than or equal to 100% // TODO should we cap this below 100%? - if (minBidPercent < MIN_BID_PERCENT || minBidPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + if (minBidPercent < MIN_BID_PERCENT || minBidPercent > ONE_HUNDRED_PERCENT) { + revert Auction_InvalidParams(); + } // publicKeyModulus must be 1024 bits (128 bytes) if (publicKeyModulus.length != 128) revert Auction_InvalidParams(); @@ -350,13 +391,15 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Initialize sorted bid queue lotSortedBids[lotId_].initialize(); } - + function _cancelAuction(uint96 lotId_) internal override { // Auction cannot be cancelled once it has concluded - if (auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].conclusion) revert Auction_WrongState(); + if ( + auctionData[lotId_].status != AuctionStatus.Created + || block.timestamp < lotData[lotId_].conclusion + ) revert Auction_WrongState(); // Set auction status to settled so that bids can be refunded auctionData[lotId_].status = AuctionStatus.Settled; } - } diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index 889d1ae3..761a118e 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -13,19 +13,18 @@ struct Bid { /// @author FrankieIsLost /// @author Oighty (edits) library MinPriorityQueue { - struct Queue { ///@notice incrementing bid id uint96 nextBidId; ///@notice array backing priority queue uint96[] bidIdList; - ///@notice total number of bids in queue + ///@notice total number of bids in queue uint96 numBids; //@notice map bid ids to bids mapping(uint96 => Bid) bidIdToBidMap; } - ///@notice initialize must be called before using queue. + ///@notice initialize must be called before using queue. function initialize(Queue storage self) public { self.bidIdList.push(0); self.nextBidId = 1; @@ -55,17 +54,17 @@ library MinPriorityQueue { ///@notice move bid up heap function swim(Queue storage self, uint96 k) private { - while(k > 1 && isGreater(self, k/2, k)) { - exchange(self, k, k/2); - k = k/2; + while (k > 1 && isGreater(self, k / 2, k)) { + exchange(self, k, k / 2); + k = k / 2; } } ///@notice move bid down heap function sink(Queue storage self, uint96 k) private { - while(2 * k <= self.numBids) { + while (2 * k <= self.numBids) { uint96 j = 2 * k; - if(j < self.numBids && isGreater(self, j, j+1)) { + if (j < self.numBids && isGreater(self, j, j + 1)) { j++; } if (!isGreater(self, k, j)) { @@ -76,12 +75,17 @@ library MinPriorityQueue { } } - ///@notice insert bid in heap - function insert(Queue storage self, uint96 encId, uint256 amountIn, uint256 minAmountOut) public { + ///@notice insert bid in heap + function insert( + Queue storage self, + uint96 encId, + uint256 amountIn, + uint256 minAmountOut + ) public { insert(self, Bid(self.nextBidId++, encId, amountIn, minAmountOut)); } - ///@notice insert bid in heap + ///@notice insert bid in heap function insert(Queue storage self, Bid memory bid) private { self.bidIdList.push(bid.bidId); self.bidIdToBidMap[bid.bidId] = bid; @@ -89,7 +93,7 @@ library MinPriorityQueue { swim(self, self.numBids); } - ///@notice delete min bid from heap and return + ///@notice delete min bid from heap and return function delMin(Queue storage self) public returns (Bid memory) { require(!isEmpty(self), "nothing to delete"); Bid memory min = self.bidIdToBidMap[self.bidIdList[1]]; @@ -100,7 +104,7 @@ library MinPriorityQueue { return min; } - ///@notice helper function to determine ordering. When two bids have the same price, give priority + ///@notice helper function to determine ordering. When two bids have the same price, give priority ///to the lower bid ID (inserted earlier) function isGreater(Queue storage self, uint256 i, uint256 j) private view returns (bool) { uint96 iId = self.bidIdList[i]; @@ -109,16 +113,16 @@ library MinPriorityQueue { Bid memory bidJ = self.bidIdToBidMap[jId]; uint256 relI = bidI.amountIn * bidJ.minAmountOut; uint256 relJ = bidJ.amountIn * bidI.minAmountOut; - if(relI == relJ) { + if (relI == relJ) { return iId < jId; } return relI > relJ; - } + } ///@notice helper function to exchange to bids in the heap function exchange(Queue storage self, uint256 i, uint256 j) private { uint96 tempId = self.bidIdList[i]; self.bidIdList[i] = self.bidIdList[j]; self.bidIdList[j] = tempId; - } -} \ No newline at end of file + } +} From 8ff6fc7a664514079e5a272b56469d77c44b461e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 12:48:41 +0400 Subject: [PATCH 021/117] Complete migration of lotId to uint96. Fix compilation errors. --- src/AuctionHouse.sol | 28 +++++---- src/bases/Auctioneer.sol | 34 +++++------ src/modules/Auction.sol | 2 +- src/modules/auctions/LSBBA/LSBBA.sol | 61 ++++++++++--------- src/modules/auctions/bases/BatchAuction.sol | 2 +- test/AuctionHouse/auction.t.sol | 12 ++-- test/AuctionHouse/cancel.t.sol | 2 +- test/AuctionHouse/purchase.t.sol | 2 +- .../Auction/MockAtomicAuctionModule.sol | 18 +++--- test/modules/Auction/MockAuctionModule.sol | 14 ++--- .../Auction/MockBatchAuctionModule.sol | 14 ++--- test/modules/Auction/cancel.t.sol | 12 ++-- 12 files changed, 98 insertions(+), 103 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 5a969c4f..4f7632e5 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -55,7 +55,7 @@ abstract contract Router is FeeManager { address recipient; address referrer; uint48 approvalDeadline; - uint256 lotId; + uint96 lotId; uint256 amount; uint256 minAmountOut; uint256 approvalNonce; @@ -344,7 +344,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { bytes calldata auctionData_, bytes calldata allowlistProof_, bytes calldata permit2Data_ - ) external override isValidLot(lotId_) { + ) external override isValidLot(lotId_) returns (uint256) { // Load routing data for the lot Routing memory routing = lotRouting[lotId_]; @@ -354,21 +354,23 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Transfer the quote token from the bidder - Permit2Approval memory permit2Approval = abi.decode(permit2Data_, (Permit2Approval)); - _collectPayment( - lotId_, - amount_, - routing.quoteToken, - routing.hooks, - permit2Approval.deadline, - permit2Approval.nonce, - permit2Approval.signature - ); + { + Permit2Approval memory permit2Approval = abi.decode(permit2Data_, (Permit2Approval)); + _collectPayment( + lotId_, + amount_, + routing.quoteToken, + routing.hooks, + permit2Approval.deadline, + permit2Approval.nonce, + permit2Approval.signature + ); + } // Record the bid on the auction module // The module will determine if the bid is valid - minimum bid size, minimum price, etc AuctionModule module = _getModuleForId(lotId_); - module.bid( + return module.bid( lotId_, recipient_, referrer_, diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 137c8258..b9eb6aaa 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -30,13 +30,13 @@ abstract contract Auctioneer is WithModules { // ========= ERRORS ========= // error InvalidParams(); - error InvalidLotId(uint256 id_); + error InvalidLotId(uint96 id_); error InvalidModuleType(Veecode reference_); error NotAuctionOwner(address caller_); // ========= EVENTS ========= // - event AuctionCreated(uint256 id, address baseToken, address quoteToken); + event AuctionCreated(uint96 id, address baseToken, address quoteToken); // ========= DATA STRUCTURES ========== // @@ -74,10 +74,10 @@ abstract contract Auctioneer is WithModules { uint48 internal constant _ONE_HUNDRED_PERCENT = 1e5; /// @notice Counter for auction lots - uint256 public lotCounter; + uint96 public lotCounter; /// @notice Mapping of lot IDs to their auction type (represented by the Keycode for the auction submodule) - mapping(uint256 lotId => Routing) public lotRouting; + mapping(uint96 lotId => Routing) public lotRouting; /// @notice Mapping auction and derivative references to the condenser that is used to pass data between them mapping(Veecode auctionRef => mapping(Veecode derivativeRef => Veecode condenserRef)) public @@ -89,7 +89,7 @@ abstract contract Auctioneer is WithModules { /// @dev Reverts if the lot ID is invalid /// /// @param lotId_ ID of the auction lot - modifier isValidLot(uint256 lotId_) { + modifier isValidLot(uint96 lotId_) { if (lotId_ >= lotCounter) revert InvalidLotId(lotId_); if (lotRouting[lotId_].owner == address(0)) revert InvalidLotId(lotId_); @@ -116,7 +116,7 @@ abstract contract Auctioneer is WithModules { function auction( RoutingParams calldata routing_, Auction.AuctionParams calldata params_ - ) external returns (uint256 lotId) { + ) external returns (uint96 lotId) { // Load auction type module, this checks that it is installed. // We load it here vs. later to avoid two checks. AuctionModule auctionModule = AuctionModule(_getLatestModuleIfActive(routing_.auctionType)); @@ -241,14 +241,14 @@ abstract contract Auctioneer is WithModules { /// - The respective auction module reverts /// /// @param lotId_ ID of the auction lot - function cancel(uint256 lotId_) external isValidLot(lotId_) { + function cancel(uint96 lotId_) external isValidLot(lotId_) { // Check that caller is the auction owner if (msg.sender != lotRouting[lotId_].owner) revert NotAuctionOwner(msg.sender); AuctionModule module = _getModuleForId(lotId_); // Cancel the auction on the module - module.cancel(lotId_); + module.cancelAuction(lotId_); } // ========== AUCTION INFORMATION ========== // @@ -259,48 +259,48 @@ abstract contract Auctioneer is WithModules { /// /// @param id_ ID of the auction lot /// @return routing Routing information for the auction lot - function getRouting(uint256 id_) external view isValidLot(id_) returns (Routing memory) { + function getRouting(uint96 id_) external view isValidLot(id_) returns (Routing memory) { // Get routing from lot routing return lotRouting[id_]; } // TODO need to add the fee calculations back in at this level for all of these functions - function payoutFor(uint256 id_, uint256 amount_) external view returns (uint256) { + function payoutFor(uint96 id_, uint256 amount_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get payout from module return module.payoutFor(id_, amount_); } - function priceFor(uint256 id_, uint256 payout_) external view returns (uint256) { + function priceFor(uint96 id_, uint256 payout_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get price from module return module.priceFor(id_, payout_); } - function maxPayout(uint256 id_) external view returns (uint256) { + function maxPayout(uint96 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get max payout from module return module.maxPayout(id_); } - function maxAmountAccepted(uint256 id_) external view returns (uint256) { + function maxAmountAccepted(uint96 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get max amount accepted from module return module.maxAmountAccepted(id_); } - function isLive(uint256 id_) external view returns (bool) { + function isLive(uint96 id_) external view returns (bool) { AuctionModule module = _getModuleForId(id_); // Get isLive from module return module.isLive(id_); } - function ownerOf(uint256 id_) external view returns (address) { + function ownerOf(uint96 id_) external view returns (address) { // Check that lot ID is valid if (id_ >= lotCounter) revert InvalidLotId(id_); @@ -308,7 +308,7 @@ abstract contract Auctioneer is WithModules { return lotRouting[id_].owner; } - function remainingCapacity(uint256 id_) external view returns (uint256) { + function remainingCapacity(uint96 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get remaining capacity from module @@ -323,7 +323,7 @@ abstract contract Auctioneer is WithModules { /// - The module for the auction type is not installed /// /// @param lotId_ ID of the auction lot - function _getModuleForId(uint256 lotId_) internal view returns (AuctionModule) { + function _getModuleForId(uint96 lotId_) internal view returns (AuctionModule) { // Confirm lot ID is valid if (lotId_ >= lotCounter) revert InvalidLotId(lotId_); diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index def91a47..aeb45111 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -93,7 +93,7 @@ abstract contract Auction { uint256 amount_, bytes calldata auctionData_, bytes calldata approval_ - ) external virtual; + ) external virtual returns (uint256 bidId); function cancelBid(uint96 lotId_, uint96 bidId_) external virtual; diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 7d4ae611..0ffaed92 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -87,21 +87,35 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { return Type.Auction; } - // =========== BID =========== // - function bid( - uint96 lotId_, - address recipient_, - address referrer_, - uint256 amount_, - bytes calldata auctionData_ - ) external onlyInternal returns (uint256 bidId) { + // ========== MODIFIERS ========== // + + modifier auctionIsLive(uint96 lotId_) { // Check that bids are allowed to be submitted for the lot if ( auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion ) revert Auction_NotLive(); + _; + } + modifier onlyBidder(address sender_, uint96 lotId_, uint96 bidId_) { + // Bid ID must be less than number of bids for lot + if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); + + // Check that sender is the bidder + if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); + _; + } + + // =========== BID =========== // + function bid( + uint96 lotId_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_ + ) external onlyInternal auctionIsLive(lotId_) returns (uint256 bidId) { // Validate inputs // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); @@ -123,21 +137,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { lotEncryptedBids[lotId_].push(userBid); } - function cancelBid(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { + function cancelBid( + uint96 lotId_, + uint96 bidId_, + address sender_ + ) external onlyInternal auctionIsLive(lotId_) onlyBidder(sender_, lotId_, bidId_) { // Validate inputs - // Auction for lot must still be live - if ( - auctionData[lotId_].status != AuctionStatus.Created - || block.timestamp < lotData[lotId_].start - || block.timestamp >= lotData[lotId_].conclusion - ) revert Auction_NotLive(); - - // Bid ID must be less than number of bids for lot - if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); - - // Sender must be bidder - if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); - // Bid is not already cancelled if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { revert Auction_AlreadyCancelled(); @@ -148,11 +153,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } // TODO need a top-level function on the Auction House that actually sends the funds to the recipient - function claimRefund(uint96 lotId_, uint96 bidId_, address sender_) external onlyInternal { + function claimRefund( + uint96 lotId_, + uint96 bidId_, + address sender_ + ) external onlyInternal onlyBidder(sender_, lotId_, bidId_) { // Validate inputs - // Sender must be bidder - if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); - // Auction for must have settled to claim refund // User must not have won the auction or claimed a refund already // TODO should we allow cancel bids to claim earlier? @@ -165,9 +171,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { || bidStatus == BidStatus.Won ) revert Auction_WrongState(); - // Bid ID must be less than number of bids for lot - if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); - // Set bid status to refunded lotEncryptedBids[lotId_][bidId_].status = BidStatus.Refunded; } diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 79966f8b..5efa5681 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -46,7 +46,7 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { uint256 amount_, bytes calldata auctionData_, bytes calldata approval_ - ) external override onlyParent { + ) external override onlyParent returns (uint256 bidId) { // TODO // Validate inputs diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index 501b0bc4..fdb63976 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -214,7 +214,7 @@ contract AuctionTest is Test, Permit2User { function test_success() external whenAuctionModuleIsInstalled { // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values ( @@ -252,7 +252,7 @@ contract AuctionTest is Test, Permit2User { routingParams.quoteToken = baseToken; // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values (,, ERC20 lotBaseToken, ERC20 lotQuoteToken,,,,,) = auctionHouse.lotRouting(lotId); @@ -330,7 +330,7 @@ contract AuctionTest is Test, Permit2User { whenDerivativeTypeIsSet { // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values (,,,,,, Veecode lotDerivativeType,,) = auctionHouse.lotRouting(lotId); @@ -351,7 +351,7 @@ contract AuctionTest is Test, Permit2User { routingParams.derivativeParams = abi.encode("derivative params"); // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values (,,,,,, Veecode lotDerivativeType, bytes memory lotDerivativeParams,) = @@ -412,7 +412,7 @@ contract AuctionTest is Test, Permit2User { routingParams.allowlist = mockAllowlist; // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values (,,,,, IAllowlist lotAllowlist,,,) = auctionHouse.lotRouting(lotId); @@ -467,7 +467,7 @@ contract AuctionTest is Test, Permit2User { routingParams.hooks = mockHook; // Create the auction - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values (,,,, IHooks lotHooks,,,,) = auctionHouse.lotRouting(lotId); diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancel.t.sol index 9354ef6a..795df7d0 100644 --- a/test/AuctionHouse/cancel.t.sol +++ b/test/AuctionHouse/cancel.t.sol @@ -35,7 +35,7 @@ contract CancelTest is Test, Permit2User { Auctioneer.RoutingParams internal routingParams; Auction.AuctionParams internal auctionParams; - uint256 internal lotId; + uint96 internal lotId; address internal auctionOwner = address(0x1); diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 4a82548e..8ee6f71d 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -51,7 +51,7 @@ contract PurchaseTest is Test, Permit2User { uint256 internal aliceKey; address internal alice; - uint256 internal lotId; + uint96 internal lotId; uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index c5251fed..a70fffb8 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -29,22 +29,16 @@ contract MockAtomicAuctionModule is AuctionModule { return Type.Auction; } - function _auction( - uint256, - Lot memory, - bytes memory - ) internal virtual override returns (uint256) { - return 0; - } + function _auction(uint96, Lot memory, bytes memory) internal virtual override {} - function _cancel(uint256 id_) internal override { + function _cancelAuction(uint96 id_) internal override { cancelled[id_] = true; } function purchase( uint256 id_, uint256 amount_, - bytes calldata auctionData_ + bytes calldata ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { if (purchaseReverts) revert("error"); @@ -76,7 +70,11 @@ contract MockAtomicAuctionModule is AuctionModule { uint256, bytes calldata, bytes calldata - ) external virtual override { + ) external virtual override returns (uint256) { + revert Auction_NotImplemented(); + } + + function cancelBid(uint96, uint96) external virtual override { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 3b6e837f..83adcec8 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -20,15 +20,9 @@ contract MockAuctionModule is AuctionModule { return Type.Auction; } - function _auction( - uint256, - Lot memory, - bytes memory - ) internal virtual override returns (uint256) { - return 0; - } + function _auction(uint96, Lot memory, bytes memory) internal virtual override {} - function _cancel(uint256 id_) internal override { + function _cancelAuction(uint96 id_) internal override { // } @@ -45,7 +39,7 @@ contract MockAuctionModule is AuctionModule { uint256 amount_, bytes calldata auctionData_, bytes calldata approval_ - ) external virtual override {} + ) external virtual override returns (uint256) {} function payoutFor( uint256 id_, @@ -67,6 +61,8 @@ contract MockAuctionModule is AuctionModule { bytes calldata settlementProof_, bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + + function cancelBid(uint96 lotId_, uint96 bidId_) external virtual override {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index a083ef91..1b49af85 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -20,15 +20,9 @@ contract MockBatchAuctionModule is AuctionModule { return Type.Auction; } - function _auction( - uint256, - Lot memory, - bytes memory - ) internal virtual override returns (uint256) { - return 0; - } + function _auction(uint96, Lot memory, bytes memory) internal virtual override {} - function _cancel(uint256 id_) internal override { + function _cancelAuction(uint96 id_) internal override { // } @@ -47,7 +41,9 @@ contract MockBatchAuctionModule is AuctionModule { uint256 amount_, bytes calldata auctionData_, bytes calldata approval_ - ) external virtual override {} + ) external virtual override returns (uint256) {} + + function cancelBid(uint96 lotId_, uint96 bidId_) external virtual override {} function settle( uint256 id_, diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index 49fe53cd..6b778045 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -35,7 +35,7 @@ contract CancelTest is Test, Permit2User { Auctioneer.RoutingParams internal routingParams; Auction.AuctionParams internal auctionParams; - uint256 internal lotId; + uint96 internal lotId; address internal auctionOwner = address(0x1); @@ -87,7 +87,7 @@ contract CancelTest is Test, Permit2User { bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); vm.expectRevert(err); - mockAuctionModule.cancel(lotId); + mockAuctionModule.cancelAuction(lotId); } function testReverts_whenLotIdInvalid() external { @@ -95,27 +95,27 @@ contract CancelTest is Test, Permit2User { vm.expectRevert(err); vm.prank(address(auctionHouse)); - mockAuctionModule.cancel(lotId); + mockAuctionModule.cancelAuction(lotId); } function testReverts_whenLotIsInactive() external whenLotIsCreated { // Cancel once vm.prank(address(auctionHouse)); - mockAuctionModule.cancel(lotId); + mockAuctionModule.cancelAuction(lotId); // Cancel again bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); vm.prank(address(auctionHouse)); - mockAuctionModule.cancel(lotId); + mockAuctionModule.cancelAuction(lotId); } function test_success() external whenLotIsCreated { assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); vm.prank(address(auctionHouse)); - mockAuctionModule.cancel(lotId); + mockAuctionModule.cancelAuction(lotId); // Get lot data from the module (, uint48 lotConclusion,, uint256 lotCapacity,,) = mockAuctionModule.lotData(lotId); From 114334acd3a5329ba4f092f4191f5f55d10c6f78 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 13:00:43 +0400 Subject: [PATCH 022/117] Fix stack too deep error --- src/AuctionHouse.sol | 72 +++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 4f7632e5..617252c1 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -192,9 +192,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _PERMIT2 = IPermit2(permit2_); } - // ========== DIRECT EXECUTION ========== // - - // ========== AUCTION FUNCTIONS ========== // + // ========== FEE FUNCTIONS ========== // function _allocateFees( address referrer_, @@ -211,6 +209,31 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return toReferrer + toProtocol; } + function _allocateFees( + Auction.Bid[] memory bids_, + ERC20 quoteToken_ + ) internal returns (uint256 totalAmountIn, uint256 totalFees) { + // Calculate fees for purchase + uint256 bidCount = bids_.length; + for (uint256 i; i < bidCount; i++) { + // Calculate fees from bid amount + (uint256 toReferrer, uint256 toProtocol) = + _calculateFees(bids_[i].referrer, bids_[i].amount); + + // Update referrer fee balances if non-zero and increment the total protocol fee + if (toReferrer > 0) { + rewards[bids_[i].referrer][quoteToken_] += toReferrer; + } + totalFees += toReferrer + toProtocol; + + // Increment total amount in + totalAmountIn += bids_[i].amount; + } + + // Update protocol fee if not zero + if (totalFees > 0) rewards[_PROTOCOL][quoteToken_] += totalFees; + } + function _calculateFees( address referrer_, uint256 amount_ @@ -237,6 +260,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } + // ========== AUCTION FUNCTIONS ========== // + /// @notice Determines if `caller_` is allowed to purchase/bid on a lot. /// If no allowlist is defined, this function will return true. /// @@ -409,32 +434,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Calculate fees - // TODO extract this to a function uint256 totalAmountInLessFees; - uint256 totalAmountOut; { - uint256 bidCount = winningBids_.length; - uint256 totalProtocolFee; - for (uint256 i; i < bidCount; i++) { - // No need to check if the bid amount is greater than the amount out because it is checked in `bid()` - - // Calculate fees from bid amount - (uint256 toReferrer, uint256 toProtocol) = - _calculateFees(winningBids_[i].referrer, winningBids_[i].amount); - - // Update referrer fee balances if non-zero and increment the total protocol fee - if (toReferrer > 0) { - rewards[winningBids_[i].referrer][routing.quoteToken] += toReferrer; - } - totalProtocolFee += toProtocol; - - // Increment total amount out - totalAmountInLessFees += winningBids_[i].amount - toReferrer - toProtocol; - totalAmountOut += amountsOut[i]; - } - - // Update protocol fee if not zero - if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; + (uint256 totalAmountIn, uint256 totalFees) = + _allocateFees(winningBids_, routing.quoteToken); + totalAmountInLessFees = totalAmountIn - totalFees; } // Assumes that payment has already been collected for each bid @@ -443,7 +447,19 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); // Collect payout in bulk from the auction owner - _collectPayout(lotId_, totalAmountInLessFees, totalAmountOut, routing); + { + // Calculate amount out + uint256 totalAmountOut; + { + uint256 bidCount = amountsOut.length; + for (uint256 i; i < bidCount; i++) { + // Increment total amount out + totalAmountOut += amountsOut[i]; + } + } + + _collectPayout(lotId_, totalAmountInLessFees, totalAmountOut, routing); + } // Handle payouts to bidders { From 681be7b28d7d439de20ad76e33adce5240059cff Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 13:47:19 +0400 Subject: [PATCH 023/117] Rename test file --- test/AuctionHouse/{cancel.t.sol => cancelAuction.t.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/AuctionHouse/{cancel.t.sol => cancelAuction.t.sol} (100%) diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancelAuction.t.sol similarity index 100% rename from test/AuctionHouse/cancel.t.sol rename to test/AuctionHouse/cancelAuction.t.sol From 5651472493f313bb71c36f2e0426c2e8555c6845 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 14:08:23 +0400 Subject: [PATCH 024/117] Revert "Rename test file" This reverts commit 681be7b28d7d439de20ad76e33adce5240059cff. --- test/AuctionHouse/{cancelAuction.t.sol => cancel.t.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/AuctionHouse/{cancelAuction.t.sol => cancel.t.sol} (100%) diff --git a/test/AuctionHouse/cancelAuction.t.sol b/test/AuctionHouse/cancel.t.sol similarity index 100% rename from test/AuctionHouse/cancelAuction.t.sol rename to test/AuctionHouse/cancel.t.sol From 214a6f5c53e659fa16795c0c0173e39af5b5ffd8 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 16:22:45 +0400 Subject: [PATCH 025/117] Tests for bid() --- src/AuctionHouse.sol | 153 ++++---- src/modules/Auction.sol | 8 +- src/modules/auctions/LSBBA/LSBBA.sol | 9 +- src/modules/auctions/bases/BatchAuction.sol | 1 + test/AuctionHouse/MockAuctionHouse.sol | 16 +- test/AuctionHouse/bid.t.sol | 355 ++++++++++++++++++ test/AuctionHouse/purchase.t.sol | 41 +- .../Auction/MockAtomicAuctionModule.sol | 3 +- test/modules/Auction/MockAuctionModule.sol | 3 +- .../Auction/MockBatchAuctionModule.sol | 37 +- 10 files changed, 507 insertions(+), 119 deletions(-) create mode 100644 test/AuctionHouse/bid.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 617252c1..254c96dc 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -43,25 +43,41 @@ abstract contract Router is FeeManager { /// /// @param recipient Address to receive payout /// @param referrer Address of referrer - /// @param approvalDeadline Deadline for Permit2 approval signature /// @param lotId Lot ID /// @param amount Amount of quoteToken to purchase with (in native decimals) /// @param minAmountOut Minimum amount of baseToken to receive - /// @param approvalNonce Nonce for permit Permit2 approval signature /// @param auctionData Custom data used by the auction module - /// @param approvalSignature Permit2 approval signature for the quoteToken /// @param allowlistProof Proof of allowlist inclusion + /// @param permit2Data_ Permit2 approval for the quoteToken struct PurchaseParams { address recipient; address referrer; - uint48 approvalDeadline; uint96 lotId; uint256 amount; uint256 minAmountOut; - uint256 approvalNonce; bytes auctionData; - bytes approvalSignature; bytes allowlistProof; + bytes permit2Data; + } + + /// @notice Parameters used by the bid function + /// @dev This reduces the number of variables in scope for the bid function + /// + /// @param lotId Lot ID + /// @param recipient Address to receive payout + /// @param referrer Address of referrer + /// @param amount Amount of quoteToken to purchase with (in native decimals) + /// @param auctionData Custom data used by the auction module + /// @param allowlistProof Proof of allowlist inclusion + /// @param permit2Data_ Permit2 approval for the quoteToken (abi-encoded Permit2Approval struct) + struct BidParams { + uint96 lotId; + address recipient; + address referrer; + uint256 amount; + bytes auctionData; + bytes allowlistProof; + bytes permit2Data; } // ========== STATE VARIABLES ========== // @@ -111,22 +127,9 @@ abstract contract Router is FeeManager { /// 2. Store the bid /// 3. Transfer the amount of quote token from the bidder /// - /// @param lotId_ Lot ID - /// @param recipient_ Address to receive payout - /// @param referrer_ Address of referrer - /// @param amount_ Amount of quoteToken to purchase with (in native decimals) - /// @param auctionData_ Custom data used by the auction module - /// @param allowlistProof_ Allowlist proof - /// @param permit2Data_ Permit2 data - function bid( - uint96 lotId_, - address recipient_, - address referrer_, - uint256 amount_, - bytes calldata auctionData_, // sequential hash of bids, minimum amount out encrypted with auction public key - bytes calldata allowlistProof_, - bytes calldata permit2Data_ - ) external virtual returns (uint256 bidId); + /// @param params_ Bid parameters + /// @return bidId Bid ID + function bid(BidParams memory params_) external virtual returns (uint256 bidId); /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement @@ -333,15 +336,14 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { if (payoutAmount < params_.minAmountOut) revert AmountLessThanMinimum(); // Collect payment from the purchaser - _collectPayment( - params_.lotId, - params_.amount, - routing.quoteToken, - routing.hooks, - params_.approvalDeadline, - params_.approvalNonce, - params_.approvalSignature - ); + { + Permit2Approval memory permit2Approval = params_.permit2Data.length == 0 + ? Permit2Approval({nonce: 0, deadline: 0, signature: bytes("")}) + : abi.decode(params_.permit2Data, (Permit2Approval)); + _collectPayment( + params_.lotId, params_.amount, routing.quoteToken, routing.hooks, permit2Approval + ); + } // Send payment to auction owner _sendPayment(routing.owner, amountLessFees, routing.quoteToken, routing.hooks); @@ -356,53 +358,50 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { emit Purchase(params_.lotId, msg.sender, params_.referrer, params_.amount, payoutAmount); } - // TODO need a delegated execution function for purchase and bid because we check allowlist on the caller in the normal functions - // ========== BATCH AUCTIONS ========== // /// @inheritdoc Router - function bid( - uint96 lotId_, - address recipient_, - address referrer_, - uint256 amount_, - bytes calldata auctionData_, - bytes calldata allowlistProof_, - bytes calldata permit2Data_ - ) external override isValidLot(lotId_) returns (uint256) { + function bid(BidParams memory params_) + external + override + isValidLot(params_.lotId) + returns (uint256) + { // Load routing data for the lot - Routing memory routing = lotRouting[lotId_]; + Routing memory routing = lotRouting[params_.lotId]; // Determine if the bidder is authorized to bid - if (!_isAllowed(routing.allowlist, lotId_, msg.sender, allowlistProof_)) { + if (!_isAllowed(routing.allowlist, params_.lotId, msg.sender, params_.allowlistProof)) { revert InvalidBidder(msg.sender); } + // Record the bid on the auction module + // The module will determine if the bid is valid - minimum bid size, minimum price, auction status, etc + uint256 bidId; + { + AuctionModule module = _getModuleForId(params_.lotId); + bidId = module.bid( + params_.lotId, + msg.sender, + params_.recipient, + params_.referrer, + params_.amount, + params_.auctionData, + bytes("") // TODO approval param + ); + } + // Transfer the quote token from the bidder { - Permit2Approval memory permit2Approval = abi.decode(permit2Data_, (Permit2Approval)); + Permit2Approval memory permit2Approval = params_.permit2Data.length == 0 + ? Permit2Approval({nonce: 0, deadline: 0, signature: bytes("")}) + : abi.decode(params_.permit2Data, (Permit2Approval)); _collectPayment( - lotId_, - amount_, - routing.quoteToken, - routing.hooks, - permit2Approval.deadline, - permit2Approval.nonce, - permit2Approval.signature + params_.lotId, params_.amount, routing.quoteToken, routing.hooks, permit2Approval ); } - // Record the bid on the auction module - // The module will determine if the bid is valid - minimum bid size, minimum price, etc - AuctionModule module = _getModuleForId(lotId_); - return module.bid( - lotId_, - recipient_, - referrer_, - amount_, - auctionData_, - bytes("") // TODO approval param - ); + return bidId; } /// @inheritdoc Router @@ -595,17 +594,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param amount_ Amount of quoteToken to collect (in native decimals) /// @param quoteToken_ Quote token to collect /// @param hooks_ Hooks contract to call (optional) - /// @param approvalDeadline_ Deadline for Permit2 approval signature - /// @param approvalNonce_ Nonce for Permit2 approval signature - /// @param approvalSignature_ Permit2 approval signature for the quoteToken + /// @param permit2Approval_ Permit2 approval data (optional) function _collectPayment( uint256 lotId_, uint256 amount_, ERC20 quoteToken_, IHooks hooks_, - uint48 approvalDeadline_, - uint256 approvalNonce_, - bytes memory approvalSignature_ + Permit2Approval memory permit2Approval_ ) internal { // Call pre hook on hooks contract if provided if (address(hooks_) != address(0)) { @@ -613,10 +608,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // If a Permit2 approval signature is provided, use it to transfer the quote token - if (approvalSignature_.length != 0) { - _permit2Transfer( - amount_, quoteToken_, approvalDeadline_, approvalNonce_, approvalSignature_ - ); + if (permit2Approval_.signature.length != 0) { + _permit2Transfer(amount_, quoteToken_, permit2Approval_); } // Otherwise fallback to a standard ERC20 transfer else { @@ -818,15 +811,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// /// @param amount_ Amount of tokens to transfer (in native decimals) /// @param token_ Token to transfer - /// @param approvalDeadline_ Deadline for Permit2 approval signature - /// @param approvalNonce_ Nonce for Permit2 approval signature - /// @param approvalSignature_ Permit2 approval signature for the token + /// @param permit2Approval_ Permit2 approval data function _permit2Transfer( uint256 amount_, ERC20 token_, - uint48 approvalDeadline_, - uint256 approvalNonce_, - bytes memory approvalSignature_ + Permit2Approval memory permit2Approval_ ) internal { uint256 balanceBefore = token_.balanceOf(address(this)); @@ -834,12 +823,12 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _PERMIT2.permitTransferFrom( IPermit2.PermitTransferFrom( IPermit2.TokenPermissions(address(token_), amount_), - approvalNonce_, - approvalDeadline_ + permit2Approval_.nonce, + permit2Approval_.deadline ), IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: amount_}), msg.sender, // Spender of the tokens - approvalSignature_ + permit2Approval_.signature ); // Check that it is not a fee-on-transfer token diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index aeb45111..b692a0f0 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -6,13 +6,13 @@ import {Module} from "src/modules/Modules.sol"; abstract contract Auction { /* ========== ERRORS ========== */ - error Auction_MarketNotActive(uint256 lotId); + error Auction_MarketNotActive(uint96 lotId); error Auction_InvalidStart(uint48 start_, uint48 minimum_); error Auction_InvalidDuration(uint48 duration_, uint48 minimum_); - error Auction_InvalidLotId(uint256 lotId); + error Auction_InvalidLotId(uint96 lotId); error Auction_OnlyMarketOwner(); error Auction_AmountLessThanMinimum(); @@ -71,7 +71,7 @@ abstract contract Auction { // ========== ATOMIC AUCTIONS ========== // function purchase( - uint256 id_, + uint96 id_, uint256 amount_, bytes calldata auctionData_ ) external virtual returns (uint256 payout, bytes memory auctionOutput); @@ -81,6 +81,7 @@ abstract contract Auction { /// @notice Bid on an auction lot /// /// @param lotId_ The lot id + /// @param bidder_ The bidder of the purchased tokens /// @param recipient_ The recipient of the purchased tokens /// @param referrer_ The referrer of the bid /// @param amount_ The amount of quote tokens to bid @@ -88,6 +89,7 @@ abstract contract Auction { /// @param approval_ The user approval data function bid( uint96 lotId_, + address bidder_, address recipient_, address referrer_, uint256 amount_, diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 0ffaed92..69d5431e 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -109,13 +109,16 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } // =========== BID =========== // + function bid( uint96 lotId_, + address bidder_, address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_ - ) external onlyInternal auctionIsLive(lotId_) returns (uint256 bidId) { + bytes calldata auctionData_, + bytes calldata approval_ + ) external override onlyInternal auctionIsLive(lotId_) returns (uint256 bidId) { // Validate inputs // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); @@ -123,7 +126,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Store bid data // Auction data should just be the encrypted amount out (no decoding required) EncryptedBid memory userBid; - userBid.bidder = msg.sender; + userBid.bidder = bidder_; userBid.recipient = recipient_; userBid.referrer = referrer_; userBid.amount = amount_; diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 5efa5681..63a4552e 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -41,6 +41,7 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { /// @inheritdoc Auction function bid( uint96 lotId_, + address bidder_, address recipient_, address referrer_, uint256 amount_, diff --git a/test/AuctionHouse/MockAuctionHouse.sol b/test/AuctionHouse/MockAuctionHouse.sol index 0db87252..decfe966 100644 --- a/test/AuctionHouse/MockAuctionHouse.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -23,15 +23,13 @@ contract MockAuctionHouse is AuctionHouse { uint256 approvalNonce_, bytes memory approvalSignature_ ) external { - return _collectPayment( - lotId_, - amount_, - quoteToken_, - hooks_, - approvalDeadline_, - approvalNonce_, - approvalSignature_ - ); + Permit2Approval memory approval = Permit2Approval({ + deadline: approvalDeadline_, + nonce: approvalNonce_, + signature: approvalSignature_ + }); + + return _collectPayment(lotId_, amount_, quoteToken_, hooks_, approval); } function sendPayment( diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol new file mode 100644 index 00000000..bb307e04 --- /dev/null +++ b/test/AuctionHouse/bid.t.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; + +// Mocks +import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; +import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; +import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; + +// Auctions +import {AuctionHouse, Router} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; + +// Modules +import { + Keycode, + toKeycode, + Veecode, + wrapVeecode, + unwrapVeecode, + fromVeecode, + WithModules, + Module +} from "src/modules/Modules.sol"; + +contract BidTest is Test, Permit2User { + MockERC20 internal baseToken; + MockERC20 internal quoteToken; + MockBatchAuctionModule internal mockAuctionModule; + + AuctionHouse internal auctionHouse; + + uint96 internal lotId; + uint48 internal auctionDuration = 1 days; + + address internal immutable protocol = address(0x2); + address internal immutable referrer = address(0x4); + address internal immutable auctionOwner = address(0x5); + address internal immutable recipient = address(0x6); + + uint256 internal aliceKey; + address internal alice; + + uint256 internal constant BID_AMOUNT = 1e18; + + // Function parameters (can be modified) + Auctioneer.RoutingParams internal routingParams; + Auction.AuctionParams internal auctionParams; + Router.BidParams internal bidParams; + + bytes internal auctionData; + bytes internal allowlistProof; + bytes internal permit2Data; + + function setUp() external { + // Set block timestamp + vm.warp(1_000_000); + + aliceKey = _getRandomUint256(); + alice = vm.addr(aliceKey); + + baseToken = new MockERC20("Base Token", "BASE", 18); + quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + + auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); + mockAuctionModule = new MockBatchAuctionModule(address(auctionHouse)); + + auctionHouse.installModule(mockAuctionModule); + + auctionParams = Auction.AuctionParams({ + start: uint48(block.timestamp), + duration: auctionDuration, + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode("") + }); + + routingParams = Auctioneer.RoutingParams({ + auctionType: toKeycode("BATCH"), + baseToken: baseToken, + quoteToken: quoteToken, + hooks: IHooks(address(0)), + allowlist: IAllowlist(address(0)), + allowlistParams: abi.encode(""), + payoutData: abi.encode(""), + derivativeType: toKeycode(""), + derivativeParams: abi.encode("") + }); + + bidParams = Router.BidParams({ + lotId: lotId, + recipient: recipient, + referrer: referrer, + amount: BID_AMOUNT, + auctionData: auctionData, + allowlistProof: allowlistProof, + permit2Data: permit2Data + }); + } + + modifier givenLotIsCreated() { + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenLotIsAtomicAuction() { + // Install the atomic auction module + MockAtomicAuctionModule mockAtomicAuctionModule = + new MockAtomicAuctionModule(address(auctionHouse)); + auctionHouse.installModule(mockAtomicAuctionModule); + + // Update routing parameters + (Keycode moduleKeycode,) = unwrapVeecode(mockAtomicAuctionModule.VEECODE()); + routingParams.auctionType = moduleKeycode; + + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + + // Update bid parameters + bidParams.lotId = lotId; + _; + } + + modifier givenLotIsCancelled() { + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + _; + } + + modifier givenLotIsConcluded() { + vm.warp(block.timestamp + auctionDuration + 1); + _; + } + + modifier whenLotIdIsInvalid() { + lotId = 255; + + // Update bid parameters + bidParams.lotId = lotId; + _; + } + + modifier givenLotHasAllowlist() { + MockAllowlist allowlist = new MockAllowlist(); + routingParams.allowlist = allowlist; + + // Register a new auction with an allowlist + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + + // Add the sender to the allowlist + allowlist.setAllowedWithProof(alice, allowlistProof, true); + + // Update bid parameters + bidParams.lotId = lotId; + _; + } + + modifier withIncorrectAllowlistProof() { + allowlistProof = abi.encode("incorrect proof"); + + // Update bid parameters + bidParams.allowlistProof = allowlistProof; + _; + } + + modifier givenUserHasQuoteTokenBalance(uint256 amount_) { + quoteToken.mint(alice, amount_); + _; + } + + modifier givenUserHasApprovedQuoteToken(uint256 amount_) { + vm.prank(alice); + quoteToken.approve(address(auctionHouse), amount_); + _; + } + + modifier whenPermit2ApprovalIsProvided() { + // Approve the Permit2 contract to spend the quote token + vm.prank(alice); + quoteToken.approve(_PERMIT2_ADDRESS, type(uint256).max); + + // Set up the Permit2 approval + uint48 deadline = uint48(block.timestamp); + uint256 nonce = _getRandomUint256(); + bytes memory signature = _signPermit( + address(quoteToken), BID_AMOUNT, nonce, deadline, address(auctionHouse), aliceKey + ); + + permit2Data = abi.encode( + Router.Permit2Approval({deadline: deadline, nonce: nonce, signature: signature}) + ); + + // Update bid parameters + bidParams.permit2Data = permit2Data; + _; + } + + // bid + // [X] given the auction is atomic + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] given the auction is cancelled + // [X] it reverts + // [X] given the auction is concluded + // [X] it reverts + // [X] given the auction has an allowlist + // [X] reverts if the sender is not on the allowlist + // [X] it succeeds + // [X] given the user does not have sufficient balance of the quote token + // [X] it reverts + // [X] when Permit2 approval is provided + // [X] it transfers the tokens from the sender using Permit2 + // [X] when Permit2 approval is not provided + // [X] it transfers the tokens from the sender + // [X] it records the bid + + function test_givenAtomicAuction_reverts() external givenLotIsAtomicAuction { + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_whenLotIdIsInvalid_reverts() external givenLotIsCreated whenLotIdIsInvalid { + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_givenLotIsCancelled_reverts() external givenLotIsCreated givenLotIsCancelled { + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_givenLotIsConcluded_reverts() external givenLotIsCreated givenLotIsConcluded { + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_incorrectAllowlistProof_reverts() + external + givenLotIsCreated + givenLotHasAllowlist + withIncorrectAllowlistProof + { + bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidBidder.selector, alice); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_givenLotHasAllowlist() + external + givenLotIsCreated + givenLotHasAllowlist + givenUserHasQuoteTokenBalance(BID_AMOUNT) + givenUserHasApprovedQuoteToken(BID_AMOUNT) + { + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_givenUserHasInsufficientBalance_reverts() + public + givenLotIsCreated + givenUserHasApprovedQuoteToken(BID_AMOUNT) + { + vm.expectRevert("TRANSFER_FROM_FAILED"); + + // Call the function + vm.prank(alice); + auctionHouse.bid(bidParams); + } + + function test_whenPermit2ApprovalIsProvided() + external + givenLotIsCreated + givenUserHasQuoteTokenBalance(BID_AMOUNT) + whenPermit2ApprovalIsProvided + { + // Call the function + vm.prank(alice); + uint256 bidId = auctionHouse.bid(bidParams); + + // Check the balances + assertEq(quoteToken.balanceOf(alice), 0, "alice: quote token balance mismatch"); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + BID_AMOUNT, + "auction house: quote token balance mismatch" + ); + + // Check the bid + Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); + assertEq(bid.bidder, alice, "bidder mismatch"); + assertEq(bid.recipient, recipient, "recipient mismatch"); + assertEq(bid.referrer, referrer, "referrer mismatch"); + assertEq(bid.amount, BID_AMOUNT, "amount mismatch"); + assertEq(bid.minAmountOut, 0, "minAmountOut mismatch"); + assertEq(bid.auctionParam, bytes32(auctionData), "auctionParam mismatch"); + } + + function test_whenPermit2ApprovalIsNotProvided() + external + givenLotIsCreated + givenUserHasQuoteTokenBalance(BID_AMOUNT) + givenUserHasApprovedQuoteToken(BID_AMOUNT) + { + // Call the function + vm.prank(alice); + uint256 bidId = auctionHouse.bid(bidParams); + + // Check the balances + assertEq(quoteToken.balanceOf(alice), 0, "alice: quote token balance mismatch"); + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + BID_AMOUNT, + "auction house: quote token balance mismatch" + ); + + // Check the bid + Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); + assertEq(bid.bidder, alice, "bidder mismatch"); + assertEq(bid.recipient, recipient, "recipient mismatch"); + assertEq(bid.referrer, referrer, "referrer mismatch"); + assertEq(bid.amount, BID_AMOUNT, "amount mismatch"); + assertEq(bid.minAmountOut, 0, "minAmountOut mismatch"); + assertEq(bid.auctionParam, bytes32(auctionData), "auctionParam mismatch"); + } +} diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 8ee6f71d..e99d48ef 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -114,9 +114,6 @@ contract PurchaseTest is Test, Permit2User { vm.prank(auctionOwner); lotId = auctionHouse.auction(routingParams, auctionParams); - approvalNonce = _getRandomUint256(); - approvalDeadline = uint48(block.timestamp) + 1 days; - // Fees referrerFee = 1000; protocolFee = 2000; @@ -134,14 +131,12 @@ contract PurchaseTest is Test, Permit2User { purchaseParams = Router.PurchaseParams({ recipient: recipient, referrer: referrer, - approvalDeadline: approvalDeadline, lotId: lotId, amount: AMOUNT_IN, minAmountOut: AMOUNT_OUT, - approvalNonce: approvalNonce, auctionData: bytes(""), - approvalSignature: approvalSignature, - allowlistProof: allowlistProof + allowlistProof: allowlistProof, + permit2Data: bytes("") }); } @@ -390,15 +385,10 @@ contract PurchaseTest is Test, Permit2User { // [X] when the permit2 signature is not provided // [X] it succeeds using ERC20 transfer - function test_whenPermit2Signature() - external - givenUserHasQuoteTokenBalance(AMOUNT_IN) - givenOwnerHasBaseTokenBalance(AMOUNT_OUT) - givenBaseTokenSpendingIsApproved - givenQuoteTokenPermit2IsApproved - { - // Set the permit2 signature - purchaseParams.approvalSignature = _signPermit( + modifier whenPermit2DataIsProvided() { + approvalNonce = _getRandomUint256(); + approvalDeadline = uint48(block.timestamp) + 1 days; + approvalSignature = _signPermit( address(quoteToken), AMOUNT_IN, approvalNonce, @@ -407,6 +397,25 @@ contract PurchaseTest is Test, Permit2User { aliceKey ); + // Update parameters + purchaseParams.permit2Data = abi.encode( + Router.Permit2Approval({ + deadline: approvalDeadline, + nonce: approvalNonce, + signature: approvalSignature + }) + ); + _; + } + + function test_whenPermit2Signature() + external + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenOwnerHasBaseTokenBalance(AMOUNT_OUT) + givenBaseTokenSpendingIsApproved + givenQuoteTokenPermit2IsApproved + whenPermit2DataIsProvided + { // Purchase vm.prank(alice); auctionHouse.purchase(purchaseParams); diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index a70fffb8..073bacc5 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -36,7 +36,7 @@ contract MockAtomicAuctionModule is AuctionModule { } function purchase( - uint256 id_, + uint96 id_, uint256 amount_, bytes calldata ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { @@ -67,6 +67,7 @@ contract MockAtomicAuctionModule is AuctionModule { uint96, address, address, + address, uint256, bytes calldata, bytes calldata diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 83adcec8..11c2a5c3 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -27,13 +27,14 @@ contract MockAuctionModule is AuctionModule { } function purchase( - uint256 id_, + uint96 id_, uint256 amount_, bytes calldata auctionData_ ) external virtual override returns (uint256 payout, bytes memory auctionOutput) {} function bid( uint96 id_, + address bidder_, address recipient_, address referrer_, uint256 amount_, diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 1b49af85..5398f1c9 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -5,9 +5,11 @@ pragma solidity 0.8.19; import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; // Auctions -import {AuctionModule} from "src/modules/Auction.sol"; +import {Auction, AuctionModule} from "src/modules/Auction.sol"; contract MockBatchAuctionModule is AuctionModule { + mapping(uint96 lotId => Bid[]) public bidData; + constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; } @@ -27,7 +29,7 @@ contract MockBatchAuctionModule is AuctionModule { } function purchase( - uint256, + uint96, uint256, bytes calldata ) external virtual override returns (uint256, bytes memory) { @@ -35,13 +37,36 @@ contract MockBatchAuctionModule is AuctionModule { } function bid( - uint96 id_, + uint96 lotId_, + address bidder_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_, bytes calldata approval_ - ) external virtual override returns (uint256) {} + ) external virtual override returns (uint256) { + // Valid lot + if (lotData[lotId_].start == 0) revert Auction.Auction_InvalidLotId(lotId_); + + // If auction is cancelled + if (isLive(lotId_) == false) revert Auction.Auction_MarketNotActive(lotId_); + + // Create a new bid + Bid memory newBid = Bid({ + bidder: bidder_, + recipient: recipient_, + referrer: referrer_, + amount: amount_, + minAmountOut: 0, + auctionParam: bytes32("") // TODO fix this + }); + + uint256 bidId = bidData[lotId_].length; + + bidData[lotId_].push(newBid); + + return bidId; + } function cancelBid(uint96 lotId_, uint96 bidId_) external virtual override {} @@ -70,4 +95,8 @@ contract MockBatchAuctionModule is AuctionModule { bytes calldata settlementProof_, bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + + function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { + bid_ = bidData[lotId_][bidId_]; + } } From 9df2923442438ce239bb68ddfc0dd18c84c457b7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 16:31:50 +0400 Subject: [PATCH 026/117] Adjust type for auctionParam --- src/modules/Auction.sol | 2 +- test/AuctionHouse/bid.t.sol | 19 +++++++++++++++++-- .../Auction/MockBatchAuctionModule.sol | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index b692a0f0..12232a37 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -46,7 +46,7 @@ abstract contract Auction { address referrer; uint256 amount; uint256 minAmountOut; - bytes32 auctionParam; // optional implementation-specific parameter for the bid + bytes auctionParam; // optional implementation-specific parameter for the bid } struct AuctionParams { diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol index bb307e04..f209e232 100644 --- a/test/AuctionHouse/bid.t.sol +++ b/test/AuctionHouse/bid.t.sol @@ -322,7 +322,7 @@ contract BidTest is Test, Permit2User { assertEq(bid.referrer, referrer, "referrer mismatch"); assertEq(bid.amount, BID_AMOUNT, "amount mismatch"); assertEq(bid.minAmountOut, 0, "minAmountOut mismatch"); - assertEq(bid.auctionParam, bytes32(auctionData), "auctionParam mismatch"); + assertEq(bid.auctionParam, auctionData, "auctionParam mismatch"); } function test_whenPermit2ApprovalIsNotProvided() @@ -350,6 +350,21 @@ contract BidTest is Test, Permit2User { assertEq(bid.referrer, referrer, "referrer mismatch"); assertEq(bid.amount, BID_AMOUNT, "amount mismatch"); assertEq(bid.minAmountOut, 0, "minAmountOut mismatch"); - assertEq(bid.auctionParam, bytes32(auctionData), "auctionParam mismatch"); + assertEq(bid.auctionParam, auctionData, "auctionParam mismatch"); + } + + function test_whenAuctionParamIsProvided() external givenLotIsCreated givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { + auctionData = abi.encode("auction data"); + + // Update bid parameters + bidParams.auctionData = auctionData; + + // Call the function + vm.prank(alice); + uint256 bidId = auctionHouse.bid(bidParams); + + // Check the bid + Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); + assertEq(bid.auctionParam, auctionData, "auctionParam mismatch"); } } diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 5398f1c9..3d2a8a75 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -58,7 +58,7 @@ contract MockBatchAuctionModule is AuctionModule { referrer: referrer_, amount: amount_, minAmountOut: 0, - auctionParam: bytes32("") // TODO fix this + auctionParam: auctionData_ }); uint256 bidId = bidData[lotId_].length; From bbb1f80ea621e158e006fd363a4a2fdb59b614b5 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 16:33:39 +0400 Subject: [PATCH 027/117] Documentation --- src/AuctionHouse.sol | 5 +++++ test/AuctionHouse/bid.t.sol | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 254c96dc..a1112d07 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -361,6 +361,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== BATCH AUCTIONS ========== // /// @inheritdoc Router + /// @dev This function reverts if: + /// - lotId is invalid + /// - the bidder is not on the optional allowlist + /// - the auction module reverts when creating a bid + /// - the quote token transfer fails function bid(BidParams memory params_) external override diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol index f209e232..223b2d78 100644 --- a/test/AuctionHouse/bid.t.sol +++ b/test/AuctionHouse/bid.t.sol @@ -353,7 +353,12 @@ contract BidTest is Test, Permit2User { assertEq(bid.auctionParam, auctionData, "auctionParam mismatch"); } - function test_whenAuctionParamIsProvided() external givenLotIsCreated givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { + function test_whenAuctionParamIsProvided() + external + givenLotIsCreated + givenUserHasQuoteTokenBalance(BID_AMOUNT) + givenUserHasApprovedQuoteToken(BID_AMOUNT) + { auctionData = abi.encode("auction data"); // Update bid parameters From 11803e3e8691476d8ad33717ed2fcbd139e2c0dd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 16:41:46 +0400 Subject: [PATCH 028/117] Document behaviour --- src/AuctionHouse.sol | 12 ++++++------ src/modules/Auction.sol | 13 ++++++++++++- src/modules/auctions/LSBBA/LSBBA.sol | 8 +++++--- test/modules/Auction/MockAtomicAuctionModule.sol | 2 +- test/modules/Auction/MockAuctionModule.sol | 2 +- test/modules/Auction/MockBatchAuctionModule.sol | 2 +- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index a1112d07..c1a7ba4f 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -82,10 +82,10 @@ abstract contract Router is FeeManager { // ========== STATE VARIABLES ========== // - /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). - /// @dev There are some situations where the fees may round down to zero if quantity of baseToken - /// is < 1e5 wei (can happen with big price differences on small decimal tokens). This is purely - /// a theoretical edge case, as the bond amount would not be practical. + /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). + /// @dev There are some situations where the fees may round down to zero if quantity of baseToken + /// is < 1e5 wei (can happen with big price differences on small decimal tokens). This is purely + /// a theoretical edge case, as the bond amount would not be practical. mapping(address => uint48) public referrerFees; // TODO allow charging fees based on the auction type and/or derivative type @@ -122,7 +122,7 @@ abstract contract Router is FeeManager { // ========== BATCH AUCTIONS ========== // /// @notice Bid on a lot in a batch auction - /// @notice The implementing function must perform the following: + /// @dev The implementing function must perform the following: /// 1. Validate the bid /// 2. Store the bid /// 3. Transfer the amount of quote token from the bidder @@ -134,7 +134,7 @@ abstract contract Router is FeeManager { /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement /// - /// @notice The implementing function must perform the following: + /// @dev The implementing function must perform the following: /// 1. Validate that the caller is authorized to settle the auction /// 2. Calculate fees /// 3. Pass the bids to the auction module to validate the settlement diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 12232a37..c646bcca 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -79,6 +79,9 @@ abstract contract Auction { // ========== BATCH AUCTIONS ========== // /// @notice Bid on an auction lot + /// @dev The implementing function should handle the following: + /// - Validate the bid parameters + /// - Store the bid data /// /// @param lotId_ The lot id /// @param bidder_ The bidder of the purchased tokens @@ -97,7 +100,15 @@ abstract contract Auction { bytes calldata approval_ ) external virtual returns (uint256 bidId); - function cancelBid(uint96 lotId_, uint96 bidId_) external virtual; + /// @notice Cancel a bid + /// @dev The implementing function should handle the following: + /// - Validate the bid parameters + /// - Update the bid data + /// + /// @param lotId_ The lot id + /// @param bidId_ The bid id + /// @param bidder_ The bidder of the purchased tokens + function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual; /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 69d5431e..d0572903 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.19; // import "src/modules/auctions/bases/BatchAuction.sol"; -import {AuctionModule} from "src/modules/Auction.sol"; +import {Auction, AuctionModule} from "src/modules/Auction.sol"; import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; @@ -110,6 +110,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== BID =========== // + /// @inheritdoc Auction function bid( uint96 lotId_, address bidder_, @@ -140,11 +141,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { lotEncryptedBids[lotId_].push(userBid); } + /// @inheritdoc Auction function cancelBid( uint96 lotId_, uint96 bidId_, - address sender_ - ) external onlyInternal auctionIsLive(lotId_) onlyBidder(sender_, lotId_, bidId_) { + address bidder_ + ) external override onlyInternal auctionIsLive(lotId_) onlyBidder(bidder_, lotId_, bidId_) { // Validate inputs // Bid is not already cancelled if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 073bacc5..ca7d0589 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -75,7 +75,7 @@ contract MockAtomicAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function cancelBid(uint96, uint96) external virtual override { + function cancelBid(uint96, uint96, address) external virtual override { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 11c2a5c3..0e000fa5 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -63,7 +63,7 @@ contract MockAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function cancelBid(uint96 lotId_, uint96 bidId_) external virtual override {} + function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual override {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 3d2a8a75..dcd00c42 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -68,7 +68,7 @@ contract MockBatchAuctionModule is AuctionModule { return bidId; } - function cancelBid(uint96 lotId_, uint96 bidId_) external virtual override {} + function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual override {} function settle( uint256 id_, From da6dbe69ac6d11d4cdeac9929f4e9027b5af62ae Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 16:56:09 +0400 Subject: [PATCH 029/117] Add stub for claimRefund(). Consistent type for bidId. --- src/AuctionHouse.sol | 22 ++++++++++++++++++- src/modules/Auction.sol | 12 +++++++++- src/modules/auctions/LSBBA/LSBBA.sol | 10 ++++----- .../Auction/MockAtomicAuctionModule.sol | 8 ++++++- test/modules/Auction/MockAuctionModule.sol | 8 ++++++- .../Auction/MockBatchAuctionModule.sol | 16 +++++++++++--- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index c1a7ba4f..0bec1aba 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -153,7 +153,15 @@ abstract contract Router is FeeManager { bytes calldata settlementData_ ) external virtual; - // TODO bid refunds + /// @notice Claims a refund for a failed or cancelled bid + /// @dev The implementing function must perform the following: + /// 1. Validate that the `lotId_` is valid + /// 2. Pass the request to the auction module to validate and update data + /// 3. Send the refund to the bidder + /// + /// @param lotId_ Lot ID + /// @param bidId_ Bid ID + function claimRefund(uint96 lotId_, uint256 bidId_) external virtual; // ========== FEE MANAGEMENT ========== // @@ -410,6 +418,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } /// @inheritdoc Router + /// @dev This function reverts if: + /// - the lot ID is invalid + /// - the caller is not authorized to settle the auction + /// - the auction module reverts when settling the auction + /// - transferring the quote token to the auction owner fails + /// - collecting the payout from the auction owner fails + /// - sending the payout to each bidder fails function settle( uint96 lotId_, Auction.Bid[] calldata winningBids_, @@ -475,6 +490,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } + /// @inheritdoc Router + function claimRefund(uint96 lotId_, uint256 bidId_) external override isValidLot(lotId_) { + // + } + // // External submission and evaluation // function settle(uint256 id_, ExternalSettlement memory settlement_) external override { // // Load routing data for the lot diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index c646bcca..b37a05ca 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -108,7 +108,17 @@ abstract contract Auction { /// @param lotId_ The lot id /// @param bidId_ The bid id /// @param bidder_ The bidder of the purchased tokens - function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual; + function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual; + + /// @notice Claim a refund for a bid + /// @dev The implementing function should handle the following: + /// - Validate the bid parameters + /// - Update the bid data + /// + /// @param lotId_ The lot id + /// @param bidId_ The bid id + /// @param bidder_ The bidder of the purchased tokens + function claimRefund(uint96 lotId_, uint256 bidId_, address bidder_) external virtual; /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index d0572903..2c9b1250 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -99,7 +99,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { _; } - modifier onlyBidder(address sender_, uint96 lotId_, uint96 bidId_) { + modifier onlyBidder(address sender_, uint96 lotId_, uint256 bidId_) { // Bid ID must be less than number of bids for lot if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); @@ -144,7 +144,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { /// @inheritdoc Auction function cancelBid( uint96 lotId_, - uint96 bidId_, + uint256 bidId_, address bidder_ ) external override onlyInternal auctionIsLive(lotId_) onlyBidder(bidder_, lotId_, bidId_) { // Validate inputs @@ -157,12 +157,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; } - // TODO need a top-level function on the Auction House that actually sends the funds to the recipient + /// @inheritdoc Auction function claimRefund( uint96 lotId_, - uint96 bidId_, + uint256 bidId_, address sender_ - ) external onlyInternal onlyBidder(sender_, lotId_, bidId_) { + ) external override onlyInternal onlyBidder(sender_, lotId_, bidId_) { // Validate inputs // Auction for must have settled to claim refund // User must not have won the auction or claimed a refund already diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index ca7d0589..1452af98 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -75,7 +75,7 @@ contract MockAtomicAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function cancelBid(uint96, uint96, address) external virtual override { + function cancelBid(uint96, uint256, address) external virtual override { revert Auction_NotImplemented(); } @@ -104,4 +104,10 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementProof_, bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + + function claimRefund( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual override {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 0e000fa5..9cffbf97 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -63,7 +63,13 @@ contract MockAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual override {} + function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual override {} + + function claimRefund( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual override {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index dcd00c42..25b07784 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -46,10 +46,14 @@ contract MockBatchAuctionModule is AuctionModule { bytes calldata approval_ ) external virtual override returns (uint256) { // Valid lot - if (lotData[lotId_].start == 0) revert Auction.Auction_InvalidLotId(lotId_); + if (lotData[lotId_].start == 0) { + revert Auction.Auction_InvalidLotId(lotId_); + } // If auction is cancelled - if (isLive(lotId_) == false) revert Auction.Auction_MarketNotActive(lotId_); + if (isLive(lotId_) == false) { + revert Auction.Auction_MarketNotActive(lotId_); + } // Create a new bid Bid memory newBid = Bid({ @@ -68,7 +72,7 @@ contract MockBatchAuctionModule is AuctionModule { return bidId; } - function cancelBid(uint96 lotId_, uint96 bidId_, address bidder_) external virtual override {} + function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual override {} function settle( uint256 id_, @@ -99,4 +103,10 @@ contract MockBatchAuctionModule is AuctionModule { function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } + + function claimRefund( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual override {} } From 09e0e245a7649d7a4e1b91cf4545e9363519d65c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 22 Jan 2024 17:31:10 +0400 Subject: [PATCH 030/117] Test for cancelBid() --- src/AuctionHouse.sol | 16 ++ src/modules/Auction.sol | 6 + src/modules/auctions/LSBBA/LSBBA.sol | 1 - test/AuctionHouse/cancelBid.t.sol | 260 +++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 test/AuctionHouse/cancelBid.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 0bec1aba..97ad955f 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -131,6 +131,15 @@ abstract contract Router is FeeManager { /// @return bidId Bid ID function bid(BidParams memory params_) external virtual returns (uint256 bidId); + /// @notice Cancel a bid on a lot in a batch auction + /// @dev The implementing function must perform the following: + /// 1. Validate the bid + /// 2. Call the auction module to cancel the bid + /// + /// @param lotId_ Lot ID + /// @param bidId_ Bid ID + function cancelBid(uint96 lotId_, uint256 bidId_) external virtual; + /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement /// @@ -417,6 +426,13 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { return bidId; } + /// @inheritdoc Router + function cancelBid(uint96 lotId_, uint256 bidId_) external override isValidLot(lotId_) { + // Cancel the bid on the auction module + AuctionModule module = _getModuleForId(lotId_); + module.cancelBid(lotId_, bidId_, msg.sender); + } + /// @inheritdoc Router /// @dev This function reverts if: /// - the lot ID is invalid diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index b37a05ca..75edef12 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -14,6 +14,8 @@ abstract contract Auction { error Auction_InvalidLotId(uint96 lotId); + error Auction_InvalidBidId(uint256 bidId); + error Auction_OnlyMarketOwner(); error Auction_AmountLessThanMinimum(); error Auction_NotEnoughCapacity(); @@ -21,6 +23,8 @@ abstract contract Auction { error Auction_NotAuthorized(); error Auction_NotImplemented(); + error Auction_NotBidder(); + /* ========== EVENTS ========== */ event AuctionCreated( @@ -103,6 +107,7 @@ abstract contract Auction { /// @notice Cancel a bid /// @dev The implementing function should handle the following: /// - Validate the bid parameters + /// - Authorize `bidder_` /// - Update the bid data /// /// @param lotId_ The lot id @@ -113,6 +118,7 @@ abstract contract Auction { /// @notice Claim a refund for a bid /// @dev The implementing function should handle the following: /// - Validate the bid parameters + /// - Authorize `bidder_` /// - Update the bid data /// /// @param lotId_ The lot id diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 2c9b1250..886cfa06 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -18,7 +18,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== ERRORS ========== // error Auction_BidDoesNotExist(); - error Auction_NotBidder(); error Auction_AlreadyCancelled(); error Auction_WrongState(); error Auction_NotLive(); diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol new file mode 100644 index 00000000..476b14e1 --- /dev/null +++ b/test/AuctionHouse/cancelBid.t.sol @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; + +// Mocks +import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; +import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; +import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; + +// Auctions +import {AuctionHouse, Router} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; + +// Modules +import { + Keycode, + toKeycode, + Veecode, + wrapVeecode, + unwrapVeecode, + fromVeecode, + WithModules, + Module +} from "src/modules/Modules.sol"; + +contract CancelBidTest is Test, Permit2User { + MockERC20 internal baseToken; + MockERC20 internal quoteToken; + MockBatchAuctionModule internal mockAuctionModule; + + AuctionHouse internal auctionHouse; + + uint48 internal auctionDuration = 1 days; + + address internal immutable protocol = address(0x2); + address internal immutable referrer = address(0x4); + address internal immutable auctionOwner = address(0x5); + address internal immutable recipient = address(0x6); + address internal immutable alice = address(0x7); + + uint256 internal constant BID_AMOUNT = 1e18; + + Auctioneer.RoutingParams internal routingParams; + Auction.AuctionParams internal auctionParams; + + // Function parameters (can be modified) + uint96 internal lotId; + uint256 internal bidId; + + function setUp() external { + // Set block timestamp + vm.warp(1_000_000); + + baseToken = new MockERC20("Base Token", "BASE", 18); + quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + + auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); + mockAuctionModule = new MockBatchAuctionModule(address(auctionHouse)); + + auctionHouse.installModule(mockAuctionModule); + + auctionParams = Auction.AuctionParams({ + start: uint48(block.timestamp), + duration: auctionDuration, + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode("") + }); + + routingParams = Auctioneer.RoutingParams({ + auctionType: toKeycode("BATCH"), + baseToken: baseToken, + quoteToken: quoteToken, + hooks: IHooks(address(0)), + allowlist: IAllowlist(address(0)), + allowlistParams: abi.encode(""), + payoutData: abi.encode(""), + derivativeType: toKeycode(""), + derivativeParams: abi.encode("") + }); + } + + modifier givenLotIsCreated() { + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenLotIsAtomicAuction() { + // Install the atomic auction module + MockAtomicAuctionModule mockAtomicAuctionModule = + new MockAtomicAuctionModule(address(auctionHouse)); + auctionHouse.installModule(mockAtomicAuctionModule); + + // Update routing parameters + (Keycode moduleKeycode,) = unwrapVeecode(mockAtomicAuctionModule.VEECODE()); + routingParams.auctionType = moduleKeycode; + + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenLotIsCancelled() { + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + _; + } + + modifier givenLotIsConcluded() { + vm.warp(block.timestamp + auctionDuration + 1); + _; + } + + modifier whenLotIdIsInvalid() { + lotId = 255; + _; + } + + modifier givenBidIsCreated() { + // Mint quote tokens to alice + quoteToken.mint(alice, BID_AMOUNT); + + // Approve spending + quoteToken.approve(address(auctionHouse), BID_AMOUNT); + + // Create the bid + Router.BidParams memory bidParams = Router.BidParams({ + lotId: lotId, + recipient: recipient, + referrer: referrer, + amount: BID_AMOUNT, + auctionData: bytes(""), + allowlistProof: bytes(""), + permit2Data: bytes("") + }); + + vm.prank(alice); + auctionHouse.bid(bidParams); + _; + } + + modifier givenBidIsCancelled() { + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + _; + } + + // cancelBid + // [X] given the auction lot does not exist + // [X] it reverts + // [X] given the auction lot is not an atomic auction + // [X] it reverts + // [X] given the auction lot is cancelled + // [X] it reverts + // [X] given the auction lot is concluded + // [X] it reverts + // [X] given the bid does not exist + // [X] it reverts + // [X] given the bid is already cancelled + // [X] it reverts + // [X] given the caller is not the bid owner + // [X] it reverts + // [ ] it cancels the bid + + function test_invalidLotId_reverts() external { + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_invalidAuctionType_reverts() external givenLotIsAtomicAuction { + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_lotCancelled_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenLotIsCancelled + { + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_lotConcluded_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenLotIsConcluded + { + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_givenBidDoesNotExist_reverts() external givenLotIsCreated { + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, bidId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_givenBidCancelled_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenBidIsCancelled + { + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, bidId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_givenCallerIsNotBidOwner_reverts() external givenLotIsCreated givenBidIsCreated { + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); + vm.expectRevert(err); + + // Call the function + vm.prank(auctionOwner); + auctionHouse.cancelBid(lotId, bidId); + } + + function test_itCancelsTheBid() external givenLotIsCreated givenBidIsCreated { + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + + // Assert the bid is cancelled + Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); + // assertTrue(bid.cancelled); + // TODO cancelled status + } +} From 84183c655b07801f14c15342ebf920aa2dcbac95 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 10:42:56 +0400 Subject: [PATCH 031/117] Remove redundant parameter to bid() --- src/AuctionHouse.sol | 3 +-- src/modules/Auction.sol | 4 +--- src/modules/auctions/LSBBA/LSBBA.sol | 3 +-- src/modules/auctions/bases/BatchAuction.sol | 3 +-- test/modules/Auction/MockAtomicAuctionModule.sol | 1 - test/modules/Auction/MockAuctionModule.sol | 3 +-- test/modules/Auction/MockBatchAuctionModule.sol | 3 +-- 7 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 97ad955f..36d12e82 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -408,8 +408,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { params_.recipient, params_.referrer, params_.amount, - params_.auctionData, - bytes("") // TODO approval param + params_.auctionData ); } diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 75edef12..a603d96b 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -93,15 +93,13 @@ abstract contract Auction { /// @param referrer_ The referrer of the bid /// @param amount_ The amount of quote tokens to bid /// @param auctionData_ The auction-specific data - /// @param approval_ The user approval data function bid( uint96 lotId_, address bidder_, address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, - bytes calldata approval_ + bytes calldata auctionData_ ) external virtual returns (uint256 bidId); /// @notice Cancel a bid diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 886cfa06..633a8ca0 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -116,8 +116,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, - bytes calldata approval_ + bytes calldata auctionData_ ) external override onlyInternal auctionIsLive(lotId_) returns (uint256 bidId) { // Validate inputs // Amount at least minimum bid size for lot diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 63a4552e..18964aa8 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -45,8 +45,7 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, - bytes calldata approval_ + bytes calldata auctionData_ ) external override onlyParent returns (uint256 bidId) { // TODO // Validate inputs diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 1452af98..6371889b 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -69,7 +69,6 @@ contract MockAtomicAuctionModule is AuctionModule { address, address, uint256, - bytes calldata, bytes calldata ) external virtual override returns (uint256) { revert Auction_NotImplemented(); diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 9cffbf97..614bb142 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -38,8 +38,7 @@ contract MockAuctionModule is AuctionModule { address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, - bytes calldata approval_ + bytes calldata auctionData_ ) external virtual override returns (uint256) {} function payoutFor( diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 25b07784..3edb9172 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -42,8 +42,7 @@ contract MockBatchAuctionModule is AuctionModule { address recipient_, address referrer_, uint256 amount_, - bytes calldata auctionData_, - bytes calldata approval_ + bytes calldata auctionData_ ) external virtual override returns (uint256) { // Valid lot if (lotData[lotId_].start == 0) { From adf0c55f38a02c15a84f56e2fe6d9617f95f5364 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:01:09 +0400 Subject: [PATCH 032/117] Finish tests for cancelBid() --- src/AuctionHouse.sol | 4 +++ src/modules/Auction.sol | 18 ++++++++++++- test/AuctionHouse/cancelBid.t.sol | 7 ++++-- .../Auction/MockBatchAuctionModule.sol | 25 ++++++++++++++++++- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 6b32db3e..1fa595c2 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -426,8 +426,12 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } /// @inheritdoc Router + /// @dev This function reverts if: + /// - the lot ID is invalid + /// - the auction module reverts when cancelling the bid function cancelBid(uint96 lotId_, uint256 bidId_) external override isValidLot(lotId_) { // Cancel the bid on the auction module + // The auction module is responsible for validating the bid and authorizing the caller AuctionModule module = _getModuleForId(lotId_); module.cancelBid(lotId_, bidId_, msg.sender); } diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a603d96b..1a506ec5 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -14,7 +14,7 @@ abstract contract Auction { error Auction_InvalidLotId(uint96 lotId); - error Auction_InvalidBidId(uint256 bidId); + error Auction_InvalidBidId(uint96 lotId, uint256 bidId); error Auction_OnlyMarketOwner(); error Auction_AmountLessThanMinimum(); @@ -247,4 +247,20 @@ abstract contract AuctionModule is Auction, Module { function remainingCapacity(uint256 id_) external view override returns (uint256) { return lotData[id_].capacity; } + + // ========== MODIFIERS ========== // + + modifier isLotValid(uint96 lotId_) { + if (lotData[lotId_].start == 0) revert Auction_InvalidLotId(lotId_); + _; + } + + modifier isLotActive(uint96 lotId_) { + Lot memory lot = lotData[lotId_]; + if ( + lot.capacity == 0 || lot.conclusion < uint48(block.timestamp) + || lot.start > block.timestamp + ) revert Auction_MarketNotActive(lotId_); + _; + } } diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index 476b14e1..eb25961b 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -128,6 +128,7 @@ contract CancelBidTest is Test, Permit2User { quoteToken.mint(alice, BID_AMOUNT); // Approve spending + vm.prank(alice); quoteToken.approve(address(auctionHouse), BID_AMOUNT); // Create the bid @@ -216,7 +217,8 @@ contract CancelBidTest is Test, Permit2User { } function test_givenBidDoesNotExist_reverts() external givenLotIsCreated { - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, bidId); + bytes memory err = + abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); vm.expectRevert(err); // Call the function @@ -230,7 +232,8 @@ contract CancelBidTest is Test, Permit2User { givenBidIsCreated givenBidIsCancelled { - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, bidId); + bytes memory err = + abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); vm.expectRevert(err); // Call the function diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 3edb9172..89559f83 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -9,6 +9,7 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; contract MockBatchAuctionModule is AuctionModule { mapping(uint96 lotId => Bid[]) public bidData; + mapping(uint96 lotId => mapping(uint256 => bool)) public bidCancelled; constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; @@ -71,7 +72,29 @@ contract MockBatchAuctionModule is AuctionModule { return bidId; } - function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual override {} + function cancelBid( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual override isLotValid(lotId_) isLotActive(lotId_) { + // Check that the bid exists + if (bidData[lotId_].length <= bidId_) { + revert Auction.Auction_InvalidBidId(lotId_, bidId_); + } + + // Check that the bid has not been cancelled + if (bidCancelled[lotId_][bidId_] == true) { + revert Auction.Auction_InvalidBidId(lotId_, bidId_); + } + + // Check that the bidder is the owner of the bid + if (bidData[lotId_][bidId_].bidder != bidder_) { + revert Auction.Auction_NotBidder(); + } + + // Cancel the bid + bidCancelled[lotId_][bidId_] = true; + } function settle( uint256 id_, From f578982676b388b33db8cfa853d095f19c08a263 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:08:31 +0400 Subject: [PATCH 033/117] Modifiers --- src/AuctionHouse.sol | 10 +++++----- src/bases/Auctioneer.sol | 18 ++++++++++++------ src/modules/Auction.sol | 8 ++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 1fa595c2..2cf50d43 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -326,7 +326,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { function purchase(PurchaseParams memory params_) external override - isValidLot(params_.lotId) + isLotValid(params_.lotId) returns (uint256 payoutAmount) { // Load routing data for the lot @@ -386,7 +386,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { function bid(BidParams memory params_) external override - isValidLot(params_.lotId) + isLotValid(params_.lotId) returns (uint256) { // Load routing data for the lot @@ -429,7 +429,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @dev This function reverts if: /// - the lot ID is invalid /// - the auction module reverts when cancelling the bid - function cancelBid(uint96 lotId_, uint256 bidId_) external override isValidLot(lotId_) { + function cancelBid(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { // Cancel the bid on the auction module // The auction module is responsible for validating the bid and authorizing the caller AuctionModule module = _getModuleForId(lotId_); @@ -449,7 +449,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { Auction.Bid[] calldata winningBids_, bytes calldata settlementProof_, bytes calldata settlementData_ - ) external override isValidLot(lotId_) { + ) external override isLotValid(lotId_) { // Load routing data for the lot Routing memory routing = lotRouting[lotId_]; @@ -510,7 +510,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } /// @inheritdoc Router - function claimRefund(uint96 lotId_, uint256 bidId_) external override isValidLot(lotId_) { + function claimRefund(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { // } diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index b9eb6aaa..06ce6c0d 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -89,13 +89,22 @@ abstract contract Auctioneer is WithModules { /// @dev Reverts if the lot ID is invalid /// /// @param lotId_ ID of the auction lot - modifier isValidLot(uint96 lotId_) { + modifier isLotValid(uint96 lotId_) { if (lotId_ >= lotCounter) revert InvalidLotId(lotId_); if (lotRouting[lotId_].owner == address(0)) revert InvalidLotId(lotId_); _; } + /// @notice Checks that the caller is the auction owner + /// @dev Reverts if the caller is not the auction owner + /// + /// @param lotId_ ID of the auction lot + modifier isLotOwner(uint96 lotId_) { + if (msg.sender != lotRouting[lotId_].owner) revert NotAuctionOwner(msg.sender); + _; + } + // ========== AUCTION MANAGEMENT ========== // /// @notice Creates a new auction lot @@ -241,10 +250,7 @@ abstract contract Auctioneer is WithModules { /// - The respective auction module reverts /// /// @param lotId_ ID of the auction lot - function cancel(uint96 lotId_) external isValidLot(lotId_) { - // Check that caller is the auction owner - if (msg.sender != lotRouting[lotId_].owner) revert NotAuctionOwner(msg.sender); - + function cancel(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { AuctionModule module = _getModuleForId(lotId_); // Cancel the auction on the module @@ -259,7 +265,7 @@ abstract contract Auctioneer is WithModules { /// /// @param id_ ID of the auction lot /// @return routing Routing information for the auction lot - function getRouting(uint96 id_) external view isValidLot(id_) returns (Routing memory) { + function getRouting(uint96 id_) external view isLotValid(id_) returns (Routing memory) { // Get routing from lot routing return lotRouting[id_]; } diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 1a506ec5..4409e9fd 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -250,11 +250,19 @@ abstract contract AuctionModule is Auction, Module { // ========== MODIFIERS ========== // + /// @notice Checks that the lot ID is valid + /// @dev Reverts if the lot ID is invalid + /// + /// @param lotId_ The lot identifier modifier isLotValid(uint96 lotId_) { if (lotData[lotId_].start == 0) revert Auction_InvalidLotId(lotId_); _; } + /// @notice Checks that the lot is active + /// @dev Reverts if the lot is not active + /// + /// @param lotId_ The lot identifier modifier isLotActive(uint96 lotId_) { Lot memory lot = lotData[lotId_]; if ( From 5b9a305da453d7cfd20d6caa93fa9a232154797a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:35:23 +0400 Subject: [PATCH 034/117] Prevent auction lot cancellation when active. Fixes tests. --- src/modules/Auction.sol | 15 ++++++----- test/AuctionHouse/bid.t.sol | 20 +++++++++++++-- test/AuctionHouse/cancel.t.sol | 22 ++++++++++++---- test/AuctionHouse/cancelBid.t.sol | 41 +++++++++++++++--------------- test/AuctionHouse/purchase.t.sol | 8 +++++- test/modules/Auction/auction.t.sol | 2 +- test/modules/Auction/cancel.t.sol | 18 ++++++++++--- 7 files changed, 87 insertions(+), 39 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 4409e9fd..a83501a7 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -8,6 +8,8 @@ abstract contract Auction { error Auction_MarketNotActive(uint96 lotId); + error Auction_MarketActive(uint96 lotId); + error Auction_InvalidStart(uint48 start_, uint48 minimum_); error Auction_InvalidDuration(uint48 duration_, uint48 minimum_); @@ -213,18 +215,19 @@ abstract contract AuctionModule is Auction, Module { /// @dev This function reverts if: /// - the caller is not the parent of the module /// - the lot id is invalid - /// - the lot is not active + /// - the lot has already been cancelled + /// - the lot is active /// /// @param lotId_ The lot id - function cancelAuction(uint96 lotId_) external override onlyParent { + function cancelAuction(uint96 lotId_) external override onlyParent isLotValid(lotId_) { Lot storage lot = lotData[lotId_]; - // Invalid lot - if (lot.start == 0) revert Auction_InvalidLotId(lotId_); - - // Inactive lot + // Already cancelled if (lot.capacity == 0) revert Auction_MarketNotActive(lotId_); + // Already started + if (lot.start <= block.timestamp) revert Auction_MarketActive(lotId_); + lot.conclusion = uint48(block.timestamp); lot.capacity = 0; diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol index 223b2d78..4bb9b42c 100644 --- a/test/AuctionHouse/bid.t.sol +++ b/test/AuctionHouse/bid.t.sol @@ -74,7 +74,7 @@ contract BidTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), + start: uint48(block.timestamp) + 1, duration: auctionDuration, capacityInQuote: false, capacity: 10e18, @@ -134,6 +134,11 @@ contract BidTest is Test, Permit2User { _; } + modifier givenLotHasStarted() { + vm.warp(auctionParams.start); + _; + } + modifier givenLotIsConcluded() { vm.warp(block.timestamp + auctionDuration + 1); _; @@ -232,7 +237,12 @@ contract BidTest is Test, Permit2User { auctionHouse.bid(bidParams); } - function test_whenLotIdIsInvalid_reverts() external givenLotIsCreated whenLotIdIsInvalid { + function test_whenLotIdIsInvalid_reverts() + external + givenLotIsCreated + givenLotHasStarted + whenLotIdIsInvalid + { bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); vm.expectRevert(err); @@ -262,6 +272,7 @@ contract BidTest is Test, Permit2User { function test_incorrectAllowlistProof_reverts() external givenLotIsCreated + givenLotHasStarted givenLotHasAllowlist withIncorrectAllowlistProof { @@ -276,6 +287,7 @@ contract BidTest is Test, Permit2User { function test_givenLotHasAllowlist() external givenLotIsCreated + givenLotHasStarted givenLotHasAllowlist givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) @@ -288,6 +300,7 @@ contract BidTest is Test, Permit2User { function test_givenUserHasInsufficientBalance_reverts() public givenLotIsCreated + givenLotHasStarted givenUserHasApprovedQuoteToken(BID_AMOUNT) { vm.expectRevert("TRANSFER_FROM_FAILED"); @@ -300,6 +313,7 @@ contract BidTest is Test, Permit2User { function test_whenPermit2ApprovalIsProvided() external givenLotIsCreated + givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) whenPermit2ApprovalIsProvided { @@ -328,6 +342,7 @@ contract BidTest is Test, Permit2User { function test_whenPermit2ApprovalIsNotProvided() external givenLotIsCreated + givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { @@ -356,6 +371,7 @@ contract BidTest is Test, Permit2User { function test_whenAuctionParamIsProvided() external givenLotIsCreated + givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancel.t.sol index 795df7d0..26fb6eb4 100644 --- a/test/AuctionHouse/cancel.t.sol +++ b/test/AuctionHouse/cancel.t.sol @@ -37,9 +37,9 @@ contract CancelTest is Test, Permit2User { uint96 internal lotId; - address internal auctionOwner = address(0x1); + address internal immutable auctionOwner = address(0x1); - address internal protocol = address(0x2); + address internal immutable protocol = address(0x2); function setUp() external { baseToken = new MockERC20("Base Token", "BASE", 18); @@ -51,7 +51,7 @@ contract CancelTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), + start: uint48(block.timestamp + 1), // start in 1 second, so we can cancel duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -110,19 +110,31 @@ contract CancelTest is Test, Permit2User { } function testReverts_whenLotIsInactive() external whenLotIsCreated { + // Cancel once vm.prank(auctionOwner); auctionHouse.cancel(lotId); bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); + // Cancel again vm.prank(auctionOwner); auctionHouse.cancel(lotId); } - function test_success() external whenLotIsCreated { - assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); + function test_givenLotHasStarted_reverts() external whenLotIsCreated { + // Warp beyond the start time + vm.warp(uint48(block.timestamp + 1)); + + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + + // Cancel + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + } + function test_success() external whenLotIsCreated { vm.prank(auctionOwner); auctionHouse.cancel(lotId); diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index eb25961b..b7c058c6 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -66,7 +66,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), + start: uint48(block.timestamp) + 1, duration: auctionDuration, capacityInQuote: false, capacity: 10e18, @@ -113,6 +113,11 @@ contract CancelBidTest is Test, Permit2User { _; } + modifier givenLotHasStarted() { + vm.warp(auctionParams.start); + _; + } + modifier givenLotIsConcluded() { vm.warp(block.timestamp + auctionDuration + 1); _; @@ -158,8 +163,6 @@ contract CancelBidTest is Test, Permit2User { // [X] it reverts // [X] given the auction lot is not an atomic auction // [X] it reverts - // [X] given the auction lot is cancelled - // [X] it reverts // [X] given the auction lot is concluded // [X] it reverts // [X] given the bid does not exist @@ -188,23 +191,10 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_lotCancelled_reverts() - external - givenLotIsCreated - givenBidIsCreated - givenLotIsCancelled - { - bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); - vm.expectRevert(err); - - // Call the function - vm.prank(alice); - auctionHouse.cancelBid(lotId, bidId); - } - function test_lotConcluded_reverts() external givenLotIsCreated + givenLotHasStarted givenBidIsCreated givenLotIsConcluded { @@ -216,7 +206,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_givenBidDoesNotExist_reverts() external givenLotIsCreated { + function test_givenBidDoesNotExist_reverts() external givenLotIsCreated givenLotHasStarted { bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); vm.expectRevert(err); @@ -229,6 +219,7 @@ contract CancelBidTest is Test, Permit2User { function test_givenBidCancelled_reverts() external givenLotIsCreated + givenLotHasStarted givenBidIsCreated givenBidIsCancelled { @@ -241,7 +232,12 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_givenCallerIsNotBidOwner_reverts() external givenLotIsCreated givenBidIsCreated { + function test_givenCallerIsNotBidOwner_reverts() + external + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated + { bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); vm.expectRevert(err); @@ -250,7 +246,12 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_itCancelsTheBid() external givenLotIsCreated givenBidIsCreated { + function test_itCancelsTheBid() + external + givenLotIsCreated + givenLotHasStarted + givenBidIsCreated + { // Call the function vm.prank(alice); auctionHouse.cancelBid(lotId, bidId); diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 138956f5..31afe84e 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -90,7 +90,7 @@ contract PurchaseTest is Test, Permit2User { mockHook = new MockHook(address(quoteToken), address(baseToken)); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), + start: uint48(block.timestamp) + 1, duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -140,6 +140,9 @@ contract PurchaseTest is Test, Permit2User { allowlistProof: allowlistProof, permit2Data: bytes("") }); + + // Warp to the start of the auction + vm.warp(auctionParams.start); } modifier givenDerivativeModuleIsInstalled() { @@ -195,6 +198,9 @@ contract PurchaseTest is Test, Permit2User { } modifier givenAuctionIsCancelled() { + // Warp to before the auction start + vm.warp(auctionParams.start - 1); + vm.prank(auctionOwner); auctionHouse.cancel(lotId); _; diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index 9443c0d9..4d739c49 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -26,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract AuctionTest is Test, Permit2User { +contract AuctionModuleAuctionTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index 6b778045..98028196 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -26,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract CancelTest is Test, Permit2User { +contract AuctionModuleCancelTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -51,7 +51,7 @@ contract CancelTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), + start: uint48(block.timestamp) + 1, duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -111,9 +111,19 @@ contract CancelTest is Test, Permit2User { mockAuctionModule.cancelAuction(lotId); } - function test_success() external whenLotIsCreated { - assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); + function test_givenLotIsActive_reverts() external whenLotIsCreated { + // Warp to the start of the auction + vm.warp(auctionParams.start); + + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + // Cancel once + vm.prank(address(auctionHouse)); + mockAuctionModule.cancelAuction(lotId); + } + + function test_success() external whenLotIsCreated { vm.prank(address(auctionHouse)); mockAuctionModule.cancelAuction(lotId); From 9e5c17c0fda7e9c83ac76aa83db953deb6c76f5b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:38:12 +0400 Subject: [PATCH 035/117] Completes tests for cancelBid --- test/AuctionHouse/cancelBid.t.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index b7c058c6..f066f680 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -171,7 +171,7 @@ contract CancelBidTest is Test, Permit2User { // [X] it reverts // [X] given the caller is not the bid owner // [X] it reverts - // [ ] it cancels the bid + // [X] it cancels the bid function test_invalidLotId_reverts() external { bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); @@ -257,8 +257,6 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); // Assert the bid is cancelled - Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); - // assertTrue(bid.cancelled); - // TODO cancelled status + assertTrue(mockAuctionModule.bidCancelled(lotId, bidId)); } } From 740a2100087c8604b0eec5074913cac04a45d1bd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:41:05 +0400 Subject: [PATCH 036/117] Rename function --- src/AuctionHouse.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 2cf50d43..c86f121e 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -170,7 +170,7 @@ abstract contract Router is FeeManager { /// /// @param lotId_ Lot ID /// @param bidId_ Bid ID - function claimRefund(uint96 lotId_, uint256 bidId_) external virtual; + function claimBidRefund(uint96 lotId_, uint256 bidId_) external virtual; // ========== FEE MANAGEMENT ========== // @@ -510,7 +510,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } /// @inheritdoc Router - function claimRefund(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { + function claimBidRefund(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { // } From 5182254648ec3550761299f5e882faa053225681 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:48:03 +0400 Subject: [PATCH 037/117] Revert "Prevent auction lot cancellation when active. Fixes tests." This reverts commit 5b9a305da453d7cfd20d6caa93fa9a232154797a. --- src/modules/Auction.sol | 15 +++++------ test/AuctionHouse/bid.t.sol | 20 ++------------- test/AuctionHouse/cancel.t.sol | 22 ++++------------ test/AuctionHouse/cancelBid.t.sol | 41 +++++++++++++++--------------- test/AuctionHouse/purchase.t.sol | 8 +----- test/modules/Auction/auction.t.sol | 2 +- test/modules/Auction/cancel.t.sol | 18 +++---------- 7 files changed, 39 insertions(+), 87 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a83501a7..4409e9fd 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -8,8 +8,6 @@ abstract contract Auction { error Auction_MarketNotActive(uint96 lotId); - error Auction_MarketActive(uint96 lotId); - error Auction_InvalidStart(uint48 start_, uint48 minimum_); error Auction_InvalidDuration(uint48 duration_, uint48 minimum_); @@ -215,18 +213,17 @@ abstract contract AuctionModule is Auction, Module { /// @dev This function reverts if: /// - the caller is not the parent of the module /// - the lot id is invalid - /// - the lot has already been cancelled - /// - the lot is active + /// - the lot is not active /// /// @param lotId_ The lot id - function cancelAuction(uint96 lotId_) external override onlyParent isLotValid(lotId_) { + function cancelAuction(uint96 lotId_) external override onlyParent { Lot storage lot = lotData[lotId_]; - // Already cancelled - if (lot.capacity == 0) revert Auction_MarketNotActive(lotId_); + // Invalid lot + if (lot.start == 0) revert Auction_InvalidLotId(lotId_); - // Already started - if (lot.start <= block.timestamp) revert Auction_MarketActive(lotId_); + // Inactive lot + if (lot.capacity == 0) revert Auction_MarketNotActive(lotId_); lot.conclusion = uint48(block.timestamp); lot.capacity = 0; diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol index 4bb9b42c..223b2d78 100644 --- a/test/AuctionHouse/bid.t.sol +++ b/test/AuctionHouse/bid.t.sol @@ -74,7 +74,7 @@ contract BidTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp) + 1, + start: uint48(block.timestamp), duration: auctionDuration, capacityInQuote: false, capacity: 10e18, @@ -134,11 +134,6 @@ contract BidTest is Test, Permit2User { _; } - modifier givenLotHasStarted() { - vm.warp(auctionParams.start); - _; - } - modifier givenLotIsConcluded() { vm.warp(block.timestamp + auctionDuration + 1); _; @@ -237,12 +232,7 @@ contract BidTest is Test, Permit2User { auctionHouse.bid(bidParams); } - function test_whenLotIdIsInvalid_reverts() - external - givenLotIsCreated - givenLotHasStarted - whenLotIdIsInvalid - { + function test_whenLotIdIsInvalid_reverts() external givenLotIsCreated whenLotIdIsInvalid { bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); vm.expectRevert(err); @@ -272,7 +262,6 @@ contract BidTest is Test, Permit2User { function test_incorrectAllowlistProof_reverts() external givenLotIsCreated - givenLotHasStarted givenLotHasAllowlist withIncorrectAllowlistProof { @@ -287,7 +276,6 @@ contract BidTest is Test, Permit2User { function test_givenLotHasAllowlist() external givenLotIsCreated - givenLotHasStarted givenLotHasAllowlist givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) @@ -300,7 +288,6 @@ contract BidTest is Test, Permit2User { function test_givenUserHasInsufficientBalance_reverts() public givenLotIsCreated - givenLotHasStarted givenUserHasApprovedQuoteToken(BID_AMOUNT) { vm.expectRevert("TRANSFER_FROM_FAILED"); @@ -313,7 +300,6 @@ contract BidTest is Test, Permit2User { function test_whenPermit2ApprovalIsProvided() external givenLotIsCreated - givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) whenPermit2ApprovalIsProvided { @@ -342,7 +328,6 @@ contract BidTest is Test, Permit2User { function test_whenPermit2ApprovalIsNotProvided() external givenLotIsCreated - givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { @@ -371,7 +356,6 @@ contract BidTest is Test, Permit2User { function test_whenAuctionParamIsProvided() external givenLotIsCreated - givenLotHasStarted givenUserHasQuoteTokenBalance(BID_AMOUNT) givenUserHasApprovedQuoteToken(BID_AMOUNT) { diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancel.t.sol index 26fb6eb4..795df7d0 100644 --- a/test/AuctionHouse/cancel.t.sol +++ b/test/AuctionHouse/cancel.t.sol @@ -37,9 +37,9 @@ contract CancelTest is Test, Permit2User { uint96 internal lotId; - address internal immutable auctionOwner = address(0x1); + address internal auctionOwner = address(0x1); - address internal immutable protocol = address(0x2); + address internal protocol = address(0x2); function setUp() external { baseToken = new MockERC20("Base Token", "BASE", 18); @@ -51,7 +51,7 @@ contract CancelTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp + 1), // start in 1 second, so we can cancel + start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -110,31 +110,19 @@ contract CancelTest is Test, Permit2User { } function testReverts_whenLotIsInactive() external whenLotIsCreated { - // Cancel once vm.prank(auctionOwner); auctionHouse.cancel(lotId); bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); - // Cancel again - vm.prank(auctionOwner); - auctionHouse.cancel(lotId); - } - - function test_givenLotHasStarted_reverts() external whenLotIsCreated { - // Warp beyond the start time - vm.warp(uint48(block.timestamp + 1)); - - bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); - vm.expectRevert(err); - - // Cancel vm.prank(auctionOwner); auctionHouse.cancel(lotId); } function test_success() external whenLotIsCreated { + assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); + vm.prank(auctionOwner); auctionHouse.cancel(lotId); diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index f066f680..4f9205ea 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -66,7 +66,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp) + 1, + start: uint48(block.timestamp), duration: auctionDuration, capacityInQuote: false, capacity: 10e18, @@ -113,11 +113,6 @@ contract CancelBidTest is Test, Permit2User { _; } - modifier givenLotHasStarted() { - vm.warp(auctionParams.start); - _; - } - modifier givenLotIsConcluded() { vm.warp(block.timestamp + auctionDuration + 1); _; @@ -163,6 +158,8 @@ contract CancelBidTest is Test, Permit2User { // [X] it reverts // [X] given the auction lot is not an atomic auction // [X] it reverts + // [X] given the auction lot is cancelled + // [X] it reverts // [X] given the auction lot is concluded // [X] it reverts // [X] given the bid does not exist @@ -191,10 +188,23 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } + function test_lotCancelled_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenLotIsCancelled + { + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + } + function test_lotConcluded_reverts() external givenLotIsCreated - givenLotHasStarted givenBidIsCreated givenLotIsConcluded { @@ -206,7 +216,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_givenBidDoesNotExist_reverts() external givenLotIsCreated givenLotHasStarted { + function test_givenBidDoesNotExist_reverts() external givenLotIsCreated { bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); vm.expectRevert(err); @@ -219,7 +229,6 @@ contract CancelBidTest is Test, Permit2User { function test_givenBidCancelled_reverts() external givenLotIsCreated - givenLotHasStarted givenBidIsCreated givenBidIsCancelled { @@ -232,12 +241,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_givenCallerIsNotBidOwner_reverts() - external - givenLotIsCreated - givenLotHasStarted - givenBidIsCreated - { + function test_givenCallerIsNotBidOwner_reverts() external givenLotIsCreated givenBidIsCreated { bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); vm.expectRevert(err); @@ -246,12 +250,7 @@ contract CancelBidTest is Test, Permit2User { auctionHouse.cancelBid(lotId, bidId); } - function test_itCancelsTheBid() - external - givenLotIsCreated - givenLotHasStarted - givenBidIsCreated - { + function test_itCancelsTheBid() external givenLotIsCreated givenBidIsCreated { // Call the function vm.prank(alice); auctionHouse.cancelBid(lotId, bidId); diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 31afe84e..138956f5 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -90,7 +90,7 @@ contract PurchaseTest is Test, Permit2User { mockHook = new MockHook(address(quoteToken), address(baseToken)); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp) + 1, + start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -140,9 +140,6 @@ contract PurchaseTest is Test, Permit2User { allowlistProof: allowlistProof, permit2Data: bytes("") }); - - // Warp to the start of the auction - vm.warp(auctionParams.start); } modifier givenDerivativeModuleIsInstalled() { @@ -198,9 +195,6 @@ contract PurchaseTest is Test, Permit2User { } modifier givenAuctionIsCancelled() { - // Warp to before the auction start - vm.warp(auctionParams.start - 1); - vm.prank(auctionOwner); auctionHouse.cancel(lotId); _; diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index 4d739c49..9443c0d9 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -26,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract AuctionModuleAuctionTest is Test, Permit2User { +contract AuctionTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index 98028196..6b778045 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -26,7 +26,7 @@ import { Module } from "src/modules/Modules.sol"; -contract AuctionModuleCancelTest is Test, Permit2User { +contract CancelTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; MockAuctionModule internal mockAuctionModule; @@ -51,7 +51,7 @@ contract AuctionModuleCancelTest is Test, Permit2User { auctionHouse.installModule(mockAuctionModule); auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp) + 1, + start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, capacity: 10e18, @@ -111,19 +111,9 @@ contract AuctionModuleCancelTest is Test, Permit2User { mockAuctionModule.cancelAuction(lotId); } - function test_givenLotIsActive_reverts() external whenLotIsCreated { - // Warp to the start of the auction - vm.warp(auctionParams.start); - - bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); - vm.expectRevert(err); - - // Cancel once - vm.prank(address(auctionHouse)); - mockAuctionModule.cancelAuction(lotId); - } - function test_success() external whenLotIsCreated { + assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); + vm.prank(address(auctionHouse)); mockAuctionModule.cancelAuction(lotId); From 5a0b303fe044269b2ecfc4f6f65ecca232962172 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 11:50:51 +0400 Subject: [PATCH 038/117] Prevent LSBBA from being cancelled after start --- src/modules/auctions/LSBBA/LSBBA.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 633a8ca0..f025bcdf 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -399,6 +399,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } function _cancelAuction(uint96 lotId_) internal override { + // Batch auctions cannot be cancelled once started, otherwise the seller could cancel the auction after bids have been submitted + if (lotData[lotId_].start <= block.timestamp) revert Auction_WrongState(); + // Auction cannot be cancelled once it has concluded if ( auctionData[lotId_].status != AuctionStatus.Created From 5a7fbfc12b7b85a54ce15de7a43ff2136d1147cd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 14:26:50 +0400 Subject: [PATCH 039/117] Implement pre-funding when the auction module supports it --- src/AuctionHouse.sol | 4 - src/bases/Auctioneer.sol | 76 +++++- src/interfaces/IHooks.sol | 7 + src/modules/Auction.sol | 41 ++- src/modules/auctions/LSBBA/LSBBA.sol | 12 +- test/AuctionHouse/auction.t.sol | 237 ++++++++++++++++-- test/AuctionHouse/collectPayment.t.sol | 5 +- test/AuctionHouse/collectPayout.t.sol | 13 +- test/AuctionHouse/sendPayout.t.sol | 7 +- .../Auction/MockAtomicAuctionModule.sol | 9 +- test/modules/Auction/MockAuctionModule.sol | 2 +- .../Auction/MockBatchAuctionModule.sol | 2 +- test/modules/Auction/MockHook.sol | 33 ++- 13 files changed, 384 insertions(+), 64 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index c86f121e..2ae0894b 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -192,10 +192,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { error AmountLessThanMinimum(); - error UnsupportedToken(address token_); - - error InvalidHook(); - error InvalidBidder(address bidder_); // ========== EVENTS ========== // diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 06ce6c0d..c0972714 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -1,7 +1,8 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import { fromKeycode, @@ -27,12 +28,16 @@ import {IAllowlist} from "src/interfaces/IAllowlist.sol"; /// - Cancelling auction lots /// - Storing information about how to handle inputs and outputs for auctions ("routing") abstract contract Auctioneer is WithModules { + using SafeTransferLib for ERC20; + // ========= ERRORS ========= // error InvalidParams(); error InvalidLotId(uint96 id_); error InvalidModuleType(Veecode reference_); error NotAuctionOwner(address caller_); + error InvalidHook(); + error UnsupportedToken(address token_); // ========= EVENTS ========= // @@ -40,17 +45,28 @@ abstract contract Auctioneer is WithModules { // ========= DATA STRUCTURES ========== // - /// @notice Auction routing information for a lot + /// @notice Auction routing information for a lot + /// @param auctionReference Auction module, represented by its Veecode + /// @param owner Lot owner + /// @param baseToken Token provided by seller + /// @param quoteToken Token to accept as payment + /// @param hooks (optional) Address to call for any hooks to be executed + /// @param allowlist (optional) Contract that implements an allowlist for the auction lot + /// @param derivativeReference (optional) Derivative module, represented by its Veecode + /// @param derivativeParams (optional) abi-encoded data to be used to create payout derivatives on a purchase + /// @param wrapDerivative (optional) Whether to wrap the derivative in a ERC20 token instead of the native ERC6909 format + /// @param prefunded Set by the auction module if the auction is prefunded struct Routing { - Veecode auctionReference; // auction module, represented by its Veecode - address owner; // market owner. sends payout tokens, receives quote tokens - ERC20 baseToken; // token provided by seller - ERC20 quoteToken; // token to accept as payment - IHooks hooks; // (optional) address to call for any hooks to be executed on a purchase. Must implement IHooks. - IAllowlist allowlist; // (optional) contract that implements an allowlist for the market, based on IAllowlist - Veecode derivativeReference; // (optional) derivative module, represented by its Veecode. If not set, no derivative will be created. - bytes derivativeParams; // (optional) abi-encoded data to be used to create payout derivatives on a purchase - bool wrapDerivative; // (optional) whether to wrap the derivative in a ERC20 token instead of the native ERC6909 format. + Veecode auctionReference; + address owner; + ERC20 baseToken; + ERC20 quoteToken; + IHooks hooks; + IAllowlist allowlist; + Veecode derivativeReference; + bytes derivativeParams; + bool wrapDerivative; + bool prefunded; } /// @notice Auction routing information provided as input parameters @@ -140,9 +156,11 @@ abstract contract Auctioneer is WithModules { lotId = lotCounter++; // Auction Module + bool requiresPrefunding; + uint256 lotCapacity; { // Call module auction function to store implementation-specific data - auctionModule.auction(lotId, params_); + (requiresPrefunding, lotCapacity) = auctionModule.auction(lotId, params_); } // Validate routing parameters @@ -240,6 +258,38 @@ abstract contract Auctioneer is WithModules { routing.hooks = routing_.hooks; } + // Perform pre-funding, if needed + if (requiresPrefunding) { + // Store pre-funding information + routing.prefunded = true; + + // TODO copied from AuctionHouse. Consider consolidating. + // Get the balance of the base token before the transfer + uint256 balanceBefore = routing_.baseToken.balanceOf(address(this)); + + // Call hook on hooks contract if provided + if (address(routing_.hooks) != address(0)) { + // The pre-auction create hook should transfer the base token to this contract + routing_.hooks.preAuctionCreate(lotId); + + // Check that the hook transferred the expected amount of base tokens + if (routing_.baseToken.balanceOf(address(this)) < balanceBefore + lotCapacity) { + revert InvalidHook(); + } + } + // Otherwise fallback to a standard ERC20 transfer + else { + // Transfer the base token from the auction owner + // `safeTransferFrom()` will revert upon failure or the lack of allowance or balance + routing_.baseToken.safeTransferFrom(msg.sender, address(this), lotCapacity); + + // Check that it is not a fee-on-transfer token + if (routing_.baseToken.balanceOf(address(this)) < balanceBefore + lotCapacity) { + revert UnsupportedToken(address(routing_.baseToken)); + } + } + } + emit AuctionCreated(lotId, address(routing.baseToken), address(routing.quoteToken)); } @@ -257,6 +307,8 @@ abstract contract Auctioneer is WithModules { module.cancelAuction(lotId_); } + // TODO claim refund if pre-funded and concluded + // ========== AUCTION INFORMATION ========== // /// @notice Gets the routing information for a given lot ID diff --git a/src/interfaces/IHooks.sol b/src/interfaces/IHooks.sol index 61e25d43..ab55b1bf 100644 --- a/src/interfaces/IHooks.sol +++ b/src/interfaces/IHooks.sol @@ -4,6 +4,13 @@ pragma solidity >=0.8.0; /// @title IHooks /// @notice Interface for hook contracts to be called during auction payment and payout interface IHooks { + /// @notice Called before auction creation + /// @notice Requirements: + /// - If the lot is pre-funded, the hook must ensure that the auctioneer has the required balance of base tokens + /// + /// @param lotId_ The auction's lot ID + function preAuctionCreate(uint96 lotId_) external; + /// @notice Called before payment and payout /// TODO define expected state, invariants function pre(uint256 lotId_, uint256 amount_) external; diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 4409e9fd..06b623e7 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -145,7 +145,16 @@ abstract contract Auction { // TODO NatSpec comments // TODO validate function - function auction(uint96 id_, AuctionParams memory params_) external virtual; + /// @notice Create an auction lot + /// + /// @param lotId_ The lot id + /// @param params_ The auction parameters + /// @return prefundingRequired Whether or not prefunding is required + /// @return capacity The capacity of the lot + function auction( + uint96 lotId_, + AuctionParams memory params_ + ) external virtual returns (bool prefundingRequired, uint256 capacity); function cancelAuction(uint96 id_) external virtual; @@ -171,16 +180,17 @@ abstract contract AuctionModule is Auction, Module { // ========== AUCTION MANAGEMENT ========== // - /// @notice Create an auction lot + /// @inheritdoc Auction /// @dev If the start time is zero, the auction will have a start time of the current block timestamp /// /// @dev This function reverts if: /// - the caller is not the parent of the module /// - the start time is in the past /// - the duration is less than the minimum - /// - /// @param lotId_ The lot id - function auction(uint96 lotId_, AuctionParams memory params_) external override onlyParent { + function auction( + uint96 lotId_, + AuctionParams memory params_ + ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidStart(params_.start, uint48(block.timestamp)); @@ -199,14 +209,29 @@ abstract contract AuctionModule is Auction, Module { lot.capacity = params_.capacity; // Call internal createAuction function to store implementation-specific data - _auction(lotId_, lot, params_.implParams); + (prefundingRequired) = _auction(lotId_, lot, params_.implParams); + + // Cannot pre-fund if capacity is in quote token + if (prefundingRequired && lot.capacityInQuote) revert Auction_InvalidParams(); // Store lot data lotData[lotId_] = lot; + + return (prefundingRequired, lot.capacity); } - /// @dev implementation-specific auction creation logic can be inserted by overriding this function - function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal virtual; + /// @notice Implementation-specific auction creation logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID + /// @param lot_ The lot data + /// @param params_ Additional auction parameters + /// @return prefundingRequired Whether or not prefunding is required + function _auction( + uint96 lotId_, + Lot memory lot_, + bytes memory params_ + ) internal virtual returns (bool prefundingRequired); /// @notice Cancel an auction lot /// @dev Owner is stored in the Routing information on the AuctionHouse, so we check permissions there diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index f025bcdf..705fa861 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -360,8 +360,13 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== AUCTION MANAGEMENT ========== // - // TODO auction creation - function _auction(uint96 lotId_, Lot memory lot_, bytes memory params_) internal override { + /// @inheritdoc AuctionModule + /// @dev Creates a new auction lot for the LSBBA auction type. + function _auction( + uint96 lotId_, + Lot memory lot_, + bytes memory params_ + ) internal override returns (bool prefundingRequired) { // Decode implementation params ( uint256 minimumPrice, @@ -396,6 +401,9 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Initialize sorted bid queue lotSortedBids[lotId_].initialize(); + + // This auction type requires pre-funding + return (true); } function _cancelAuction(uint96 lotId_) internal override { diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index fdb63976..6fbfef35 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -7,7 +7,8 @@ import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; // Mocks import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; -import {MockAuctionModule} from "test/modules/Auction/MockAuctionModule.sol"; +import {MockFeeOnTransferERC20} from "test/lib/mocks/MockFeeOnTransferERC20.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; import {MockDerivativeModule} from "test/modules/Derivative/MockDerivativeModule.sol"; import {MockCondenserModule} from "test/modules/Condenser/MockCondenserModule.sol"; import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; @@ -31,9 +32,9 @@ import { } from "src/modules/Modules.sol"; contract AuctionTest is Test, Permit2User { - MockERC20 internal baseToken; + MockFeeOnTransferERC20 internal baseToken; MockERC20 internal quoteToken; - MockAuctionModule internal mockAuctionModule; + MockAtomicAuctionModule internal mockAuctionModule; MockDerivativeModule internal mockDerivativeModule; MockCondenserModule internal mockCondenserModule; MockAllowlist internal mockAllowlist; @@ -45,12 +46,14 @@ contract AuctionTest is Test, Permit2User { address internal immutable protocol = address(0x2); + uint256 internal constant LOT_CAPACITY = 10e18; + function setUp() external { - baseToken = new MockERC20("Base Token", "BASE", 18); + baseToken = new MockFeeOnTransferERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); - mockAuctionModule = new MockAuctionModule(address(auctionHouse)); + mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); mockDerivativeModule = new MockDerivativeModule(address(auctionHouse)); mockCondenserModule = new MockCondenserModule(address(auctionHouse)); mockAllowlist = new MockAllowlist(); @@ -60,12 +63,12 @@ contract AuctionTest is Test, Permit2User { start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, - capacity: 10e18, + capacity: LOT_CAPACITY, implParams: abi.encode("") }); routingParams = Auctioneer.RoutingParams({ - auctionType: toKeycode("MOCK"), + auctionType: toKeycode("ATOM"), baseToken: baseToken, quoteToken: quoteToken, hooks: IHooks(address(0)), @@ -118,7 +121,7 @@ contract AuctionTest is Test, Permit2User { function testReverts_whenModuleNotInstalled() external { bytes memory err = - abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 0); + abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("ATOM"), 0); vm.expectRevert(err); auctionHouse.auction(routingParams, auctionParams); @@ -142,11 +145,11 @@ contract AuctionTest is Test, Permit2User { function testReverts_whenModuleIsSunset() external whenAuctionModuleIsInstalled { // Sunset the module, which prevents the creation of new auctions using that module - auctionHouse.sunsetModule(toKeycode("MOCK")); + auctionHouse.sunsetModule(toKeycode("ATOM")); // Expect revert bytes memory err = - abi.encodeWithSelector(WithModules.ModuleIsSunset.selector, toKeycode("MOCK")); + abi.encodeWithSelector(WithModules.ModuleIsSunset.selector, toKeycode("ATOM")); vm.expectRevert(err); auctionHouse.auction(routingParams, auctionParams); @@ -226,7 +229,8 @@ contract AuctionTest is Test, Permit2User { IAllowlist lotAllowlist, Veecode lotDerivativeType, bytes memory lotDerivativeParams, - bool lotWrapDerivative + bool lotWrapDerivative, + bool lotPrefunded ) = auctionHouse.lotRouting(lotId); assertEq( fromVeecode(lotAuctionType), @@ -241,6 +245,7 @@ contract AuctionTest is Test, Permit2User { assertEq(fromVeecode(lotDerivativeType), "", "derivative type mismatch"); assertEq(lotDerivativeParams, "", "derivative params mismatch"); assertEq(lotWrapDerivative, false, "wrap derivative mismatch"); + assertEq(lotPrefunded, false, "prefunded mismatch"); // Auction module also updated (uint48 lotStart,,,,,) = mockAuctionModule.lotData(lotId); @@ -255,7 +260,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values - (,, ERC20 lotBaseToken, ERC20 lotQuoteToken,,,,,) = auctionHouse.lotRouting(lotId); + (,, ERC20 lotBaseToken, ERC20 lotQuoteToken,,,,,,) = auctionHouse.lotRouting(lotId); assertEq(address(lotBaseToken), address(baseToken), "base token mismatch"); assertEq(address(lotQuoteToken), address(baseToken), "quote token mismatch"); } @@ -282,7 +287,7 @@ contract AuctionTest is Test, Permit2User { function testReverts_whenDerivativeTypeIncorrect() external whenAuctionModuleIsInstalled { // Update routing params - routingParams.derivativeType = toKeycode("MOCK"); + routingParams.derivativeType = toKeycode("ATOM"); // Expect revert bytes memory err = abi.encodeWithSelector( @@ -333,7 +338,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values - (,,,,,, Veecode lotDerivativeType,,) = auctionHouse.lotRouting(lotId); + (,,,,,, Veecode lotDerivativeType,,,) = auctionHouse.lotRouting(lotId); assertEq( fromVeecode(lotDerivativeType), fromVeecode(mockDerivativeModule.VEECODE()), @@ -354,7 +359,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values - (,,,,,, Veecode lotDerivativeType, bytes memory lotDerivativeParams,) = + (,,,,,, Veecode lotDerivativeType, bytes memory lotDerivativeParams,,) = auctionHouse.lotRouting(lotId); assertEq( fromVeecode(lotDerivativeType), @@ -407,15 +412,22 @@ contract AuctionTest is Test, Permit2User { // [X] reverts when allowlist validation fails // [X] sets the allowlist on the auction lot - function test_success_allowlistIsSet() external whenAuctionModuleIsInstalled { + modifier whenAllowlistIsSet() { // Update routing params routingParams.allowlist = mockAllowlist; + _; + } + function test_success_allowlistIsSet() + external + whenAuctionModuleIsInstalled + whenAllowlistIsSet + { // Create the auction uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values - (,,,,, IAllowlist lotAllowlist,,,) = auctionHouse.lotRouting(lotId); + (,,,,, IAllowlist lotAllowlist,,,,) = auctionHouse.lotRouting(lotId); assertEq(address(lotAllowlist), address(mockAllowlist), "allowlist mismatch"); @@ -451,6 +463,12 @@ contract AuctionTest is Test, Permit2User { // [X] reverts when the hooks address is not a contract // [X] sets the hooks on the auction lot + modifier whenHooksIsSet() { + // Update routing params + routingParams.hooks = mockHook; + _; + } + function testReverts_whenHooksIsNotContract() external whenAuctionModuleIsInstalled { // Update routing params routingParams.hooks = IHooks(address(0x10)); @@ -462,16 +480,191 @@ contract AuctionTest is Test, Permit2User { auctionHouse.auction(routingParams, auctionParams); } - function test_success_hooksIsSet() external whenAuctionModuleIsInstalled { - // Update routing params - routingParams.hooks = mockHook; - + function test_success_hooksIsSet() external whenAuctionModuleIsInstalled whenHooksIsSet { // Create the auction uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Assert values - (,,,, IHooks lotHooks,,,,) = auctionHouse.lotRouting(lotId); + (,,,, IHooks lotHooks,,,,,) = auctionHouse.lotRouting(lotId); assertEq(address(lotHooks), address(mockHook), "hooks mismatch"); } + + // [X] given the auction module requires prefunding + // [X] reverts when the auction has capacity in quote + // [X] when the auction has hooks + // [X] reverts when the hook does not transfer enough payout tokens + // [X] it succeeds + // [X] when the auction does not have hooks + // [X] reverts when the auction owner does not have enough balance + // [X] reverts when the auction owner does not have enough allowance + // [X] it succeeds + + modifier givenAuctionRequiresPrefunding() { + mockAuctionModule.setRequiredPrefunding(true); + _; + } + + modifier whenAuctionCapacityInQuote() { + auctionParams.capacityInQuote = true; + _; + } + + modifier givenHookHasBaseTokenBalance(uint256 amount_) { + // Mint the amount to the hook + baseToken.mint(address(mockHook), amount_); + _; + } + + modifier givenPreAuctionCreateHookBreaksInvariant() { + mockHook.setPreAuctionCreateMultiplier(9000); + _; + } + + modifier givenOwnerHasBaseTokenAllowance(uint256 amount_) { + // Approve the auction house + baseToken.approve(address(auctionHouse), amount_); + _; + } + + modifier givenOwnerHasBaseTokenBalance(uint256 amount_) { + // Mint the amount to the owner + baseToken.mint(address(this), amount_); + _; + } + + modifier givenBaseTokenTakesFeeOnTransfer() { + // Set the fee on transfer + baseToken.setTransferFee(1000); + _; + } + + function test_prefunding_capacityInQuote_reverts() + external + whenAuctionModuleIsInstalled + givenAuctionRequiresPrefunding + whenAuctionCapacityInQuote + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding_withHooks_invariantBreaks_reverts() + external + whenAuctionModuleIsInstalled + whenHooksIsSet + givenAuctionRequiresPrefunding + givenHookHasBaseTokenBalance(LOT_CAPACITY) + givenPreAuctionCreateHookBreaksInvariant + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidHook.selector); + vm.expectRevert(err); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding_withHooks_feeOnTransfer_reverts() + external + whenAuctionModuleIsInstalled + whenHooksIsSet + givenAuctionRequiresPrefunding + givenHookHasBaseTokenBalance(LOT_CAPACITY) + givenBaseTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidHook.selector); + vm.expectRevert(err); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding_withHooks() + external + whenAuctionModuleIsInstalled + whenHooksIsSet + givenAuctionRequiresPrefunding + givenHookHasBaseTokenBalance(LOT_CAPACITY) + { + // Create the auction + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); + + // Check the prefunding status + (,,,,,,,,, bool lotPrefunded) = auctionHouse.lotRouting(lotId); + assertEq(lotPrefunded, true, "prefunded mismatch"); + + // Check balances + assertEq(baseToken.balanceOf(address(mockHook)), 0, "hook balance mismatch"); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + LOT_CAPACITY, + "auction house balance mismatch" + ); + } + + function test_prefunding_insufficientBalance_reverts() + external + whenAuctionModuleIsInstalled + givenAuctionRequiresPrefunding + givenOwnerHasBaseTokenAllowance(LOT_CAPACITY) + { + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding_insufficientAllowance_reverts() + external + whenAuctionModuleIsInstalled + givenAuctionRequiresPrefunding + givenOwnerHasBaseTokenBalance(LOT_CAPACITY) + { + // Expect revert + vm.expectRevert("TRANSFER_FROM_FAILED"); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding_feeOnTransfer_reverts() + external + whenAuctionModuleIsInstalled + givenAuctionRequiresPrefunding + givenOwnerHasBaseTokenBalance(LOT_CAPACITY) + givenOwnerHasBaseTokenAllowance(LOT_CAPACITY) + givenBaseTokenTakesFeeOnTransfer + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(baseToken)); + vm.expectRevert(err); + + auctionHouse.auction(routingParams, auctionParams); + } + + function test_prefunding() + external + whenAuctionModuleIsInstalled + givenAuctionRequiresPrefunding + givenOwnerHasBaseTokenBalance(LOT_CAPACITY) + givenOwnerHasBaseTokenAllowance(LOT_CAPACITY) + { + // Create the auction + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); + + // Check the prefunding status + (,,,,,,,,, bool lotPrefunded) = auctionHouse.lotRouting(lotId); + assertEq(lotPrefunded, true, "prefunded mismatch"); + + // Check balances + assertEq(baseToken.balanceOf(address(this)), 0, "owner balance mismatch"); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + LOT_CAPACITY, + "auction house balance mismatch" + ); + } } diff --git a/test/AuctionHouse/collectPayment.t.sol b/test/AuctionHouse/collectPayment.t.sol index 1badf1ac..c1f30322 100644 --- a/test/AuctionHouse/collectPayment.t.sol +++ b/test/AuctionHouse/collectPayment.t.sol @@ -11,6 +11,7 @@ import {Permit2User} from "test/lib/permit2/Permit2User.sol"; import {IPermit2} from "src/lib/permit2/interfaces/IPermit2.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; contract CollectPaymentTest is Test, Permit2User { @@ -287,7 +288,7 @@ contract CollectPaymentTest is Test, Permit2User { { // Expect the error bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(quoteToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(quoteToken)); vm.expectRevert(err); // Call @@ -359,7 +360,7 @@ contract CollectPaymentTest is Test, Permit2User { { // Expect the error bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(quoteToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(quoteToken)); vm.expectRevert(err); // Call diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 32e82d6d..4c41577b 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -64,7 +64,8 @@ contract CollectPayoutTest is Test, Permit2User { allowlist: IAllowlist(address(0)), derivativeReference: derivativeReference, derivativeParams: derivativeParams, - wrapDerivative: wrapDerivative + wrapDerivative: wrapDerivative, + prefunded: false }); } @@ -153,7 +154,7 @@ contract CollectPayoutTest is Test, Permit2User { whenMidHookBreaksInvariant { // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidHook.selector); vm.expectRevert(err); // Call @@ -169,7 +170,7 @@ contract CollectPayoutTest is Test, Permit2User { givenTokenTakesFeeOnTransfer { // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidHook.selector); vm.expectRevert(err); // Call @@ -264,7 +265,7 @@ contract CollectPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call @@ -320,7 +321,7 @@ contract CollectPayoutTest is Test, Permit2User { whenMidHookBreaksInvariant { // Expect revert - bytes memory err = abi.encodeWithSelector(AuctionHouse.InvalidHook.selector); + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidHook.selector); vm.expectRevert(err); // Call @@ -410,7 +411,7 @@ contract CollectPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index 3c280f68..1b4ceb6b 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -86,7 +86,8 @@ contract SendPayoutTest is Test, Permit2User { allowlist: IAllowlist(address(0)), derivativeReference: derivativeReference, derivativeParams: derivativeParams, - wrapDerivative: wrapDerivative + wrapDerivative: wrapDerivative, + prefunded: false }); } @@ -155,7 +156,7 @@ contract SendPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call @@ -229,7 +230,7 @@ contract SendPayoutTest is Test, Permit2User { { // Expect revert bytes memory err = - abi.encodeWithSelector(AuctionHouse.UnsupportedToken.selector, address(payoutToken)); + abi.encodeWithSelector(Auctioneer.UnsupportedToken.selector, address(payoutToken)); vm.expectRevert(err); // Call diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 6371889b..67e9a809 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -10,6 +10,7 @@ import {AuctionModule} from "src/modules/Auction.sol"; contract MockAtomicAuctionModule is AuctionModule { mapping(uint256 => uint256) public payoutData; bool public purchaseReverts; + bool public requiresPrefunding; struct Output { uint256 multiplier; @@ -29,7 +30,13 @@ contract MockAtomicAuctionModule is AuctionModule { return Type.Auction; } - function _auction(uint96, Lot memory, bytes memory) internal virtual override {} + function setRequiredPrefunding(bool prefunding_) external virtual { + requiresPrefunding = prefunding_; + } + + function _auction(uint96, Lot memory, bytes memory) internal virtual override returns (bool) { + return requiresPrefunding; + } function _cancelAuction(uint96 id_) internal override { cancelled[id_] = true; diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 614bb142..9eaadbd3 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -20,7 +20,7 @@ contract MockAuctionModule is AuctionModule { return Type.Auction; } - function _auction(uint96, Lot memory, bytes memory) internal virtual override {} + function _auction(uint96, Lot memory, bytes memory) internal virtual override returns (bool) {} function _cancelAuction(uint96 id_) internal override { // diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 89559f83..4675ead5 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -23,7 +23,7 @@ contract MockBatchAuctionModule is AuctionModule { return Type.Auction; } - function _auction(uint96, Lot memory, bytes memory) internal virtual override {} + function _auction(uint96, Lot memory, bytes memory) internal virtual override returns (bool) {} function _cancelAuction(uint96 id_) internal override { // diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index 95ed04dc..dfbade79 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -3,7 +3,8 @@ pragma solidity 0.8.19; import {ERC20} from "solmate/tokens/ERC20.sol"; -import {IHooks} from "src/bases/Auctioneer.sol"; +import {IHooks} from "src/interfaces/IHooks.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; @@ -38,6 +39,8 @@ contract MockHook is IHooks { bool public postHookCalled; bool public postHookReverts; + uint256 public preAuctionCreateMultiplier; + constructor(address quoteToken_, address payoutToken_) { quoteToken = ERC20(quoteToken_); payoutToken = ERC20(payoutToken_); @@ -94,7 +97,7 @@ contract MockHook is IHooks { } } - uint256 actualPayout = payout_ * midHookMultiplier / 10_000; + uint256 actualPayout = (payout_ * midHookMultiplier) / 10_000; // Has to transfer the payout token to the router ERC20(payoutToken).safeTransfer(msg.sender, actualPayout); @@ -147,4 +150,30 @@ contract MockHook is IHooks { function setPayoutToken(address payoutToken_) external { payoutToken = ERC20(payoutToken_); } + + function setPreAuctionCreateMultiplier(uint256 multiplier_) external { + preAuctionCreateMultiplier = multiplier_; + } + + function preAuctionCreate(uint96 lotId_) external override { + // Get the lot information + Auctioneer.Routing memory routing = Auctioneer(msg.sender).getRouting(lotId_); + + // If pre-funding is required + if (routing.prefunded) { + // Get the capacity + uint256 capacity = Auctioneer(msg.sender).remainingCapacity(lotId_); + + // If the multiplier is set, apply that + if (preAuctionCreateMultiplier > 0) { + capacity = (capacity * preAuctionCreateMultiplier) / 10_000; + } + + // Approve transfer + routing.baseToken.safeApprove(address(msg.sender), capacity); + + // Transfer the base token to the auctioneer + routing.baseToken.safeTransfer(msg.sender, capacity); + } + } } From c56142bcf8b715fb8160ca835cc1799f3fa3742a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 14:50:04 +0400 Subject: [PATCH 040/117] Prevent double-transfer when purchasing from a pre-funded auction --- src/AuctionHouse.sol | 6 +++ test/AuctionHouse/collectPayout.t.sol | 51 ++++++++++++++++++++++- test/AuctionHouse/purchase.t.sol | 58 ++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 2ae0894b..4f418819 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -690,6 +690,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @dev This function handles the following: /// 1. Calls the mid hook on the hooks contract (if provided) /// 2. Transfers the payout token from the auction owner + /// 2a. If the auction is pre-funded, then the transfer is skipped /// /// This function reverts if: /// - Approval has not been granted to transfer the payout token @@ -709,6 +710,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { uint256 payoutAmount_, Routing memory routingParams_ ) internal { + // If pre-funded, then the payout token is already in this contract + if (routingParams_.prefunded) { + return; + } + // Get the balance of the payout token before the transfer uint256 balanceBefore = routingParams_.baseToken.balanceOf(address(this)); diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 4c41577b..24e78404 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -292,7 +292,7 @@ contract CollectPayoutTest is Test, Permit2User { // [X] given the auction has hooks defined // [X] given the hook breaks the invariant // [X] it reverts - // [X] it succeeds - base token is transferred to the derivativeModule, mid hook is called before transfer + // [X] it succeeds - base token is transferred to the auction house, mid hook is called before transfer // [X] given the auction does not have hooks defined // [X] given the auction owner has insufficient balance of the payout token // [X] it reverts @@ -300,7 +300,7 @@ contract CollectPayoutTest is Test, Permit2User { // [X] it reverts // [X] given transferring the payout token would result in a lesser amount being received // [X] it reverts - // [X] it succeeds - base token is transferred to the derivativeModule + // [X] it succeeds - base token is transferred to the auction house modifier givenAuctionHasDerivative() { // Install the derivative module @@ -444,4 +444,51 @@ contract CollectPayoutTest is Test, Permit2User { "payout token: derivativeModule balance mismatch" ); } + + // ========== Prefunding flow ========== // + + // [X] given the auction is pre-funded + // [X] it does not transfer the base token to the auction house + + modifier givenAuctionIsPrefunded() { + routingParams.prefunded = true; + _; + } + + modifier givenAuctionHouseHasPayoutTokenBalance(uint256 amount_) { + payoutToken.mint(address(auctionHouse), amount_); + _; + } + + function test_prefunded() + public + givenAuctionIsPrefunded + givenAuctionHouseHasPayoutTokenBalance(payoutAmount) + { + // Assert previous balance + assertEq( + payoutToken.balanceOf(address(auctionHouse)), + payoutAmount, + "payout token: auctionHouse balance mismatch" + ); + + // Call + vm.prank(USER); + auctionHouse.collectPayout(lotId, paymentAmount, payoutAmount, routingParams); + + // Check balances + assertEq(payoutToken.balanceOf(OWNER), 0, "payout token: owner balance mismatch"); + assertEq(payoutToken.balanceOf(USER), 0, "payout token: user balance mismatch"); + assertEq( + payoutToken.balanceOf(address(auctionHouse)), + payoutAmount, + "payout token: auctionHouse balance mismatch" + ); + assertEq(payoutToken.balanceOf(address(hook)), 0, "payout token: hook balance mismatch"); + assertEq( + payoutToken.balanceOf(address(mockDerivativeModule)), + 0, + "payout token: derivativeModule balance mismatch" + ); + } } diff --git a/test/AuctionHouse/purchase.t.sol b/test/AuctionHouse/purchase.t.sol index 138956f5..e77d98fd 100644 --- a/test/AuctionHouse/purchase.t.sol +++ b/test/AuctionHouse/purchase.t.sol @@ -53,6 +53,8 @@ contract PurchaseTest is Test, Permit2User { uint96 internal lotId; + uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal constant AMOUNT_IN = 1e18; uint256 internal AMOUNT_OUT; @@ -93,7 +95,7 @@ contract PurchaseTest is Test, Permit2User { start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, - capacity: 10e18, + capacity: LOT_CAPACITY, implParams: abi.encode("") }); @@ -635,4 +637,58 @@ contract PurchaseTest is Test, Permit2User { 0 ); } + + // ======== Prefunding flow ======== // + + // [X] given the auction is prefunded + // [X] it succeeds - base token is not transferred from auction owner again + + modifier givenAuctionIsPrefunded() { + // Set the auction to be prefunded + mockAuctionModule.setRequiredPrefunding(true); + + // Mint base tokens to the owner + baseToken.mint(auctionOwner, LOT_CAPACITY); + + // Approve the auction house to transfer the base tokens + vm.prank(auctionOwner); + baseToken.approve(address(auctionHouse), LOT_CAPACITY); + + // Create a new auction + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + + // Update purchase parameters + purchaseParams.lotId = lotId; + _; + } + + function test_prefunded() + external + givenAuctionIsPrefunded + givenUserHasQuoteTokenBalance(AMOUNT_IN) + givenQuoteTokenSpendingIsApproved + { + // Auction house has base tokens + assertEq( + baseToken.balanceOf(address(auctionHouse)), + LOT_CAPACITY, + "pre-purchase: balance mismatch on auction house" + ); + + // Purchase + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + + // Check balances of the base token + assertEq(baseToken.balanceOf(alice), 0, "balance mismatch on alice"); + assertEq(baseToken.balanceOf(recipient), AMOUNT_OUT, "balance mismatch on recipient"); + assertEq(baseToken.balanceOf(address(mockHook)), 0, "balance mismatch on hook"); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + LOT_CAPACITY - AMOUNT_OUT, + "balance mismatch on auction house" + ); + assertEq(baseToken.balanceOf(auctionOwner), 0, "balance mismatch on auction owner"); + } } From 31ebe351a803f45e5298407b97548ab0a3284c2f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 14:58:13 +0400 Subject: [PATCH 041/117] chore: linting --- src/bases/Derivatizer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index 32589a79..a0c6670a 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -1,7 +1,7 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import {WithModules, Veecode} from "src/modules/Modules.sol"; +import {WithModules} from "src/modules/Modules.sol"; abstract contract Derivatizer is WithModules { // ========== DERIVATIVE MANAGEMENT ========== // From bc0981a77a4878ef05a126f9f84e20317a555ac9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 16:30:25 +0400 Subject: [PATCH 042/117] Implementation and tests for claimBidRefund() --- src/AuctionHouse.sol | 10 +- src/modules/Auction.sol | 13 +- src/modules/auctions/LSBBA/LSBBA.sol | 10 +- test/AuctionHouse/cancelBid.t.sol | 2 +- test/AuctionHouse/claimBidRefund.t.sol | 295 ++++++++++++++++++ .../Auction/MockAtomicAuctionModule.sol | 8 +- test/modules/Auction/MockAuctionModule.sol | 2 +- .../Auction/MockBatchAuctionModule.sol | 41 ++- 8 files changed, 362 insertions(+), 19 deletions(-) create mode 100644 test/AuctionHouse/claimBidRefund.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 4f418819..9ec7a1c2 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -507,7 +507,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @inheritdoc Router function claimBidRefund(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { - // + // Claim the refund on the auction module + // The auction module is responsible for validating the bid and authorizing the caller + AuctionModule module = _getModuleForId(lotId_); + uint256 refundAmount = module.claimRefund(lotId_, bidId_, msg.sender); + + // Transfer the quote token to the bidder + // The ownership of the bid has already been verified by the auction module + // TODO consider if another check is required + lotRouting[lotId_].quoteToken.safeTransfer(msg.sender, refundAmount); } // // External submission and evaluation diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 06b623e7..6ce60410 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -119,10 +119,15 @@ abstract contract Auction { /// - Authorize `bidder_` /// - Update the bid data /// - /// @param lotId_ The lot id - /// @param bidId_ The bid id - /// @param bidder_ The bidder of the purchased tokens - function claimRefund(uint96 lotId_, uint256 bidId_, address bidder_) external virtual; + /// @param lotId_ The lot id + /// @param bidId_ The bid id + /// @param bidder_ The bidder of the purchased tokens + /// @return refundAmount The amount of quote tokens refunded + function claimRefund( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual returns (uint256 refundAmount); /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 705fa861..236a294e 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -160,7 +160,13 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { uint96 lotId_, uint256 bidId_, address sender_ - ) external override onlyInternal onlyBidder(sender_, lotId_, bidId_) { + ) + external + override + onlyInternal + onlyBidder(sender_, lotId_, bidId_) + returns (uint256 refundAmount) + { // Validate inputs // Auction for must have settled to claim refund // User must not have won the auction or claimed a refund already @@ -176,6 +182,8 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Set bid status to refunded lotEncryptedBids[lotId_][bidId_].status = BidStatus.Refunded; + + return lotEncryptedBids[lotId_][bidId_].amount; } // =========== DECRYPTION =========== // diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index 4f9205ea..6db54ed2 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -156,7 +156,7 @@ contract CancelBidTest is Test, Permit2User { // cancelBid // [X] given the auction lot does not exist // [X] it reverts - // [X] given the auction lot is not an atomic auction + // [X] given the auction lot is an atomic auction // [X] it reverts // [X] given the auction lot is cancelled // [X] it reverts diff --git a/test/AuctionHouse/claimBidRefund.t.sol b/test/AuctionHouse/claimBidRefund.t.sol new file mode 100644 index 00000000..9fa38db4 --- /dev/null +++ b/test/AuctionHouse/claimBidRefund.t.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; + +// Mocks +import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; +import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; +import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; + +// Auctions +import {AuctionHouse, Router} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; + +// Modules +import { + Keycode, + toKeycode, + Veecode, + wrapVeecode, + unwrapVeecode, + fromVeecode, + WithModules, + Module +} from "src/modules/Modules.sol"; + +contract ClaimnBidRefundTest is Test, Permit2User { + MockERC20 internal baseToken; + MockERC20 internal quoteToken; + MockBatchAuctionModule internal mockAuctionModule; + + AuctionHouse internal auctionHouse; + + uint48 internal auctionDuration = 1 days; + + address internal immutable protocol = address(0x2); + address internal immutable referrer = address(0x4); + address internal immutable auctionOwner = address(0x5); + address internal immutable recipient = address(0x6); + address internal immutable alice = address(0x7); + + uint256 internal constant BID_AMOUNT = 1e18; + + Auctioneer.RoutingParams internal routingParams; + Auction.AuctionParams internal auctionParams; + + // Function parameters (can be modified) + uint96 internal lotId; + uint256 internal bidId; + + function setUp() external { + // Set block timestamp + vm.warp(1_000_000); + + baseToken = new MockERC20("Base Token", "BASE", 18); + quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + + auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); + mockAuctionModule = new MockBatchAuctionModule(address(auctionHouse)); + + auctionHouse.installModule(mockAuctionModule); + + auctionParams = Auction.AuctionParams({ + start: uint48(block.timestamp), + duration: auctionDuration, + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode("") + }); + + routingParams = Auctioneer.RoutingParams({ + auctionType: toKeycode("BATCH"), + baseToken: baseToken, + quoteToken: quoteToken, + hooks: IHooks(address(0)), + allowlist: IAllowlist(address(0)), + allowlistParams: abi.encode(""), + payoutData: abi.encode(""), + derivativeType: toKeycode(""), + derivativeParams: abi.encode("") + }); + } + + modifier givenLotIsCreated() { + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenLotIsCancelled() { + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + _; + } + + modifier givenLotIsAtomicAuction() { + // Install the atomic auction module + MockAtomicAuctionModule mockAtomicAuctionModule = + new MockAtomicAuctionModule(address(auctionHouse)); + auctionHouse.installModule(mockAtomicAuctionModule); + + // Update routing parameters + (Keycode moduleKeycode,) = unwrapVeecode(mockAtomicAuctionModule.VEECODE()); + routingParams.auctionType = moduleKeycode; + + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + + modifier givenLotIsConcluded() { + vm.warp(block.timestamp + auctionDuration + 1); + _; + } + + modifier whenLotIdIsInvalid() { + lotId = 255; + _; + } + + modifier givenBidIsCreated() { + // Mint quote tokens to alice + quoteToken.mint(alice, BID_AMOUNT); + + // Approve spending + vm.prank(alice); + quoteToken.approve(address(auctionHouse), BID_AMOUNT); + + // Create the bid + Router.BidParams memory bidParams = Router.BidParams({ + lotId: lotId, + recipient: recipient, + referrer: referrer, + amount: BID_AMOUNT, + auctionData: bytes(""), + allowlistProof: bytes(""), + permit2Data: bytes("") + }); + + vm.prank(alice); + auctionHouse.bid(bidParams); + _; + } + + modifier givenBidIsCancelled() { + vm.prank(alice); + auctionHouse.cancelBid(lotId, bidId); + _; + } + + // claimBidRefund + // [ ] given the auction lot does not exist + // [ ] it reverts + // [ ] given the auction lot is an atomic auction + // [ ] it reverts + // [ ] given the bid does not exist + // [ ] it reverts + // [ ] given the bid is not cancelled + // [ ] it reverts + // [ ] given the caller is not the bid owner + // [ ] it reverts + // [ ] given the bid refund has already been claimed + // [ ] it reverts + // [ ] given the auction lot is concluded + // [ ] it refunds the bid + // [ ] given the auction lot is cancelled + // [ ] it refunds the bid + + function test_noAuction_reverts() external { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_givenAtomicAuction_reverts() external givenLotIsAtomicAuction { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); + vm.expectRevert(err); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_noBid_reverts() external givenLotIsCreated { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); + vm.expectRevert(err); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_bidNotCancelled_reverts() external givenLotIsCreated givenBidIsCreated { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_notBidOwner_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenBidIsCancelled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); + vm.expectRevert(err); + + // Call as a different user + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_bidRefundClaimed_reverts() + external + givenLotIsCreated + givenBidIsCreated + givenBidIsCancelled + { + // Claim the bid refund + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + } + + function test_lotConcluded_refundsBid() + external + givenLotIsCreated + givenBidIsCreated + givenBidIsCancelled + givenLotIsConcluded + { + // Get alice's balance + uint256 aliceBalance = quoteToken.balanceOf(alice); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + + // Expect alice's balance to increase + assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); + } + + function test_lotCancelled_refundsBid() + external + givenLotIsCreated + givenBidIsCreated + givenBidIsCancelled + givenLotIsCancelled + { + // Get alice's balance + uint256 aliceBalance = quoteToken.balanceOf(alice); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + + // Expect alice's balance to increase + assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); + } + + function test_refundsBid() external givenLotIsCreated givenBidIsCreated givenBidIsCancelled { + // Get alice's balance + uint256 aliceBalance = quoteToken.balanceOf(alice); + + // Call + vm.prank(alice); + auctionHouse.claimBidRefund(lotId, bidId); + + // Expect alice's balance to increase + assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); + } +} diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 67e9a809..1083d84c 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -111,9 +111,7 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function claimRefund( - uint96 lotId_, - uint256 bidId_, - address bidder_ - ) external virtual override {} + function claimRefund(uint96, uint256, address) external virtual override returns (uint256) { + revert Auction_NotImplemented(); + } } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 9eaadbd3..5d0a1d25 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -68,7 +68,7 @@ contract MockAuctionModule is AuctionModule { uint96 lotId_, uint256 bidId_, address bidder_ - ) external virtual override {} + ) external virtual override returns (uint256 refundAmount) {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 4675ead5..4b58c6ab 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -10,6 +10,7 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; contract MockBatchAuctionModule is AuctionModule { mapping(uint96 lotId => Bid[]) public bidData; mapping(uint96 lotId => mapping(uint256 => bool)) public bidCancelled; + mapping(uint96 lotId => mapping(uint256 => bool)) public bidRefunded; constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; @@ -96,6 +97,40 @@ contract MockBatchAuctionModule is AuctionModule { bidCancelled[lotId_][bidId_] = true; } + function claimRefund( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) external virtual override returns (uint256 refundAmount) { + // Check that the bid exists + if (bidData[lotId_].length <= bidId_) { + revert Auction.Auction_InvalidBidId(lotId_, bidId_); + } + + // Check that the bid has been cancelled + if (bidCancelled[lotId_][bidId_] == false) { + revert Auction.Auction_InvalidParams(); + } + + // Check that the bidder is the owner of the bid + if (bidData[lotId_][bidId_].bidder != bidder_) { + revert Auction.Auction_NotBidder(); + } + + // Check that the bid has not been refunded + if (bidRefunded[lotId_][bidId_] == true) { + revert Auction.Auction_InvalidParams(); + } + + // Get the bid amount + refundAmount = bidData[lotId_][bidId_].amount; + + // Mark the bid as refunded + bidRefunded[lotId_][bidId_] = true; + + return refundAmount; + } + function settle( uint256 id_, Bid[] memory bids_ @@ -125,10 +160,4 @@ contract MockBatchAuctionModule is AuctionModule { function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } - - function claimRefund( - uint96 lotId_, - uint256 bidId_, - address bidder_ - ) external virtual override {} } From cd14b713be358b3559ee38acdeba5662ec0388e2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 16:48:41 +0400 Subject: [PATCH 043/117] Stubs for claimAuctionRefund() --- src/bases/Auctioneer.sol | 16 ++++++++++++++++ src/modules/Auction.sol | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index c0972714..fccd7589 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -307,6 +307,22 @@ abstract contract Auctioneer is WithModules { module.cancelAuction(lotId_); } + /// @notice Claims a refund for a cancelled auction lot + /// @dev This function performs the following: + /// - Checks that the lot ID is valid + /// - Calls the auction module to update records and determine the amount to be refunded + /// - Sends the refund of payout tokens to the owner + /// + /// The function reverts if: + /// - The lot ID is not valid + /// - The auction module reverts + /// - The transfer of payout tokens fails + /// + /// @param lotId_ ID of the auction lot + function claimAuctionRefund(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { + // TODO + } + // TODO claim refund if pre-funded and concluded // ========== AUCTION INFORMATION ========== // diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 6ce60410..96628579 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -163,6 +163,8 @@ abstract contract Auction { function cancelAuction(uint96 id_) external virtual; + function claimAuctionRefund(uint96 lotId_) external virtual returns (uint256 refundAmount); + // ========== AUCTION INFORMATION ========== // function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); From 2aad0d2b58f1c961b7fd020dd55e98e7b229b3c4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 17:01:24 +0400 Subject: [PATCH 044/117] Rename claimRefund() to claimBidRefund() --- src/AuctionHouse.sol | 2 +- src/modules/Auction.sol | 2 +- src/modules/auctions/LSBBA/LSBBA.sol | 2 +- test/modules/Auction/MockAtomicAuctionModule.sol | 2 +- test/modules/Auction/MockAuctionModule.sol | 2 +- test/modules/Auction/MockBatchAuctionModule.sol | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 9ec7a1c2..466ab48c 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -510,7 +510,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Claim the refund on the auction module // The auction module is responsible for validating the bid and authorizing the caller AuctionModule module = _getModuleForId(lotId_); - uint256 refundAmount = module.claimRefund(lotId_, bidId_, msg.sender); + uint256 refundAmount = module.claimBidRefund(lotId_, bidId_, msg.sender); // Transfer the quote token to the bidder // The ownership of the bid has already been verified by the auction module diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 96628579..8388bcb6 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -123,7 +123,7 @@ abstract contract Auction { /// @param bidId_ The bid id /// @param bidder_ The bidder of the purchased tokens /// @return refundAmount The amount of quote tokens refunded - function claimRefund( + function claimBidRefund( uint96 lotId_, uint256 bidId_, address bidder_ diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 236a294e..e52d28e1 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -156,7 +156,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } /// @inheritdoc Auction - function claimRefund( + function claimBidRefund( uint96 lotId_, uint256 bidId_, address sender_ diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 1083d84c..22c6f348 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -111,7 +111,7 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function claimRefund(uint96, uint256, address) external virtual override returns (uint256) { + function claimBidRefund(uint96, uint256, address) external virtual override returns (uint256) { revert Auction_NotImplemented(); } } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 5d0a1d25..15061ece 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -64,7 +64,7 @@ contract MockAuctionModule is AuctionModule { function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual override {} - function claimRefund( + function claimBidRefund( uint96 lotId_, uint256 bidId_, address bidder_ diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 4b58c6ab..e171b078 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -97,7 +97,7 @@ contract MockBatchAuctionModule is AuctionModule { bidCancelled[lotId_][bidId_] = true; } - function claimRefund( + function claimBidRefund( uint96 lotId_, uint256 bidId_, address bidder_ From 2a12f319a625e88a0a115490501a58024648b97c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 17:01:34 +0400 Subject: [PATCH 045/117] Stub for claimAuctionRefund() --- src/bases/Auctioneer.sol | 16 +++++++++++++--- src/modules/auctions/LSBBA/LSBBA.sol | 7 +++++++ test/modules/Auction/MockAtomicAuctionModule.sol | 7 +++++++ test/modules/Auction/MockAuctionModule.sol | 7 +++++++ test/modules/Auction/MockBatchAuctionModule.sol | 7 +++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index fccd7589..795ae99a 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -310,20 +310,30 @@ abstract contract Auctioneer is WithModules { /// @notice Claims a refund for a cancelled auction lot /// @dev This function performs the following: /// - Checks that the lot ID is valid + /// - Checks that caller is the auction owner /// - Calls the auction module to update records and determine the amount to be refunded /// - Sends the refund of payout tokens to the owner /// /// The function reverts if: /// - The lot ID is not valid + /// - The caller is not the auction owner + /// - The auction lot is not prefunded /// - The auction module reverts /// - The transfer of payout tokens fails /// /// @param lotId_ ID of the auction lot function claimAuctionRefund(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { - // TODO - } + // Check that the auction lot is prefunded + if (lotRouting[lotId_].prefunded == false) revert InvalidParams(); - // TODO claim refund if pre-funded and concluded + // Call the auction module + AuctionModule module = _getModuleForId(lotId_); + uint256 refundAmount = module.claimAuctionRefund(lotId_); + + // Transfer payout tokens to the owner + Routing memory routing = lotRouting[lotId_]; + routing.baseToken.safeTransfer(routing.owner, refundAmount); + } // ========== AUCTION INFORMATION ========== // diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index e52d28e1..4d4ac6b9 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -427,4 +427,11 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Set auction status to settled so that bids can be refunded auctionData[lotId_].status = AuctionStatus.Settled; } + + function claimAuctionRefund(uint96 lotId_) + external + virtual + override + returns (uint256 refundAmount) + {} } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 22c6f348..c25de118 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -114,4 +114,11 @@ contract MockAtomicAuctionModule is AuctionModule { function claimBidRefund(uint96, uint256, address) external virtual override returns (uint256) { revert Auction_NotImplemented(); } + + function claimAuctionRefund(uint96 lotId_) + external + virtual + override + returns (uint256 refundAmount) + {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 15061ece..6296cc39 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -69,6 +69,13 @@ contract MockAuctionModule is AuctionModule { uint256 bidId_, address bidder_ ) external virtual override returns (uint256 refundAmount) {} + + function claimAuctionRefund(uint96 lotId_) + external + virtual + override + returns (uint256 refundAmount) + {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index e171b078..7df34435 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -160,4 +160,11 @@ contract MockBatchAuctionModule is AuctionModule { function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } + + function claimAuctionRefund(uint96 lotId_) + external + virtual + override + returns (uint256 refundAmount) + {} } From 0c09112ce8e765a34ce320dff24aadcb445a3d93 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 23 Jan 2024 19:42:52 +0400 Subject: [PATCH 046/117] Comment --- src/bases/Auctioneer.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 795ae99a..64098b70 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -311,7 +311,7 @@ abstract contract Auctioneer is WithModules { /// @dev This function performs the following: /// - Checks that the lot ID is valid /// - Checks that caller is the auction owner - /// - Calls the auction module to update records and determine the amount to be refunded + /// - Calls the auction module to validate state, update records and determine the amount to be refunded /// - Sends the refund of payout tokens to the owner /// /// The function reverts if: From 6438e0f33ab087d6e663d4518a649ed478165825 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 23 Jan 2024 17:18:19 -0600 Subject: [PATCH 047/117] review: comments and minor changes --- src/AuctionHouse.sol | 53 +++++++++++ src/modules/Auction.sol | 7 ++ src/modules/auctions/EBA.SOL | 3 - src/modules/auctions/LSBBA/LSBBA.sol | 93 +++++++++++++------ .../Auction/MockAtomicAuctionModule.sol | 7 ++ test/modules/Auction/MockAuctionModule.sol | 7 ++ .../Auction/MockBatchAuctionModule.sol | 7 ++ 7 files changed, 148 insertions(+), 29 deletions(-) delete mode 100644 src/modules/auctions/EBA.SOL diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 466ab48c..e6b6e14d 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -140,6 +140,10 @@ abstract contract Router is FeeManager { /// @param bidId_ Bid ID function cancelBid(uint96 lotId_, uint256 bidId_) external virtual; + /// @notice Settle a batch auction + /// @notice This function is used for versions with on-chain storage and bids and local settlement + function settle(uint96 lotId_) external virtual; + /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement /// @@ -432,6 +436,55 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { module.cancelBid(lotId_, bidId_, msg.sender); } + /// @inheritdoc Router + // TODO this is the version of `settle` we need for the LSBBA + function settle(uint96 lotId_) external override { + // Settle the lot on the auction module and get the winning bids + // Reverts if the auction cannot be settled yet + // Some common things to check: + // 1. Total of amounts out is not greater than capacity + // 2. Minimum price is enforced + // 3. Minimum bid size is enforced + // 4. Minimum capacity sold is enforced + AuctionModule module = _getModuleForId(lotId_); + (Auction.Bid[] memory winningBids, bytes memory auctionOutput) = module.settle(lotId_); + + // Load routing data for the lot + Routing memory routing = lotRouting[lotId_]; + + // TODO check this + // Payment and payout have already been sourced + + // Calculate fees + uint256 totalAmountInLessFees; + { + (uint256 totalAmountIn, uint256 totalFees) = + _allocateFees(winningBids, routing.quoteToken); + totalAmountInLessFees = totalAmountIn - totalFees; + } + + // Send payment in bulk to auction owner + _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); + + // TODO send last winning bidder partial refund if it is a partial fill. + + // Handle payouts to bidders + { + uint256 bidCount = winningBids.length; + for (uint256 i; i < bidCount; i++) { + // Send payout to each bidder + _sendPayout( + lotId_, + winningBids[i].bidder, + winningBids[i].minAmountOut, + routing, + auctionOutput + ); + } + } + } + + // TODO we can probably remove this version of the settle function for now. It won't be used initially. /// @inheritdoc Router /// @dev This function reverts if: /// - the lot ID is invalid diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 8388bcb6..c2baf64f 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -129,6 +129,13 @@ abstract contract Auction { address bidder_ ) external virtual returns (uint256 refundAmount); + /// @notice Settle a batch auciton + /// @notice This function is used for on-chain storage of bids and local settlement + function settle(uint96 lotId_) + external + virtual + returns (Bid[] memory winningBids_, bytes memory auctionOutput_); + /// @notice Settle a batch auction with the provided bids /// @notice This function is used for on-chain storage of bids and external settlement /// diff --git a/src/modules/auctions/EBA.SOL b/src/modules/auctions/EBA.SOL deleted file mode 100644 index b293390e..00000000 --- a/src/modules/auctions/EBA.SOL +++ /dev/null @@ -1,3 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 4d4ac6b9..ab5ddd29 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -140,6 +140,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } /// @inheritdoc Auction + // TODO need to change this to delete the bid so we don't have to decrypt it later + // Because of this, we can issue the refund immediately (needs to happen in the AuctionHouse) + // However, this will require more refactoring because, we run into a problem of using the array index as the bidId since it will change when we delete the bid + // It doesn't cost any more gas to store a uint96 bidId as part of the EncryptedBid. + // A better approach may be to create a mapping of lotId => bidId => EncryptedBid. Then, have an array of bidIds in the AuctionData struct that can be iterated over. + // This way, we can still lookup the bids by bidId for cancellation, etc. function cancelBid( uint96 lotId_, uint256 bidId_, @@ -252,36 +258,67 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { ); } - /// @notice View function that can be used to obtain the amount out and seed for a given bid by providing the private key - /// @dev This function can be used to decrypt bids off-chain if you know the private key - function decryptBid( + /// @notice View function that can be used to obtain a certain number of the next bids to decrypt off-chain + // TODO This assumes that cancelled bids have been removed, but hasn't been refactored based on the comments over `cancelBid` + function getNextBidsToDecrypt( uint96 lotId_, - uint96 bidId_, - bytes memory privateKey_ - ) external view returns (Decrypt memory) { - // Load encrypted bid - EncryptedBid memory encBid = lotEncryptedBids[lotId_][bidId_]; - - // Decrypt the encrypted amount out - (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt( - encBid.encryptedAmountOut, - abi.encodePacked(lotId_), - privateKey_, - auctionData[lotId_].publicKeyModulus - ); + uint256 number_ + ) external view returns (EncryptedBid[] memory) { + // Load next decrypt index + uint96 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; + + // Load number of bids to decrypt + uint256 len = lotEncryptedBids[lotId_].length - nextDecryptIndex; + if (number_ < len) len = number_; + + // Create array of encrypted bids + EncryptedBid[] memory bids = new EncryptedBid[](len); - // Cast the decrypted values - Decrypt memory decrypt; - decrypt.amountOut = abi.decode(amountOut, (uint256)); - decrypt.seed = uint256(seed); + // Iterate over bids and add them to the array + for (uint256 i; i < len; i++) { + bids[i] = lotEncryptedBids[lotId_][nextDecryptIndex + i]; + } - // Return the decrypt - return decrypt; + // Return array of encrypted bids + return bids; } + // Note: we may need to remove this function due to issues with chosen plaintext attacks on RSA implementations + // /// @notice View function that can be used to obtain the amount out and seed for a given bid by providing the private key + // /// @dev This function can be used to decrypt bids off-chain if you know the private key + // function decryptBid( + // uint96 lotId_, + // uint96 bidId_, + // bytes memory privateKey_ + // ) external view returns (Decrypt memory) { + // // Load encrypted bid + // EncryptedBid memory encBid = lotEncryptedBids[lotId_][bidId_]; + + // // Decrypt the encrypted amount out + // (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt( + // encBid.encryptedAmountOut, + // abi.encodePacked(lotId_), + // privateKey_, + // auctionData[lotId_].publicKeyModulus + // ); + + // // Cast the decrypted values + // Decrypt memory decrypt; + // decrypt.amountOut = abi.decode(amountOut, (uint256)); + // decrypt.seed = uint256(seed); + + // // Return the decrypt + // return decrypt; + // } + // =========== SETTLEMENT =========== // - function settle(uint96 lotId_) external onlyInternal returns (Bid[] memory winningBids_) { + function settle(uint96 lotId_) + external + override + onlyInternal + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + { // Check that auction is in the right state for settlement if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); @@ -318,7 +355,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) if (expended < auctionData[lotId_].minFilled) { auctionData[lotId_].status = AuctionStatus.Settled; - return winningBids_; + return (winningBids_, bytes("")); } else { marginalPrice = price; winningBidIndex = i; @@ -330,7 +367,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // If not, mark as settled and return no winning bids (so users can claim refunds) if (marginalPrice < auctionData[lotId_].minimumPrice) { auctionData[lotId_].status = AuctionStatus.Settled; - return winningBids_; + return (winningBids_, bytes("")); } // Auction can be settled at the marginal price if we reach this point @@ -341,6 +378,10 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { QueueBid memory qBid = queue.delMin(); // Calculate amount out + // TODO handle partial filling of the last winning bid + // amountIn, and amountOut will be lower + // Need to somehow refund the amountIn that wasn't used to the user + // We know it will always be the last bid in the returned array, can maybe do something with that uint256 amountOut = (qBid.amountIn * SCALE) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out @@ -363,7 +404,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { auctionData[lotId_].status = AuctionStatus.Settled; // Return winning bids - return winningBids_; + return (winningBids_, bytes("")); } // =========== AUCTION MANAGEMENT ========== // diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index c25de118..32677fed 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -104,6 +104,13 @@ contract MockAtomicAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function settle(uint96 lotId_) + external + virtual + override + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + {} + function settle( uint96 lotId_, Bid[] calldata winningBids_, diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 6296cc39..40f4969f 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -55,6 +55,13 @@ contract MockAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function settle(uint96 lotId_) + external + virtual + override + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + {} + function settle( uint96 lotId_, Bid[] calldata winningBids_, diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 7df34435..e13ceed1 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -150,6 +150,13 @@ contract MockBatchAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function settle(uint96 lotId_) + external + virtual + override + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + {} + function settle( uint96 lotId_, Bid[] calldata winningBids_, From 5f1ca55c54bb821028c57c13675f677426d51814 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 12:23:04 +0400 Subject: [PATCH 048/117] Refund bids at the time of cancellation --- src/AuctionHouse.sol | 33 +- src/modules/Auction.sol | 21 +- src/modules/auctions/LSBBA/LSBBA.sol | 45 +-- test/AuctionHouse/cancelBid.t.sol | 8 +- test/AuctionHouse/claimBidRefund.t.sol | 295 ------------------ .../Auction/MockAtomicAuctionModule.sol | 6 +- test/modules/Auction/MockAuctionModule.sol | 6 +- .../Auction/MockBatchAuctionModule.sol | 40 +-- 8 files changed, 49 insertions(+), 405 deletions(-) delete mode 100644 test/AuctionHouse/claimBidRefund.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index e6b6e14d..4c2b0d49 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -134,7 +134,8 @@ abstract contract Router is FeeManager { /// @notice Cancel a bid on a lot in a batch auction /// @dev The implementing function must perform the following: /// 1. Validate the bid - /// 2. Call the auction module to cancel the bid + /// 2. Pass the request to the auction module to validate and update data + /// 3. Send the refund to the bidder /// /// @param lotId_ Lot ID /// @param bidId_ Bid ID @@ -166,16 +167,6 @@ abstract contract Router is FeeManager { bytes calldata settlementData_ ) external virtual; - /// @notice Claims a refund for a failed or cancelled bid - /// @dev The implementing function must perform the following: - /// 1. Validate that the `lotId_` is valid - /// 2. Pass the request to the auction module to validate and update data - /// 3. Send the refund to the bidder - /// - /// @param lotId_ Lot ID - /// @param bidId_ Bid ID - function claimBidRefund(uint96 lotId_, uint256 bidId_) external virtual; - // ========== FEE MANAGEMENT ========== // /// @notice Sets the fee for the protocol @@ -433,7 +424,12 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Cancel the bid on the auction module // The auction module is responsible for validating the bid and authorizing the caller AuctionModule module = _getModuleForId(lotId_); - module.cancelBid(lotId_, bidId_, msg.sender); + uint256 refundAmount = module.cancelBid(lotId_, bidId_, msg.sender); + + // Transfer the quote token to the bidder + // The ownership of the bid has already been verified by the auction module + // TODO consider if another check is required + lotRouting[lotId_].quoteToken.safeTransfer(msg.sender, refundAmount); } /// @inheritdoc Router @@ -558,19 +554,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } - /// @inheritdoc Router - function claimBidRefund(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { - // Claim the refund on the auction module - // The auction module is responsible for validating the bid and authorizing the caller - AuctionModule module = _getModuleForId(lotId_); - uint256 refundAmount = module.claimBidRefund(lotId_, bidId_, msg.sender); - - // Transfer the quote token to the bidder - // The ownership of the bid has already been verified by the auction module - // TODO consider if another check is required - lotRouting[lotId_].quoteToken.safeTransfer(msg.sender, refundAmount); - } - // // External submission and evaluation // function settle(uint256 id_, ExternalSettlement memory settlement_) external override { // // Load routing data for the lot diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index c2baf64f..48f9e7db 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -111,23 +111,16 @@ abstract contract Auction { /// @param lotId_ The lot id /// @param bidId_ The bid id /// @param bidder_ The bidder of the purchased tokens - function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual; - - /// @notice Claim a refund for a bid - /// @dev The implementing function should handle the following: - /// - Validate the bid parameters - /// - Authorize `bidder_` - /// - Update the bid data - /// - /// @param lotId_ The lot id - /// @param bidId_ The bid id - /// @param bidder_ The bidder of the purchased tokens - /// @return refundAmount The amount of quote tokens refunded - function claimBidRefund( + /// @return bidAmount The amount of quote tokens to refund + function cancelBid( uint96 lotId_, uint256 bidId_, address bidder_ - ) external virtual returns (uint256 refundAmount); + ) external virtual returns (uint256 bidAmount); + + // _revertIfCancelled + // _revertIfLotActive + // _revertIfBidderNotOwner /// @notice Settle a batch auciton /// @notice This function is used for on-chain storage of bids and local settlement diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index ab5ddd29..cd78853e 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -140,6 +140,12 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } /// @inheritdoc Auction + /// @dev This function reverts if: + /// - the auction lot does not exist + /// - the auction lot is not live + /// - the bid does not exist + /// - `bidder_` is not the bidder + /// - the bid is already cancelled // TODO need to change this to delete the bid so we don't have to decrypt it later // Because of this, we can issue the refund immediately (needs to happen in the AuctionHouse) // However, this will require more refactoring because, we run into a problem of using the array index as the bidId since it will change when we delete the bid @@ -150,45 +156,24 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { uint96 lotId_, uint256 bidId_, address bidder_ - ) external override onlyInternal auctionIsLive(lotId_) onlyBidder(bidder_, lotId_, bidId_) { - // Validate inputs - // Bid is not already cancelled - if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { - revert Auction_AlreadyCancelled(); - } - - // Set bid status to cancelled - lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; - } - - /// @inheritdoc Auction - function claimBidRefund( - uint96 lotId_, - uint256 bidId_, - address sender_ ) external override onlyInternal - onlyBidder(sender_, lotId_, bidId_) + auctionIsLive(lotId_) + onlyBidder(bidder_, lotId_, bidId_) returns (uint256 refundAmount) { // Validate inputs - // Auction for must have settled to claim refund - // User must not have won the auction or claimed a refund already - // TODO should we allow cancel bids to claim earlier? - // Might allow legit users to change their bids - // But also allows a malicious user to use the same funds to create - // multiple bids in an attempt to grief the settlement - BidStatus bidStatus = lotEncryptedBids[lotId_][bidId_].status; - if ( - auctionData[lotId_].status != AuctionStatus.Settled || bidStatus == BidStatus.Refunded - || bidStatus == BidStatus.Won - ) revert Auction_WrongState(); + // Bid is not already cancelled + if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { + revert Auction_AlreadyCancelled(); + } - // Set bid status to refunded - lotEncryptedBids[lotId_][bidId_].status = BidStatus.Refunded; + // Set bid status to cancelled + lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; + // Return the amount to be refunded return lotEncryptedBids[lotId_][bidId_].amount; } diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index 6db54ed2..a8a47045 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -168,7 +168,7 @@ contract CancelBidTest is Test, Permit2User { // [X] it reverts // [X] given the caller is not the bid owner // [X] it reverts - // [X] it cancels the bid + // [X] it cancels the bid and transfers the quote tokens back to the bidder function test_invalidLotId_reverts() external { bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); @@ -251,11 +251,17 @@ contract CancelBidTest is Test, Permit2User { } function test_itCancelsTheBid() external givenLotIsCreated givenBidIsCreated { + // Get alice's balance + uint256 aliceBalance = quoteToken.balanceOf(alice); + // Call the function vm.prank(alice); auctionHouse.cancelBid(lotId, bidId); // Assert the bid is cancelled assertTrue(mockAuctionModule.bidCancelled(lotId, bidId)); + + // Expect alice's balance to increase + assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); } } diff --git a/test/AuctionHouse/claimBidRefund.t.sol b/test/AuctionHouse/claimBidRefund.t.sol deleted file mode 100644 index 9fa38db4..00000000 --- a/test/AuctionHouse/claimBidRefund.t.sol +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; - -// Mocks -import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; -import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; -import {MockBatchAuctionModule} from "test/modules/Auction/MockBatchAuctionModule.sol"; -import {Permit2User} from "test/lib/permit2/Permit2User.sol"; -import {MockAllowlist} from "test/modules/Auction/MockAllowlist.sol"; - -// Auctions -import {AuctionHouse, Router} from "src/AuctionHouse.sol"; -import {Auction} from "src/modules/Auction.sol"; -import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; - -// Modules -import { - Keycode, - toKeycode, - Veecode, - wrapVeecode, - unwrapVeecode, - fromVeecode, - WithModules, - Module -} from "src/modules/Modules.sol"; - -contract ClaimnBidRefundTest is Test, Permit2User { - MockERC20 internal baseToken; - MockERC20 internal quoteToken; - MockBatchAuctionModule internal mockAuctionModule; - - AuctionHouse internal auctionHouse; - - uint48 internal auctionDuration = 1 days; - - address internal immutable protocol = address(0x2); - address internal immutable referrer = address(0x4); - address internal immutable auctionOwner = address(0x5); - address internal immutable recipient = address(0x6); - address internal immutable alice = address(0x7); - - uint256 internal constant BID_AMOUNT = 1e18; - - Auctioneer.RoutingParams internal routingParams; - Auction.AuctionParams internal auctionParams; - - // Function parameters (can be modified) - uint96 internal lotId; - uint256 internal bidId; - - function setUp() external { - // Set block timestamp - vm.warp(1_000_000); - - baseToken = new MockERC20("Base Token", "BASE", 18); - quoteToken = new MockERC20("Quote Token", "QUOTE", 18); - - auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); - mockAuctionModule = new MockBatchAuctionModule(address(auctionHouse)); - - auctionHouse.installModule(mockAuctionModule); - - auctionParams = Auction.AuctionParams({ - start: uint48(block.timestamp), - duration: auctionDuration, - capacityInQuote: false, - capacity: 10e18, - implParams: abi.encode("") - }); - - routingParams = Auctioneer.RoutingParams({ - auctionType: toKeycode("BATCH"), - baseToken: baseToken, - quoteToken: quoteToken, - hooks: IHooks(address(0)), - allowlist: IAllowlist(address(0)), - allowlistParams: abi.encode(""), - payoutData: abi.encode(""), - derivativeType: toKeycode(""), - derivativeParams: abi.encode("") - }); - } - - modifier givenLotIsCreated() { - vm.prank(auctionOwner); - lotId = auctionHouse.auction(routingParams, auctionParams); - _; - } - - modifier givenLotIsCancelled() { - vm.prank(auctionOwner); - auctionHouse.cancel(lotId); - _; - } - - modifier givenLotIsAtomicAuction() { - // Install the atomic auction module - MockAtomicAuctionModule mockAtomicAuctionModule = - new MockAtomicAuctionModule(address(auctionHouse)); - auctionHouse.installModule(mockAtomicAuctionModule); - - // Update routing parameters - (Keycode moduleKeycode,) = unwrapVeecode(mockAtomicAuctionModule.VEECODE()); - routingParams.auctionType = moduleKeycode; - - vm.prank(auctionOwner); - lotId = auctionHouse.auction(routingParams, auctionParams); - _; - } - - modifier givenLotIsConcluded() { - vm.warp(block.timestamp + auctionDuration + 1); - _; - } - - modifier whenLotIdIsInvalid() { - lotId = 255; - _; - } - - modifier givenBidIsCreated() { - // Mint quote tokens to alice - quoteToken.mint(alice, BID_AMOUNT); - - // Approve spending - vm.prank(alice); - quoteToken.approve(address(auctionHouse), BID_AMOUNT); - - // Create the bid - Router.BidParams memory bidParams = Router.BidParams({ - lotId: lotId, - recipient: recipient, - referrer: referrer, - amount: BID_AMOUNT, - auctionData: bytes(""), - allowlistProof: bytes(""), - permit2Data: bytes("") - }); - - vm.prank(alice); - auctionHouse.bid(bidParams); - _; - } - - modifier givenBidIsCancelled() { - vm.prank(alice); - auctionHouse.cancelBid(lotId, bidId); - _; - } - - // claimBidRefund - // [ ] given the auction lot does not exist - // [ ] it reverts - // [ ] given the auction lot is an atomic auction - // [ ] it reverts - // [ ] given the bid does not exist - // [ ] it reverts - // [ ] given the bid is not cancelled - // [ ] it reverts - // [ ] given the caller is not the bid owner - // [ ] it reverts - // [ ] given the bid refund has already been claimed - // [ ] it reverts - // [ ] given the auction lot is concluded - // [ ] it refunds the bid - // [ ] given the auction lot is cancelled - // [ ] it refunds the bid - - function test_noAuction_reverts() external { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); - vm.expectRevert(err); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_givenAtomicAuction_reverts() external givenLotIsAtomicAuction { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_NotImplemented.selector); - vm.expectRevert(err); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_noBid_reverts() external givenLotIsCreated { - // Expect revert - bytes memory err = - abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); - vm.expectRevert(err); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_bidNotCancelled_reverts() external givenLotIsCreated givenBidIsCreated { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); - vm.expectRevert(err); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_notBidOwner_reverts() - external - givenLotIsCreated - givenBidIsCreated - givenBidIsCancelled - { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); - vm.expectRevert(err); - - // Call as a different user - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_bidRefundClaimed_reverts() - external - givenLotIsCreated - givenBidIsCreated - givenBidIsCancelled - { - // Claim the bid refund - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); - vm.expectRevert(err); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - } - - function test_lotConcluded_refundsBid() - external - givenLotIsCreated - givenBidIsCreated - givenBidIsCancelled - givenLotIsConcluded - { - // Get alice's balance - uint256 aliceBalance = quoteToken.balanceOf(alice); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - - // Expect alice's balance to increase - assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); - } - - function test_lotCancelled_refundsBid() - external - givenLotIsCreated - givenBidIsCreated - givenBidIsCancelled - givenLotIsCancelled - { - // Get alice's balance - uint256 aliceBalance = quoteToken.balanceOf(alice); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - - // Expect alice's balance to increase - assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); - } - - function test_refundsBid() external givenLotIsCreated givenBidIsCreated givenBidIsCancelled { - // Get alice's balance - uint256 aliceBalance = quoteToken.balanceOf(alice); - - // Call - vm.prank(alice); - auctionHouse.claimBidRefund(lotId, bidId); - - // Expect alice's balance to increase - assertEq(quoteToken.balanceOf(alice), aliceBalance + BID_AMOUNT); - } -} diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 32677fed..7dbbe6c5 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -81,7 +81,7 @@ contract MockAtomicAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function cancelBid(uint96, uint256, address) external virtual override { + function cancelBid(uint96, uint256, address) external virtual override returns (uint256) { revert Auction_NotImplemented(); } @@ -118,10 +118,6 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function claimBidRefund(uint96, uint256, address) external virtual override returns (uint256) { - revert Auction_NotImplemented(); - } - function claimAuctionRefund(uint96 lotId_) external virtual diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 40f4969f..4ba7fcaa 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -69,13 +69,11 @@ contract MockAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function cancelBid(uint96 lotId_, uint256 bidId_, address bidder_) external virtual override {} - - function claimBidRefund( + function cancelBid( uint96 lotId_, uint256 bidId_, address bidder_ - ) external virtual override returns (uint256 refundAmount) {} + ) external virtual override returns (uint256) {} function claimAuctionRefund(uint96 lotId_) external diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index e13ceed1..0948a899 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -77,7 +77,14 @@ contract MockBatchAuctionModule is AuctionModule { uint96 lotId_, uint256 bidId_, address bidder_ - ) external virtual override isLotValid(lotId_) isLotActive(lotId_) { + ) + external + virtual + override + isLotValid(lotId_) + isLotActive(lotId_) + returns (uint256 refundAmount) + { // Check that the bid exists if (bidData[lotId_].length <= bidId_) { revert Auction.Auction_InvalidBidId(lotId_, bidId_); @@ -95,40 +102,11 @@ contract MockBatchAuctionModule is AuctionModule { // Cancel the bid bidCancelled[lotId_][bidId_] = true; - } - - function claimBidRefund( - uint96 lotId_, - uint256 bidId_, - address bidder_ - ) external virtual override returns (uint256 refundAmount) { - // Check that the bid exists - if (bidData[lotId_].length <= bidId_) { - revert Auction.Auction_InvalidBidId(lotId_, bidId_); - } - - // Check that the bid has been cancelled - if (bidCancelled[lotId_][bidId_] == false) { - revert Auction.Auction_InvalidParams(); - } - - // Check that the bidder is the owner of the bid - if (bidData[lotId_][bidId_].bidder != bidder_) { - revert Auction.Auction_NotBidder(); - } - - // Check that the bid has not been refunded - if (bidRefunded[lotId_][bidId_] == true) { - revert Auction.Auction_InvalidParams(); - } - - // Get the bid amount - refundAmount = bidData[lotId_][bidId_].amount; // Mark the bid as refunded bidRefunded[lotId_][bidId_] = true; - return refundAmount; + return bidData[lotId_][bidId_].amount; } function settle( From 69c2c225650703fa92e790beae0484b9ed72eb1b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 12:58:23 +0400 Subject: [PATCH 049/117] Auction cancellation returns the pre-funded amount to the owner --- src/bases/Auctioneer.sol | 43 +++---- src/modules/Auction.sol | 13 ++- src/modules/auctions/LSBBA/LSBBA.sol | 7 -- test/AuctionHouse/cancel.t.sol | 108 ++++++++++++++++-- .../Auction/MockAtomicAuctionModule.sol | 10 +- test/modules/Auction/MockAuctionModule.sol | 7 -- .../Auction/MockBatchAuctionModule.sol | 7 -- 7 files changed, 129 insertions(+), 66 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 64098b70..ebb0371b 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -294,45 +294,34 @@ abstract contract Auctioneer is WithModules { } /// @notice Cancels an auction lot - /// @dev The function reverts if: - /// - The caller is not the auction owner - /// - The lot ID is invalid - /// - The respective auction module reverts - /// - /// @param lotId_ ID of the auction lot - function cancel(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { - AuctionModule module = _getModuleForId(lotId_); - - // Cancel the auction on the module - module.cancelAuction(lotId_); - } - - /// @notice Claims a refund for a cancelled auction lot /// @dev This function performs the following: /// - Checks that the lot ID is valid /// - Checks that caller is the auction owner /// - Calls the auction module to validate state, update records and determine the amount to be refunded - /// - Sends the refund of payout tokens to the owner + /// - If prefunded, sends the refund of payout tokens to the owner /// /// The function reverts if: - /// - The lot ID is not valid + /// - The lot ID is invalid /// - The caller is not the auction owner - /// - The auction lot is not prefunded - /// - The auction module reverts + /// - The respective auction module reverts /// - The transfer of payout tokens fails /// /// @param lotId_ ID of the auction lot - function claimAuctionRefund(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { - // Check that the auction lot is prefunded - if (lotRouting[lotId_].prefunded == false) revert InvalidParams(); - - // Call the auction module + function cancel(uint96 lotId_) external isLotValid(lotId_) isLotOwner(lotId_) { AuctionModule module = _getModuleForId(lotId_); - uint256 refundAmount = module.claimAuctionRefund(lotId_); - // Transfer payout tokens to the owner - Routing memory routing = lotRouting[lotId_]; - routing.baseToken.safeTransfer(routing.owner, refundAmount); + // Get remaining capacity from module + uint256 lotRemainingCapacity = module.remainingCapacity(lotId_); + + // Cancel the auction on the module + module.cancelAuction(lotId_); + + // If the auction is prefunded, transfer the remaining capacity to the owner + if (lotRouting[lotId_].prefunded) { + // Transfer payout tokens to the owner + Routing memory routing = lotRouting[lotId_]; + routing.baseToken.safeTransfer(routing.owner, lotRemainingCapacity); + } } // ========== AUCTION INFORMATION ========== // diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 48f9e7db..ddbfebe0 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -161,9 +161,16 @@ abstract contract Auction { AuctionParams memory params_ ) external virtual returns (bool prefundingRequired, uint256 capacity); - function cancelAuction(uint96 id_) external virtual; + /// @notice Cancel an auction lot + /// @dev The implementing function should handle the following: + /// - Validate the lot parameters + /// - Update the lot data + /// - Return the remaining capacity (so that the AuctionHouse can refund the owner) + /// + /// @param lotId_ The lot id + function cancelAuction(uint96 lotId_) external virtual; - function claimAuctionRefund(uint96 lotId_) external virtual returns (uint256 refundAmount); + // _revertIf... // ========== AUCTION INFORMATION ========== // @@ -264,7 +271,7 @@ abstract contract AuctionModule is Auction, Module { _cancelAuction(lotId_); } - function _cancelAuction(uint96 id_) internal virtual; + function _cancelAuction(uint96 lotId_) internal virtual; // ========== AUCTION INFORMATION ========== // diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index cd78853e..77c49dd9 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -453,11 +453,4 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Set auction status to settled so that bids can be refunded auctionData[lotId_].status = AuctionStatus.Settled; } - - function claimAuctionRefund(uint96 lotId_) - external - virtual - override - returns (uint256 refundAmount) - {} } diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancel.t.sol index 795df7d0..28a5744c 100644 --- a/test/AuctionHouse/cancel.t.sol +++ b/test/AuctionHouse/cancel.t.sol @@ -7,11 +7,11 @@ import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; // Mocks import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; -import {MockAuctionModule} from "test/modules/Auction/MockAuctionModule.sol"; +import {MockAtomicAuctionModule} from "test/modules/Auction/MockAtomicAuctionModule.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; // Auctions -import {AuctionHouse} from "src/AuctionHouse.sol"; +import {AuctionHouse, Router} from "src/AuctionHouse.sol"; import {Auction} from "src/modules/Auction.sol"; import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; @@ -26,10 +26,10 @@ import { Module } from "src/modules/Modules.sol"; -contract CancelTest is Test, Permit2User { +contract CancelAuctionTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; - MockAuctionModule internal mockAuctionModule; + MockAtomicAuctionModule internal mockAuctionModule; AuctionHouse internal auctionHouse; Auctioneer.RoutingParams internal routingParams; @@ -40,13 +40,18 @@ contract CancelTest is Test, Permit2User { address internal auctionOwner = address(0x1); address internal protocol = address(0x2); + address internal alice = address(0x3); + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint256 internal constant PURCHASE_AMOUNT = 1e18; function setUp() external { baseToken = new MockERC20("Base Token", "BASE", 18); quoteToken = new MockERC20("Quote Token", "QUOTE", 18); auctionHouse = new AuctionHouse(auctionOwner, _PERMIT2_ADDRESS); - mockAuctionModule = new MockAuctionModule(address(auctionHouse)); + mockAuctionModule = new MockAtomicAuctionModule(address(auctionHouse)); auctionHouse.installModule(mockAuctionModule); @@ -54,12 +59,12 @@ contract CancelTest is Test, Permit2User { start: uint48(block.timestamp), duration: uint48(1 days), capacityInQuote: false, - capacity: 10e18, + capacity: LOT_CAPACITY, implParams: abi.encode("") }); routingParams = Auctioneer.RoutingParams({ - auctionType: toKeycode("MOCK"), + auctionType: toKeycode("ATOM"), baseToken: baseToken, quoteToken: quoteToken, hooks: IHooks(address(0)), @@ -81,7 +86,9 @@ contract CancelTest is Test, Permit2User { // [X] reverts if not the owner // [X] reverts if lot is not active // [X] reverts if lot id is invalid - // [X] sets the lot to inactive on the AuctionModule + // [X] reverts if the lot is already cancelled + // [X] given the auction is not prefunded + // [X] it sets the lot to inactive on the AuctionModule function testReverts_whenNotAuctionOwner() external whenLotIsCreated { bytes memory err = @@ -120,6 +127,20 @@ contract CancelTest is Test, Permit2User { auctionHouse.cancel(lotId); } + function test_givenCancelled_reverts() external whenLotIsCreated { + // Cancel the lot + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call the function + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + } + function test_success() external whenLotIsCreated { assertTrue(mockAuctionModule.isLive(lotId), "before cancellation: isLive mismatch"); @@ -133,4 +154,75 @@ contract CancelTest is Test, Permit2User { assertFalse(mockAuctionModule.isLive(lotId), "after cancellation: isLive mismatch"); } + + // [X] given the auction is prefunded + // [X] it refunds the prefunded amount in payout tokens to the owner + // [X] given a purchase has been made + // [X] it refunds the remaining prefunded amount in payout tokens to the owner + + modifier givenLotIsPrefunded() { + mockAuctionModule.setRequiredPrefunding(true); + + // Mint payout tokens to the owner + baseToken.mint(auctionOwner, LOT_CAPACITY); + + // Approve transfer to the auction house + vm.prank(auctionOwner); + baseToken.approve(address(auctionHouse), LOT_CAPACITY); + _; + } + + modifier givenPurchase() { + // Mint quote tokens to alice + quoteToken.mint(alice, PURCHASE_AMOUNT); + + // Approve spending + vm.prank(alice); + quoteToken.approve(address(auctionHouse), PURCHASE_AMOUNT); + + // Create the purchase + Router.PurchaseParams memory purchaseParams = Router.PurchaseParams({ + recipient: alice, + referrer: address(0), + lotId: lotId, + amount: PURCHASE_AMOUNT, + minAmountOut: PURCHASE_AMOUNT, + auctionData: bytes(""), + allowlistProof: bytes(""), + permit2Data: bytes("") + }); + + vm.prank(alice); + auctionHouse.purchase(purchaseParams); + _; + } + + function test_prefunded() external givenLotIsPrefunded whenLotIsCreated { + // Check the owner's balance + uint256 ownerBalance = baseToken.balanceOf(auctionOwner); + + // Cancel the lot + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + + // Check the owner's balance + assertEq(baseToken.balanceOf(auctionOwner), ownerBalance + LOT_CAPACITY); + } + + function test_prefunded_givenPurchase() + external + givenLotIsPrefunded + whenLotIsCreated + givenPurchase + { + // Check the owner's balance + uint256 ownerBalance = baseToken.balanceOf(auctionOwner); + + // Cancel the lot + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + + // Check the owner's balance + assertEq(baseToken.balanceOf(auctionOwner), ownerBalance + LOT_CAPACITY - PURCHASE_AMOUNT); + } } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 7dbbe6c5..295883ca 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -57,6 +57,9 @@ contract MockAtomicAuctionModule is AuctionModule { payout = (payoutData[id_] * amount_) / 1e5; } + // Reduce capacity + lotData[id_].capacity -= payout; + Output memory output = Output({multiplier: 1}); auctionOutput = abi.encode(output); @@ -117,11 +120,4 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementProof_, bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - - function claimAuctionRefund(uint96 lotId_) - external - virtual - override - returns (uint256 refundAmount) - {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 4ba7fcaa..acb6c783 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -74,13 +74,6 @@ contract MockAuctionModule is AuctionModule { uint256 bidId_, address bidder_ ) external virtual override returns (uint256) {} - - function claimAuctionRefund(uint96 lotId_) - external - virtual - override - returns (uint256 refundAmount) - {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 0948a899..d67bcb89 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -145,11 +145,4 @@ contract MockBatchAuctionModule is AuctionModule { function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } - - function claimAuctionRefund(uint96 lotId_) - external - virtual - override - returns (uint256 refundAmount) - {} } From f7daf580eaa9ba1a6ad5dc473261fe270d77be5a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 12:59:42 +0400 Subject: [PATCH 050/117] Rename test file --- test/AuctionHouse/{cancel.t.sol => cancelAuction.t.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/AuctionHouse/{cancel.t.sol => cancelAuction.t.sol} (100%) diff --git a/test/AuctionHouse/cancel.t.sol b/test/AuctionHouse/cancelAuction.t.sol similarity index 100% rename from test/AuctionHouse/cancel.t.sol rename to test/AuctionHouse/cancelAuction.t.sol From 988e2f64a8c911ee5562f98a4f3096f921d1019e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 13:01:50 +0400 Subject: [PATCH 051/117] Check for non-zero remaining capacity --- src/bases/Auctioneer.sol | 2 +- test/AuctionHouse/cancelAuction.t.sol | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index ebb0371b..74704fe7 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -317,7 +317,7 @@ abstract contract Auctioneer is WithModules { module.cancelAuction(lotId_); // If the auction is prefunded, transfer the remaining capacity to the owner - if (lotRouting[lotId_].prefunded) { + if (lotRouting[lotId_].prefunded && lotRemainingCapacity > 0) { // Transfer payout tokens to the owner Routing memory routing = lotRouting[lotId_]; routing.baseToken.safeTransfer(routing.owner, lotRemainingCapacity); diff --git a/test/AuctionHouse/cancelAuction.t.sol b/test/AuctionHouse/cancelAuction.t.sol index 28a5744c..172018f3 100644 --- a/test/AuctionHouse/cancelAuction.t.sol +++ b/test/AuctionHouse/cancelAuction.t.sol @@ -172,21 +172,21 @@ contract CancelAuctionTest is Test, Permit2User { _; } - modifier givenPurchase() { + modifier givenPurchase(uint256 amount_) { // Mint quote tokens to alice - quoteToken.mint(alice, PURCHASE_AMOUNT); + quoteToken.mint(alice, amount_); // Approve spending vm.prank(alice); - quoteToken.approve(address(auctionHouse), PURCHASE_AMOUNT); + quoteToken.approve(address(auctionHouse), amount_); // Create the purchase Router.PurchaseParams memory purchaseParams = Router.PurchaseParams({ recipient: alice, referrer: address(0), lotId: lotId, - amount: PURCHASE_AMOUNT, - minAmountOut: PURCHASE_AMOUNT, + amount: amount_, + minAmountOut: amount_, auctionData: bytes(""), allowlistProof: bytes(""), permit2Data: bytes("") @@ -213,7 +213,7 @@ contract CancelAuctionTest is Test, Permit2User { external givenLotIsPrefunded whenLotIsCreated - givenPurchase + givenPurchase(PURCHASE_AMOUNT) { // Check the owner's balance uint256 ownerBalance = baseToken.balanceOf(auctionOwner); From 96b6e752a76966f031b61e68ef56491381477097 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 13:06:55 +0400 Subject: [PATCH 052/117] Documentation --- src/modules/Auction.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index ddbfebe0..a0c747d0 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -170,8 +170,6 @@ abstract contract Auction { /// @param lotId_ The lot id function cancelAuction(uint96 lotId_) external virtual; - // _revertIf... - // ========== AUCTION INFORMATION ========== // function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); @@ -248,8 +246,11 @@ abstract contract AuctionModule is Auction, Module { ) internal virtual returns (bool prefundingRequired); /// @notice Cancel an auction lot - /// @dev Owner is stored in the Routing information on the AuctionHouse, so we check permissions there - /// @dev This function reverts if: + /// @dev Assumptions: + /// - The parent will refund the owner the remaining capacity + /// - The parent will verify that the caller is the owner + /// + /// This function reverts if: /// - the caller is not the parent of the module /// - the lot id is invalid /// - the lot is not active @@ -271,6 +272,10 @@ abstract contract AuctionModule is Auction, Module { _cancelAuction(lotId_); } + /// @notice Implementation-specific auction cancellation logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID function _cancelAuction(uint96 lotId_) internal virtual; // ========== AUCTION INFORMATION ========== // From c1960410a8a03376fdefa783713979a89791f0c5 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 14:16:27 +0400 Subject: [PATCH 053/117] Implement pattern for implementation-specific logic with AuctionModule --- src/modules/Auction.sol | 70 +++++++++++++++++++ src/modules/auctions/LSBBA/LSBBA.sol | 19 ++++- src/modules/auctions/bases/BatchAuction.sol | 6 +- .../Auction/MockAtomicAuctionModule.sol | 4 +- test/modules/Auction/MockAuctionModule.sol | 4 +- .../Auction/MockBatchAuctionModule.sol | 14 +--- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a0c747d0..ec58c08d 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -278,6 +278,56 @@ abstract contract AuctionModule is Auction, Module { /// @param lotId_ The lot ID function _cancelAuction(uint96 lotId_) internal virtual; + // ========== BID MANAGEMENT ========== // + + /// @inheritdoc Auction + /// @dev Implements a basic bid function that: + /// - Calls implementation-specific validation logic + /// - Calls the auction module + /// + /// This function reverts if: + /// - the lot id is invalid + /// - the lot is not active + /// - the caller is not an internal module + /// + /// Inheriting contracts should override _bid to implement auction-specific logic, such as: + /// - Validating the auction-specific parameters + /// - Storing the bid data + function bid( + uint96 lotId_, + address bidder_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_ + ) external override onlyInternal returns (uint256 bidId) { + // Standard validation + _revertIfLotInvalid(lotId_); + _revertIfLotInactive(lotId_); + + // Call implementation-specific logic + return _bid(lotId_, bidder_, recipient_, referrer_, amount_, auctionData_); + } + + /// @notice Implementation-specific bid logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID + /// @param bidder_ The bidder of the purchased tokens + /// @param recipient_ The recipient of the purchased tokens + /// @param referrer_ The referrer of the bid + /// @param amount_ The amount of quote tokens to bid + /// @param auctionData_ The auction-specific data + /// @return bidId The bid ID + function _bid( + uint96 lotId_, + address bidder_, + address recipient_, + address referrer_, + uint256 amount_, + bytes calldata auctionData_ + ) internal virtual returns (uint256 bidId); + // ========== AUCTION INFORMATION ========== // // TODO does this need to change for batch auctions? @@ -294,6 +344,26 @@ abstract contract AuctionModule is Auction, Module { // ========== MODIFIERS ========== // + /// @notice Checks that `lotId_` is valid + /// @dev Should revert if the lot ID is invalid + /// Inheriting contracts can override this to implement custom logic + /// + /// @param lotId_ The lot ID + function _revertIfLotInvalid(uint96 lotId_) internal view virtual { + if (lotData[lotId_].start == 0) revert Auction_InvalidLotId(lotId_); + } + + /// @notice Checks that the lot represented by `lotId_` is active + /// @dev Should revert if the lot is not active + /// Inheriting contracts can override this to implement custom logic + /// + /// @param lotId_ The lot ID + function _revertIfLotInactive(uint96 lotId_) internal view virtual { + if (!isLive(lotId_)) revert Auction_MarketNotActive(lotId_); + } + + // TODO remove these + /// @notice Checks that the lot ID is valid /// @dev Reverts if the lot ID is invalid /// diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 77c49dd9..62c4333b 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -109,15 +109,15 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== BID =========== // - /// @inheritdoc Auction - function bid( + /// @inheritdoc AuctionModule + function _bid( uint96 lotId_, address bidder_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external override onlyInternal auctionIsLive(lotId_) returns (uint256 bidId) { + ) internal override returns (uint256 bidId) { // Validate inputs // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); @@ -137,6 +137,19 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Add bid to lot lotEncryptedBids[lotId_].push(userBid); + + return bidId; + } + + /// @inheritdoc AuctionModule + /// @dev Checks that the lot is active with the data structures used by this particular module + function _revertIfLotInactive(uint96 lotId_) internal view override { + // Check that bids are allowed to be submitted for the lot + if ( + auctionData[lotId_].status != AuctionStatus.Created + || block.timestamp < lotData[lotId_].start + || block.timestamp >= lotData[lotId_].conclusion + ) revert Auction_NotLive(); } /// @inheritdoc Auction diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 18964aa8..75b30a28 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -38,15 +38,15 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; - /// @inheritdoc Auction - function bid( + /// @inheritdoc AuctionModule + function _bid( uint96 lotId_, address bidder_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external override onlyParent returns (uint256 bidId) { + ) internal override returns (uint256 bidId) { // TODO // Validate inputs diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index 295883ca..f057fc6d 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -73,14 +73,14 @@ contract MockAtomicAuctionModule is AuctionModule { purchaseReverts = reverts_; } - function bid( + function _bid( uint96, address, address, address, uint256, bytes calldata - ) external virtual override returns (uint256) { + ) internal override returns (uint256) { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index acb6c783..b54ad774 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -32,14 +32,14 @@ contract MockAuctionModule is AuctionModule { bytes calldata auctionData_ ) external virtual override returns (uint256 payout, bytes memory auctionOutput) {} - function bid( + function _bid( uint96 id_, address bidder_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external virtual override returns (uint256) {} + ) internal override returns (uint256) {} function payoutFor( uint256 id_, diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index d67bcb89..02a34b86 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -38,24 +38,14 @@ contract MockBatchAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function bid( + function _bid( uint96 lotId_, address bidder_, address recipient_, address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external virtual override returns (uint256) { - // Valid lot - if (lotData[lotId_].start == 0) { - revert Auction.Auction_InvalidLotId(lotId_); - } - - // If auction is cancelled - if (isLive(lotId_) == false) { - revert Auction.Auction_MarketNotActive(lotId_); - } - + ) internal override returns (uint256) { // Create a new bid Bid memory newBid = Bid({ bidder: bidder_, From 838ba91e120ea97ab9e87bc1d0600d09fce756bd Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 14:38:42 +0400 Subject: [PATCH 054/117] Implement pattern for standard and implementation logic when calling cancelBid() --- src/modules/Auction.sol | 76 ++++++++++++++++++- src/modules/auctions/LSBBA/LSBBA.sol | 63 +++++++-------- .../Auction/MockAtomicAuctionModule.sol | 12 ++- test/modules/Auction/MockAuctionModule.sol | 14 +++- .../Auction/MockBatchAuctionModule.sol | 53 +++++++------ 5 files changed, 153 insertions(+), 65 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index ec58c08d..16dc2c4a 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -278,7 +278,7 @@ abstract contract AuctionModule is Auction, Module { /// @param lotId_ The lot ID function _cancelAuction(uint96 lotId_) internal virtual; - // ========== BID MANAGEMENT ========== // + // ========== BATCH AUCTIONS ========== // /// @inheritdoc Auction /// @dev Implements a basic bid function that: @@ -328,6 +328,51 @@ abstract contract AuctionModule is Auction, Module { bytes calldata auctionData_ ) internal virtual returns (uint256 bidId); + /// @inheritdoc Auction + /// @dev Implements a basic cancelBid function that: + /// - Calls implementation-specific validation logic + /// - Calls the auction module + /// + /// This function reverts if: + /// - the lot id is invalid + /// - the lot is not active + /// - the bid id is invalid + /// - the bid is already cancelled + /// - `caller_` is not the bid owner + /// - the caller is not an internal module + /// + /// Inheriting contracts should override _cancelBid to implement auction-specific logic, such as: + /// - Validating the auction-specific parameters + /// - Updating the bid data + function cancelBid( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) external override onlyInternal returns (uint256 bidAmount) { + // Standard validation + _revertIfLotInvalid(lotId_); + _revertIfLotInactive(lotId_); + _revertIfBidInvalid(lotId_, bidId_); + _revertIfNotBidOwner(lotId_, bidId_, caller_); + _revertIfBidCancelled(lotId_, bidId_); + + // Call implementation-specific logic + return _cancelBid(lotId_, bidId_, caller_); + } + + /// @notice Implementation-specific bid cancellation logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + /// @param caller_ The caller + /// @return bidAmount The amount of quote tokens to refund + function _cancelBid( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) internal virtual returns (uint256 bidAmount); + // ========== AUCTION INFORMATION ========== // // TODO does this need to change for batch auctions? @@ -362,6 +407,35 @@ abstract contract AuctionModule is Auction, Module { if (!isLive(lotId_)) revert Auction_MarketNotActive(lotId_); } + /// @notice Checks that the lot and bid combination is valid + /// @dev Should revert if the bid is invalid + /// Inheriting contracts must override this to implement custom logic + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual; + + /// @notice Checks that `caller_` is the bid owner + /// @dev Should revert if `caller_` is not the bid owner + /// Inheriting contracts must override this to implement custom logic + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + /// @param caller_ The caller + function _revertIfNotBidOwner( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) internal view virtual; + + /// @notice Checks that the bid is not cancelled + /// @dev Should revert if the bid is cancelled + /// Inheriting contracts must override this to implement custom logic + /// + /// @param lotId_ The lot ID + /// @param bidId_ The bid ID + function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual; + // TODO remove these /// @notice Checks that the lot ID is valid diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 62c4333b..dcf0c045 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -88,23 +88,42 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== MODIFIERS ========== // - modifier auctionIsLive(uint96 lotId_) { + /// @inheritdoc AuctionModule + /// @dev Checks that the lot is active with the data structures used by this particular module + function _revertIfLotInactive(uint96 lotId_) internal view override { // Check that bids are allowed to be submitted for the lot if ( auctionData[lotId_].status != AuctionStatus.Created || block.timestamp < lotData[lotId_].start || block.timestamp >= lotData[lotId_].conclusion ) revert Auction_NotLive(); - _; } - modifier onlyBidder(address sender_, uint96 lotId_, uint256 bidId_) { + /// @inheritdoc AuctionModule + /// @dev Checks that the bid is valid + function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view override { // Bid ID must be less than number of bids for lot if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); + } + /// @inheritdoc AuctionModule + /// @dev Checks that the sender is the bidder + function _revertIfNotBidOwner( + uint96 lotId_, + uint256 bidId_, + address bidder_ + ) internal view override { // Check that sender is the bidder - if (sender_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); - _; + if (bidder_ != lotEncryptedBids[lotId_][bidId_].bidder) revert Auction_NotBidder(); + } + + /// @inheritdoc AuctionModule + /// @dev Checks that the bid is not already cancelled + function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view override { + // Bid must not be cancelled + if (lotEncryptedBids[lotId_][bidId_].status == BidStatus.Cancelled) { + revert Auction_AlreadyCancelled(); + } } // =========== BID =========== // @@ -142,46 +161,18 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { } /// @inheritdoc AuctionModule - /// @dev Checks that the lot is active with the data structures used by this particular module - function _revertIfLotInactive(uint96 lotId_) internal view override { - // Check that bids are allowed to be submitted for the lot - if ( - auctionData[lotId_].status != AuctionStatus.Created - || block.timestamp < lotData[lotId_].start - || block.timestamp >= lotData[lotId_].conclusion - ) revert Auction_NotLive(); - } - - /// @inheritdoc Auction - /// @dev This function reverts if: - /// - the auction lot does not exist - /// - the auction lot is not live - /// - the bid does not exist - /// - `bidder_` is not the bidder - /// - the bid is already cancelled // TODO need to change this to delete the bid so we don't have to decrypt it later // Because of this, we can issue the refund immediately (needs to happen in the AuctionHouse) // However, this will require more refactoring because, we run into a problem of using the array index as the bidId since it will change when we delete the bid // It doesn't cost any more gas to store a uint96 bidId as part of the EncryptedBid. // A better approach may be to create a mapping of lotId => bidId => EncryptedBid. Then, have an array of bidIds in the AuctionData struct that can be iterated over. // This way, we can still lookup the bids by bidId for cancellation, etc. - function cancelBid( + function _cancelBid( uint96 lotId_, uint256 bidId_, - address bidder_ - ) - external - override - onlyInternal - auctionIsLive(lotId_) - onlyBidder(bidder_, lotId_, bidId_) - returns (uint256 refundAmount) - { + address + ) internal override returns (uint256 refundAmount) { // Validate inputs - // Bid is not already cancelled - if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { - revert Auction_AlreadyCancelled(); - } // Set bid status to cancelled lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index f057fc6d..d5210d9c 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -84,7 +84,7 @@ contract MockAtomicAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function cancelBid(uint96, uint256, address) external virtual override returns (uint256) { + function _cancelBid(uint96, uint256, address) internal virtual override returns (uint256) { revert Auction_NotImplemented(); } @@ -120,4 +120,14 @@ contract MockAtomicAuctionModule is AuctionModule { bytes calldata settlementProof_, bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + + function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override {} + + function _revertIfNotBidOwner( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) internal view virtual override {} + + function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index b54ad774..6ad9daba 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -69,11 +69,21 @@ contract MockAuctionModule is AuctionModule { bytes calldata settlementData_ ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function cancelBid( + function _cancelBid( uint96 lotId_, uint256 bidId_, address bidder_ - ) external virtual override returns (uint256) {} + ) internal virtual override returns (uint256) {} + + function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override {} + + function _revertIfNotBidOwner( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) internal view virtual override {} + + function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 02a34b86..4dbf7635 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -63,33 +63,11 @@ contract MockBatchAuctionModule is AuctionModule { return bidId; } - function cancelBid( + function _cancelBid( uint96 lotId_, uint256 bidId_, - address bidder_ - ) - external - virtual - override - isLotValid(lotId_) - isLotActive(lotId_) - returns (uint256 refundAmount) - { - // Check that the bid exists - if (bidData[lotId_].length <= bidId_) { - revert Auction.Auction_InvalidBidId(lotId_, bidId_); - } - - // Check that the bid has not been cancelled - if (bidCancelled[lotId_][bidId_] == true) { - revert Auction.Auction_InvalidBidId(lotId_, bidId_); - } - - // Check that the bidder is the owner of the bid - if (bidData[lotId_][bidId_].bidder != bidder_) { - revert Auction.Auction_NotBidder(); - } - + address + ) internal virtual override returns (uint256 refundAmount) { // Cancel the bid bidCancelled[lotId_][bidId_] = true; @@ -135,4 +113,29 @@ contract MockBatchAuctionModule is AuctionModule { function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } + + function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override { + // Check that the bid exists + if (bidData[lotId_].length <= bidId_) { + revert Auction.Auction_InvalidBidId(lotId_, bidId_); + } + } + + function _revertIfNotBidOwner( + uint96 lotId_, + uint256 bidId_, + address caller_ + ) internal view virtual override { + // Check that the bidder is the owner of the bid + if (bidData[lotId_][bidId_].bidder != caller_) { + revert Auction.Auction_NotBidder(); + } + } + + function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override { + // Check that the bid has not been cancelled + if (bidCancelled[lotId_][bidId_] == true) { + revert Auction.Auction_InvalidBidId(lotId_, bidId_); + } + } } From 2880ed0b5f06abb1641941850b8bab136a4cfd35 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 14:40:45 +0400 Subject: [PATCH 055/117] Remove redundant modifiers --- src/modules/Auction.sol | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 16dc2c4a..e4d2cddf 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -118,10 +118,6 @@ abstract contract Auction { address bidder_ ) external virtual returns (uint256 bidAmount); - // _revertIfCancelled - // _revertIfLotActive - // _revertIfBidderNotOwner - /// @notice Settle a batch auciton /// @notice This function is used for on-chain storage of bids and local settlement function settle(uint96 lotId_) @@ -202,7 +198,7 @@ abstract contract AuctionModule is Auction, Module { function auction( uint96 lotId_, AuctionParams memory params_ - ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { + ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { // TODO onlyInternal? // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidStart(params_.start, uint48(block.timestamp)); @@ -256,7 +252,7 @@ abstract contract AuctionModule is Auction, Module { /// - the lot is not active /// /// @param lotId_ The lot id - function cancelAuction(uint96 lotId_) external override onlyParent { + function cancelAuction(uint96 lotId_) external override onlyParent { // TODO onlyInternal? Lot storage lot = lotData[lotId_]; // Invalid lot @@ -435,28 +431,4 @@ abstract contract AuctionModule is Auction, Module { /// @param lotId_ The lot ID /// @param bidId_ The bid ID function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual; - - // TODO remove these - - /// @notice Checks that the lot ID is valid - /// @dev Reverts if the lot ID is invalid - /// - /// @param lotId_ The lot identifier - modifier isLotValid(uint96 lotId_) { - if (lotData[lotId_].start == 0) revert Auction_InvalidLotId(lotId_); - _; - } - - /// @notice Checks that the lot is active - /// @dev Reverts if the lot is not active - /// - /// @param lotId_ The lot identifier - modifier isLotActive(uint96 lotId_) { - Lot memory lot = lotData[lotId_]; - if ( - lot.capacity == 0 || lot.conclusion < uint48(block.timestamp) - || lot.start > block.timestamp - ) revert Auction_MarketNotActive(lotId_); - _; - } } From 8738b3360d953b036406bbca2c9c52cfbd880e2f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 14:52:40 +0400 Subject: [PATCH 056/117] Implement standard and implementation-logic pattern for atomic purchases --- src/modules/Auction.sol | 70 ++++++++++++++++--- src/modules/auctions/LSBBA/LSBBA.sol | 33 +++++++++ .../Auction/MockAtomicAuctionModule.sol | 6 +- test/modules/Auction/MockAuctionModule.sol | 4 +- .../Auction/MockBatchAuctionModule.sol | 4 +- 5 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index e4d2cddf..8d2b2d8b 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -74,8 +74,18 @@ abstract contract Auction { // ========== ATOMIC AUCTIONS ========== // + /// @notice Purchase tokens from an auction lot + /// @dev The implementing function should handle the following: + /// - Validate the purchase parameters + /// - Store the purchase data + /// + /// @param lotId_ The lot id + /// @param amount_ The amount of quote tokens to purchase + /// @param auctionData_ The auction-specific data + /// @return payout The amount of payout tokens to receive + /// @return auctionOutput The auction-specific output function purchase( - uint96 id_, + uint96 lotId_, uint256 amount_, bytes calldata auctionData_ ) external virtual returns (uint256 payout, bytes memory auctionOutput); @@ -198,7 +208,8 @@ abstract contract AuctionModule is Auction, Module { function auction( uint96 lotId_, AuctionParams memory params_ - ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { // TODO onlyInternal? + ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { + // TODO onlyInternal? // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidStart(params_.start, uint48(block.timestamp)); @@ -252,14 +263,13 @@ abstract contract AuctionModule is Auction, Module { /// - the lot is not active /// /// @param lotId_ The lot id - function cancelAuction(uint96 lotId_) external override onlyParent { // TODO onlyInternal? - Lot storage lot = lotData[lotId_]; - - // Invalid lot - if (lot.start == 0) revert Auction_InvalidLotId(lotId_); + function cancelAuction(uint96 lotId_) external override onlyParent { + // TODO onlyInternal? + // Validation + _revertIfLotInvalid(lotId_); + _revertIfLotInactive(lotId_); - // Inactive lot - if (lot.capacity == 0) revert Auction_MarketNotActive(lotId_); + Lot storage lot = lotData[lotId_]; lot.conclusion = uint48(block.timestamp); lot.capacity = 0; @@ -274,6 +284,48 @@ abstract contract AuctionModule is Auction, Module { /// @param lotId_ The lot ID function _cancelAuction(uint96 lotId_) internal virtual; + // ========== ATOMIC AUCTIONS ========== // + + /// @inheritdoc Auction + /// @dev Implements a basic purchase function that: + /// - Calls implementation-specific validation logic + /// - Calls the auction module + /// + /// This function reverts if: + /// - the lot id is invalid + /// - the lot is not active + /// - the caller is not an internal module + /// + /// Inheriting contracts should override _purchase to implement auction-specific logic, such as: + /// - Validating the auction-specific parameters + /// - Storing the purchase data + function purchase( + uint96 lotId_, + uint256 amount_, + bytes calldata auctionData_ + ) external override onlyInternal returns (uint256 payout, bytes memory auctionOutput) { + // Standard validation + _revertIfLotInvalid(lotId_); + _revertIfLotInactive(lotId_); + + // Call implementation-specific logic + return _purchase(lotId_, amount_, auctionData_); + } + + /// @notice Implementation-specific purchase logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID + /// @param amount_ The amount of quote tokens to purchase + /// @param auctionData_ The auction-specific data + /// @return payout The amount of payout tokens to receive + /// @return auctionOutput The auction-specific output + function _purchase( + uint96 lotId_, + uint256 amount_, + bytes calldata auctionData_ + ) internal virtual returns (uint256 payout, bytes memory auctionOutput); + // ========== BATCH AUCTIONS ========== // /// @inheritdoc Auction diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index dcf0c045..fc252be4 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -457,4 +457,37 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // Set auction status to settled so that bids can be refunded auctionData[lotId_].status = AuctionStatus.Settled; } + + function settle( + uint96 lotId_, + Bid[] calldata winningBids_, + bytes calldata settlementProof_, + bytes calldata settlementData_ + ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + + function payoutFor( + uint256 id_, + uint256 amount_ + ) public view virtual override returns (uint256) {} + + function priceFor( + uint256 id_, + uint256 payout_ + ) public view virtual override returns (uint256) {} + + function maxPayout(uint256 id_) public view virtual override returns (uint256) {} + + function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + + // =========== ATOMIC AUCTION STUBS ========== // + + /// @inheritdoc AuctionModule + /// @dev Atomic auctions are not supported by this auction type + function _purchase( + uint96, + uint256, + bytes calldata + ) internal pure override returns (uint256, bytes memory) { + revert Auction_NotImplemented(); + } } diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index d5210d9c..d4d7cb03 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -42,11 +42,11 @@ contract MockAtomicAuctionModule is AuctionModule { cancelled[id_] = true; } - function purchase( + function _purchase( uint96 id_, uint256 amount_, bytes calldata - ) external virtual override returns (uint256 payout, bytes memory auctionOutput) { + ) internal override returns (uint256 payout, bytes memory auctionOutput) { if (purchaseReverts) revert("error"); if (cancelled[id_]) revert Auction_MarketNotActive(id_); @@ -80,7 +80,7 @@ contract MockAtomicAuctionModule is AuctionModule { address, uint256, bytes calldata - ) internal override returns (uint256) { + ) internal pure override returns (uint256) { revert Auction_NotImplemented(); } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 6ad9daba..1f67bdf4 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -26,11 +26,11 @@ contract MockAuctionModule is AuctionModule { // } - function purchase( + function _purchase( uint96 id_, uint256 amount_, bytes calldata auctionData_ - ) external virtual override returns (uint256 payout, bytes memory auctionOutput) {} + ) internal override returns (uint256 payout, bytes memory auctionOutput) {} function _bid( uint96 id_, diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 4dbf7635..b84b8fac 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -30,11 +30,11 @@ contract MockBatchAuctionModule is AuctionModule { // } - function purchase( + function _purchase( uint96, uint256, bytes calldata - ) external virtual override returns (uint256, bytes memory) { + ) internal pure override returns (uint256, bytes memory) { revert Auction_NotImplemented(); } From 9a18fad353121bbf48724e546a4862e76307322b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 15:02:32 +0400 Subject: [PATCH 057/117] Remove redundant settle() code --- src/AuctionHouse.sol | 221 +++++----------------------------------- src/modules/Auction.sol | 2 + 2 files changed, 25 insertions(+), 198 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 4c2b0d49..ca9075ad 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -145,28 +145,6 @@ abstract contract Router is FeeManager { /// @notice This function is used for versions with on-chain storage and bids and local settlement function settle(uint96 lotId_) external virtual; - /// @notice Settle a batch auction with the provided bids - /// @notice This function is used for on-chain storage of bids and external settlement - /// - /// @dev The implementing function must perform the following: - /// 1. Validate that the caller is authorized to settle the auction - /// 2. Calculate fees - /// 3. Pass the bids to the auction module to validate the settlement - /// 4. Send payment to the auction owner - /// 5. Collect payout from the auction owner - /// 6. Send payout to each bidder - /// - /// @param lotId_ Lot ID - /// @param winningBids_ Winning bids - /// @param settlementProof_ Proof of settlement validity - /// @param settlementData_ Settlement data - function settle( - uint96 lotId_, - Auction.Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual; - // ========== FEE MANAGEMENT ========== // /// @notice Sets the fee for the protocol @@ -432,55 +410,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { lotRouting[lotId_].quoteToken.safeTransfer(msg.sender, refundAmount); } - /// @inheritdoc Router - // TODO this is the version of `settle` we need for the LSBBA - function settle(uint96 lotId_) external override { - // Settle the lot on the auction module and get the winning bids - // Reverts if the auction cannot be settled yet - // Some common things to check: - // 1. Total of amounts out is not greater than capacity - // 2. Minimum price is enforced - // 3. Minimum bid size is enforced - // 4. Minimum capacity sold is enforced - AuctionModule module = _getModuleForId(lotId_); - (Auction.Bid[] memory winningBids, bytes memory auctionOutput) = module.settle(lotId_); - - // Load routing data for the lot - Routing memory routing = lotRouting[lotId_]; - - // TODO check this - // Payment and payout have already been sourced - - // Calculate fees - uint256 totalAmountInLessFees; - { - (uint256 totalAmountIn, uint256 totalFees) = - _allocateFees(winningBids, routing.quoteToken); - totalAmountInLessFees = totalAmountIn - totalFees; - } - - // Send payment in bulk to auction owner - _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); - - // TODO send last winning bidder partial refund if it is a partial fill. - - // Handle payouts to bidders - { - uint256 bidCount = winningBids.length; - for (uint256 i; i < bidCount; i++) { - // Send payout to each bidder - _sendPayout( - lotId_, - winningBids[i].bidder, - winningBids[i].minAmountOut, - routing, - auctionOutput - ); - } - } - } - - // TODO we can probably remove this version of the settle function for now. It won't be used initially. /// @inheritdoc Router /// @dev This function reverts if: /// - the lot ID is invalid @@ -489,38 +418,28 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// - transferring the quote token to the auction owner fails /// - collecting the payout from the auction owner fails /// - sending the payout to each bidder fails - function settle( - uint96 lotId_, - Auction.Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external override isLotValid(lotId_) { - // Load routing data for the lot - Routing memory routing = lotRouting[lotId_]; - - // Validate that sender is authorized to settle the auction - // TODO + function settle(uint96 lotId_) external override isLotValid(lotId_) { + // Validation + // TODO check the caller is authorised - // Send auction inputs to auction module to validate settlement - // We do this because the format of the bids and signatures is specific to the auction module + // Settle the lot on the auction module and get the winning bids + // Reverts if the auction cannot be settled yet // Some common things to check: // 1. Total of amounts out is not greater than capacity // 2. Minimum price is enforced // 3. Minimum bid size is enforced // 4. Minimum capacity sold is enforced - uint256[] memory amountsOut; - bytes memory auctionOutput; - { - AuctionModule module = _getModuleForId(lotId_); - (amountsOut, auctionOutput) = - module.settle(lotId_, winningBids_, settlementProof_, settlementData_); - } + AuctionModule module = _getModuleForId(lotId_); + (Auction.Bid[] memory winningBids, bytes memory auctionOutput) = module.settle(lotId_); + + // Load routing data for the lot + Routing memory routing = lotRouting[lotId_]; // Calculate fees uint256 totalAmountInLessFees; { (uint256 totalAmountIn, uint256 totalFees) = - _allocateFees(winningBids_, routing.quoteToken); + _allocateFees(winningBids, routing.quoteToken); totalAmountInLessFees = totalAmountIn - totalFees; } @@ -529,15 +448,17 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Send payment in bulk to auction owner _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); + // TODO send last winning bidder partial refund if it is a partial fill. + // Collect payout in bulk from the auction owner { // Calculate amount out uint256 totalAmountOut; { - uint256 bidCount = amountsOut.length; + uint256 bidCount = winningBids.length; for (uint256 i; i < bidCount; i++) { // Increment total amount out - totalAmountOut += amountsOut[i]; + totalAmountOut += winningBids[i].minAmountOut; } } @@ -546,116 +467,20 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders { - uint256 bidCount = winningBids_.length; + uint256 bidCount = winningBids.length; for (uint256 i; i < bidCount; i++) { // Send payout to each bidder - _sendPayout(lotId_, winningBids_[i].bidder, amountsOut[i], routing, auctionOutput); + _sendPayout( + lotId_, + winningBids[i].bidder, + winningBids[i].minAmountOut, + routing, + auctionOutput + ); } } } - // // External submission and evaluation - // function settle(uint256 id_, ExternalSettlement memory settlement_) external override { - // // Load routing data for the lot - // Routing memory routing = lotRouting[id_]; - - // // Validate that sender is authorized to settle the auction - // // TODO - - // // Validate array lengths all match - // uint256 len = settlement_.bids.length; - // if ( - // len != settlement_.bidSignatures.length || len != settlement_.amountsIn.length - // || len != settlement_.amountsOut.length || len != settlement_.approvals.length - // || len != settlement_.allowlistProofs.length - // ) revert InvalidParams(); - - // // Bid-level validation and fee calculations - // uint256[] memory amountsInLessFees = new uint256[](len); - // uint256 totalProtocolFee; - // uint256 totalAmountInLessFees; - // uint256 totalAmountOut; - // for (uint256 i; i < len; i++) { - // // If there is an allowlist, validate that the winners are on the allowlist - // if ( - // !_isAllowed( - // routing.allowlist, - // id_, - // settlement_.bids[i].bidder, - // settlement_.allowlistProofs[i] - // ) - // ) { - // revert InvalidBidder(settlement_.bids[i].bidder); - // } - - // // Check that the amounts out are at least the minimum specified by the bidder - // // If a bid is a partial fill, then it's amountIn will be less than the amount specified by the bidder - // // If so, we need to adjust the minAmountOut proportionally for the slippage check - // // We also verify that the amountIn is not more than the bidder specified - // uint256 minAmountOut = settlement_.bids[i].minAmountOut; - // if (settlement_.amountsIn[i] > settlement_.bids[i].amount) { - // revert InvalidParams(); - // } else if (settlement_.amountsIn[i] < settlement_.bids[i].amount) { - // minAmountOut = - // (minAmountOut * settlement_.amountsIn[i]) / settlement_.bids[i].amount; // TODO need to think about scaling and rounding here - // } - // if (settlement_.amountsOut[i] < minAmountOut) revert AmountLessThanMinimum(); - - // // Calculate fees from bid amount - // (uint256 toReferrer, uint256 toProtocol) = - // _calculateFees(settlement_.bids[i].referrer, settlement_.amountsIn[i]); - // amountsInLessFees[i] = settlement_.amountsIn[i] - toReferrer - toProtocol; - - // // Update referrer fee balances if non-zero and increment the total protocol fee - // if (toReferrer > 0) { - // rewards[settlement_.bids[i].referrer][routing.quoteToken] += toReferrer; - // } - // totalProtocolFee += toProtocol; - - // // Increment total amount out - // totalAmountInLessFees += amountsInLessFees[i]; - // totalAmountOut += settlement_.amountsOut[i]; - // } - - // // Update protocol fee if not zero - // if (totalProtocolFee > 0) rewards[_PROTOCOL][routing.quoteToken] += totalProtocolFee; - - // // Send auction inputs to auction module to validate settlement - // // We do this because the format of the bids and signatures is specific to the auction module - // // Some common things to check: - // // 1. Total of amounts out is not greater than capacity - // // 2. Minimum price is enforced - // // 3. Minimum bid size is enforced - // // 4. Minimum capacity sold is enforced - // AuctionModule module = _getModuleForId(id_); - - // // TODO update auction module interface and base function to handle these inputs, and perhaps others - // bytes memory auctionOutput = module.settle( - // id_, - // settlement_.bids, - // settlement_.bidSignatures, - // amountsInLessFees, - // settlement_.amountsOut, - // settlement_.validityProof - // ); - - // // Assumes that payment has already been collected for each bid - - // // Send payment in bulk to auction owner - // _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); - - // // Collect payout in bulk from the auction owner - // _collectPayout(id_, totalAmountInLessFees, totalAmountOut, routing); - - // // Handle payouts to bidders - // for (uint256 i; i < len; i++) { - // // Send payout to each bidder - // _sendPayout( - // id_, settlement_.bids[i].bidder, settlement_.amountsOut[i], routing, auctionOutput - // ); - // } - // } - // ========== TOKEN TRANSFERS ========== // /// @notice Collects payment of the quote token from the user diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 8d2b2d8b..c8917b97 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -359,6 +359,8 @@ abstract contract AuctionModule is Auction, Module { /// @notice Implementation-specific bid logic /// @dev Auction modules should override this to perform any additional logic + /// The returned `bidId` should be a unique and persistent identifier for the bid, + /// which can be used in subsequent calls (e.g. `cancelBid()` or `settle()`). /// /// @param lotId_ The lot ID /// @param bidder_ The bidder of the purchased tokens From b71f3a479adb24a7fdef3cdcaf4c077d88c22220 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 16:33:04 +0400 Subject: [PATCH 058/117] Remove obsolete settle() function. Implement implementation-logic pattern for settle(). --- src/modules/Auction.sol | 116 +++++++++++++++--- src/modules/auctions/LSBBA/LSBBA.sol | 18 ++- src/modules/auctions/bases/BatchAuction.sol | 84 ++++++------- .../Auction/MockAtomicAuctionModule.sol | 18 +-- test/modules/Auction/MockAuctionModule.sol | 14 +-- .../Auction/MockBatchAuctionModule.sol | 14 +-- 6 files changed, 160 insertions(+), 104 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index c8917b97..a71da8ba 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -8,6 +8,8 @@ abstract contract Auction { error Auction_MarketNotActive(uint96 lotId); + error Auction_MarketActive(uint96 lotId); + error Auction_InvalidStart(uint48 start_, uint48 minimum_); error Auction_InvalidDuration(uint48 duration_, uint48 minimum_); @@ -128,29 +130,20 @@ abstract contract Auction { address bidder_ ) external virtual returns (uint256 bidAmount); - /// @notice Settle a batch auciton - /// @notice This function is used for on-chain storage of bids and local settlement + /// @notice Settle a batch auction lot with on-chain storage and settlement + /// @dev The implementing function should handle the following: + /// - Validate the lot parameters + /// - Determine the winning bids + /// - Update the lot data + /// + /// @param lotId_ The lot id + /// @return winningBids_ The winning bids + /// @return auctionOutput_ The auction-specific output function settle(uint96 lotId_) external virtual returns (Bid[] memory winningBids_, bytes memory auctionOutput_); - /// @notice Settle a batch auction with the provided bids - /// @notice This function is used for on-chain storage of bids and external settlement - /// - /// @param lotId_ Lot id - /// @param winningBids_ Winning bids - /// @param settlementProof_ Proof of settlement validity - /// @param settlementData_ Settlement data - /// @return amountsOut Amount out for each bid - /// @return auctionOutput Auction-specific output - function settle( - uint96 lotId_, - Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual returns (uint256[] memory amountsOut, bytes memory auctionOutput); - // ========== AUCTION MANAGEMENT ========== // // TODO NatSpec comments @@ -293,7 +286,7 @@ abstract contract AuctionModule is Auction, Module { /// /// This function reverts if: /// - the lot id is invalid - /// - the lot is not active + /// - the lot is inactive /// - the caller is not an internal module /// /// Inheriting contracts should override _purchase to implement auction-specific logic, such as: @@ -336,6 +329,7 @@ abstract contract AuctionModule is Auction, Module { /// This function reverts if: /// - the lot id is invalid /// - the lot is not active + /// - the lot is already settled /// - the caller is not an internal module /// /// Inheriting contracts should override _bid to implement auction-specific logic, such as: @@ -352,6 +346,7 @@ abstract contract AuctionModule is Auction, Module { // Standard validation _revertIfLotInvalid(lotId_); _revertIfLotInactive(lotId_); + _revertIfLotSettled(lotId_); // Call implementation-specific logic return _bid(lotId_, bidder_, recipient_, referrer_, amount_, auctionData_); @@ -386,6 +381,7 @@ abstract contract AuctionModule is Auction, Module { /// This function reverts if: /// - the lot id is invalid /// - the lot is not active + /// - the lot is already settled /// - the bid id is invalid /// - the bid is already cancelled /// - `caller_` is not the bid owner @@ -402,6 +398,7 @@ abstract contract AuctionModule is Auction, Module { // Standard validation _revertIfLotInvalid(lotId_); _revertIfLotInactive(lotId_); + _revertIfLotSettled(lotId_); _revertIfBidInvalid(lotId_, bidId_); _revertIfNotBidOwner(lotId_, bidId_, caller_); _revertIfBidCancelled(lotId_, bidId_); @@ -423,6 +420,47 @@ abstract contract AuctionModule is Auction, Module { address caller_ ) internal virtual returns (uint256 bidAmount); + /// @inheritdoc Auction + /// @dev Implements a basic settle function that: + /// - Calls implementation-specific validation logic + /// - Calls the auction module + /// + /// This function reverts if: + /// - the lot id is invalid + /// - the lot is still active + /// - the lot has already been settled + /// - the caller is not an internal module + /// + /// Inheriting contracts should override _settle to implement auction-specific logic, such as: + /// - Validating the auction-specific parameters + /// - Determining the winning bids + /// - Updating the lot data + function settle(uint96 lotId_) + external + override + onlyInternal + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + { + // Standard validation + _revertIfLotInvalid(lotId_); + _revertIfLotActive(lotId_); + _revertIfLotSettled(lotId_); + + // Call implementation-specific logic + return _settle(lotId_); + } + + /// @notice Implementation-specific lot settlement logic + /// @dev Auction modules should override this to perform any additional logic + /// + /// @param lotId_ The lot ID + /// @return winningBids_ The winning bids + /// @return auctionOutput_ The auction-specific output + function _settle(uint96 lotId_) + internal + virtual + returns (Bid[] memory winningBids_, bytes memory auctionOutput_); + // ========== AUCTION INFORMATION ========== // // TODO does this need to change for batch auctions? @@ -448,6 +486,30 @@ abstract contract AuctionModule is Auction, Module { if (lotData[lotId_].start == 0) revert Auction_InvalidLotId(lotId_); } + /// @notice Checks that the lot represented by `lotId_` has not started + /// @dev Should revert if the lot has not started + function _revertIfBeforeLotStart(uint96 lotId_) internal view virtual { + if (lotData[lotId_].start > uint48(block.timestamp)) revert Auction_MarketNotActive(lotId_); + } + + /// @notice Checks that the lot represented by `lotId_` has started + /// @dev Should revert if the lot has started + function _revertIfLotStarted(uint96 lotId_) internal view virtual { + if (lotData[lotId_].start > uint48(block.timestamp)) revert Auction_MarketActive(lotId_); + } + + /// @notice Checks that the lot represented by `lotId_` has not concluded + /// @dev Should revert if the lot has concluded + function _revertIfLotConcluded(uint96 lotId_) internal view virtual { + // Beyond the conclusion time + if (lotData[lotId_].conclusion < uint48(block.timestamp)) { + revert Auction_MarketNotActive(lotId_); + } + + // Capacity is sold-out, or cancelled + if (lotData[lotId_].capacity == 0) revert Auction_MarketNotActive(lotId_); + } + /// @notice Checks that the lot represented by `lotId_` is active /// @dev Should revert if the lot is not active /// Inheriting contracts can override this to implement custom logic @@ -457,6 +519,22 @@ abstract contract AuctionModule is Auction, Module { if (!isLive(lotId_)) revert Auction_MarketNotActive(lotId_); } + /// @notice Checks that the lot represented by `lotId_` is active + /// @dev Should revert if the lot is active + /// Inheriting contracts can override this to implement custom logic + /// + /// @param lotId_ The lot ID + function _revertIfLotActive(uint96 lotId_) internal view virtual { + if (isLive(lotId_)) revert Auction_MarketActive(lotId_); + } + + /// @notice Checks that the lot represented by `lotId_` is not settled + /// @dev Should revert if the lot is settled + /// Inheriting contracts must override this to implement custom logic + /// + /// @param lotId_ The lot ID + function _revertIfLotSettled(uint96 lotId_) internal view virtual; + /// @notice Checks that the lot and bid combination is valid /// @dev Should revert if the bid is invalid /// Inheriting contracts must override this to implement custom logic diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index fc252be4..2138a1a6 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -99,6 +99,11 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { ) revert Auction_NotLive(); } + function _revertIfLotSettled(uint96 lotId_) internal view override { + // Auction must not be settled + if (auctionData[lotId_].status == AuctionStatus.Settled) revert Auction_NotConcluded(); + } + /// @inheritdoc AuctionModule /// @dev Checks that the bid is valid function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view override { @@ -302,10 +307,10 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // =========== SETTLEMENT =========== // - function settle(uint96 lotId_) - external + /// @inheritdoc AuctionModule + function _settle(uint96 lotId_) + internal override - onlyInternal returns (Bid[] memory winningBids_, bytes memory auctionOutput_) { // Check that auction is in the right state for settlement @@ -458,13 +463,6 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { auctionData[lotId_].status = AuctionStatus.Settled; } - function settle( - uint96 lotId_, - Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function payoutFor( uint256 id_, uint256 amount_ diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 75b30a28..79a8a91c 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -34,48 +34,48 @@ abstract contract BatchAuction { } abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { - // ========== STATE VARIABLES ========== // - - mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; - - /// @inheritdoc AuctionModule - function _bid( - uint96 lotId_, - address bidder_, - address recipient_, - address referrer_, - uint256 amount_, - bytes calldata auctionData_ - ) internal override returns (uint256 bidId) { - // TODO - // Validate inputs - - // Execute user approval if provided? - - // Call implementation specific bid logic - - // Store bid data - } - - /// @inheritdoc Auction - function settle( - uint96 lotId, - Auction.Bid[] memory winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) - external - override - onlyParent - returns (uint256[] memory amountsOut, bytes memory auctionOutput) - { - // TODO - // Validate inputs - - // Call implementation specific settle logic - - // Store settle data - } +// // ========== STATE VARIABLES ========== // + +// mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; + +// /// @inheritdoc AuctionModule +// function _bid( +// uint96 lotId_, +// address bidder_, +// address recipient_, +// address referrer_, +// uint256 amount_, +// bytes calldata auctionData_ +// ) internal override returns (uint256 bidId) { +// // TODO +// // Validate inputs + +// // Execute user approval if provided? + +// // Call implementation specific bid logic + +// // Store bid data +// } + +// /// @inheritdoc Auction +// function settle( +// uint96 lotId, +// Auction.Bid[] memory winningBids_, +// bytes calldata settlementProof_, +// bytes calldata settlementData_ +// ) +// external +// override +// onlyParent +// returns (uint256[] memory amountsOut, bytes memory auctionOutput) +// { +// // TODO +// // Validate inputs + +// // Call implementation specific settle logic + +// // Store settle data +// } } // abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index d4d7cb03..c9d9d770 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -107,19 +107,9 @@ contract MockAtomicAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} - function settle(uint96 lotId_) - external - virtual - override - returns (Bid[] memory winningBids_, bytes memory auctionOutput_) - {} - - function settle( - uint96 lotId_, - Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} + function _settle(uint96) internal pure override returns (Bid[] memory, bytes memory) { + revert Auction_NotImplemented(); + } function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override {} @@ -130,4 +120,6 @@ contract MockAtomicAuctionModule is AuctionModule { ) internal view virtual override {} function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} + + function _revertIfLotSettled(uint96 lotId_) internal view virtual override {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 1f67bdf4..954fb000 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -55,20 +55,12 @@ contract MockAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} - function settle(uint96 lotId_) - external - virtual + function _settle(uint96 lotId_) + internal override returns (Bid[] memory winningBids_, bytes memory auctionOutput_) {} - function settle( - uint96 lotId_, - Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function _cancelBid( uint96 lotId_, uint256 bidId_, @@ -84,6 +76,8 @@ contract MockAuctionModule is AuctionModule { ) internal view virtual override {} function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} + + function _revertIfLotSettled(uint96 lotId_) internal view virtual override {} } contract MockAuctionModuleV2 is MockAuctionModule { diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index b84b8fac..4f584a6a 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -96,20 +96,12 @@ contract MockBatchAuctionModule is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} - function settle(uint96 lotId_) - external - virtual + function _settle(uint96 lotId_) + internal override returns (Bid[] memory winningBids_, bytes memory auctionOutput_) {} - function settle( - uint96 lotId_, - Bid[] calldata winningBids_, - bytes calldata settlementProof_, - bytes calldata settlementData_ - ) external virtual override returns (uint256[] memory amountsOut, bytes memory auctionOutput) {} - function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } @@ -138,4 +130,6 @@ contract MockBatchAuctionModule is AuctionModule { revert Auction.Auction_InvalidBidId(lotId_, bidId_); } } + + function _revertIfLotSettled(uint96 lotId_) internal view virtual override {} } From 11fa1d7808feaebcdcf84f2686ac7f4841c80827 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 16:34:12 +0400 Subject: [PATCH 059/117] Documentation --- src/modules/auctions/LSBBA/LSBBA.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 2138a1a6..689d8876 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -99,9 +99,11 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { ) revert Auction_NotLive(); } + /// @inheritdoc AuctionModule + /// @dev Checks that the lot is not yet settled function _revertIfLotSettled(uint96 lotId_) internal view override { // Auction must not be settled - if (auctionData[lotId_].status == AuctionStatus.Settled) revert Auction_NotConcluded(); + if (auctionData[lotId_].status == AuctionStatus.Settled) revert Auction_WrongState(); } /// @inheritdoc AuctionModule From 728ed12a6efa88e4fb4a5773c07d13aeed395893 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 16:40:29 +0400 Subject: [PATCH 060/117] Fix stack too deep error in RSA library --- src/lib/RSA.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index 1caca93c..72a8f136 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -191,11 +191,14 @@ library RSAOAEP { // 2. f. Let maskedDB = DB \xor dbMask. bytes memory maskedDb = _xor(db, dbMask); - // 2. g. Let seedMask = MGF(maskedDB, hLen). - bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); + bytes32 maskedSeed; + { + // 2. g. Let seedMask = MGF(maskedDB, hLen). + bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); - // 2. h. Let maskedSeed = seed \xor seedMask. - bytes32 maskedSeed = rand ^ seedMask; + // 2. h. Let maskedSeed = seed \xor seedMask. + maskedSeed = rand ^ seedMask; + } // 2. i. Concatenate a single octet with hexadecimal value 0x00, // maskedSeed, and maskedDB to form an encoded message EM of From 4cf39853b16b671ae56a9700f211d3781f43fce4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 24 Jan 2024 17:52:15 +0400 Subject: [PATCH 061/117] Tests for LSBBA auction creation --- src/modules/Auction.sol | 8 + src/modules/auctions/LSBBA/LSBBA.sol | 50 ++-- test/modules/auctions/LSBBA/auction.t.sol | 291 ++++++++++++++++++++++ 3 files changed, 333 insertions(+), 16 deletions(-) create mode 100644 test/modules/auctions/LSBBA/auction.t.sol diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a71da8ba..6d42c545 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -66,6 +66,7 @@ abstract contract Auction { // ========= STATE ========== // /// @notice Minimum auction duration in seconds + // TODO should this be set at deployment and/or through a function? uint48 public minAuctionDuration; // 1% = 1_000 or 1e3. 100% = 100_000 or 1e5. @@ -475,6 +476,13 @@ abstract contract AuctionModule is Auction, Module { return lotData[id_].capacity; } + /// @notice Get the lot data for a given lot ID + /// + /// @param lotId_ The lot ID + function getLot(uint96 lotId_) external view returns (Lot memory) { + return lotData[lotId_]; + } + // ========== MODIFIERS ========== // /// @notice Checks that `lotId_` is valid diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 689d8876..b911fe24 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -12,8 +12,7 @@ import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinP // 1. Bidding - bidders submit encrypted bids // 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting // 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred -// TODO abstract since not everything is implemented here -abstract contract LocalSealedBidBatchAuction is AuctionModule { +contract LocalSealedBidBatchAuction is AuctionModule { using MinPriorityQueue for MinPriorityQueue.Queue; // ========== ERRORS ========== // @@ -63,6 +62,19 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { bytes publicKeyModulus; } + /// @notice Struct containing parameters for creating a new LSBBA auction + /// + /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (scale: `ONE_HUNDRED_PERCENT`) + /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (scale: `ONE_HUNDRED_PERCENT`) + /// @param minimumPrice_ The minimum price that the auction can settle at + /// @param publicKeyModulus_ The public key modulus used to encrypt bids + struct AuctionDataParams { + uint24 minFillPercent; + uint24 minBidPercent; + uint256 minimumPrice; + bytes publicKeyModulus; + } + // ========== STATE VARIABLES ========== // uint256 internal constant MIN_BID_PERCENT = 1000; // 1% @@ -76,7 +88,11 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // ========== SETUP ========== // - constructor(address auctionHouse_) AuctionModule(auctionHouse_) {} + constructor(address auctionHouse_) AuctionModule(auctionHouse_) { + // Set the minimum auction duration to 1 day + // TODO is this a good default? + minAuctionDuration = 1 days; + } function VEECODE() public pure override returns (Veecode) { return toVeecode("01LSBBA"); @@ -413,12 +429,7 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { bytes memory params_ ) internal override returns (bool prefundingRequired) { // Decode implementation params - ( - uint256 minimumPrice, - uint256 minFillPercent, - uint256 minBidPercent, - bytes memory publicKeyModulus - ) = abi.decode(params_, (uint256, uint256, uint256, bytes)); + AuctionDataParams memory implParams = abi.decode(params_, (AuctionDataParams)); // Validate params // Capacity must be in base token for this auction type @@ -426,23 +437,26 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { // minFillPercent must be less than or equal to 100% // TODO should there be a minimum? - if (minFillPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + if (implParams.minFillPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); // minBidPercent must be greater than or equal to the global min and less than or equal to 100% // TODO should we cap this below 100%? - if (minBidPercent < MIN_BID_PERCENT || minBidPercent > ONE_HUNDRED_PERCENT) { + if ( + implParams.minBidPercent < MIN_BID_PERCENT + || implParams.minBidPercent > ONE_HUNDRED_PERCENT + ) { revert Auction_InvalidParams(); } // publicKeyModulus must be 1024 bits (128 bytes) - if (publicKeyModulus.length != 128) revert Auction_InvalidParams(); + if (implParams.publicKeyModulus.length != 128) revert Auction_InvalidParams(); // Store auction data AuctionData storage data = auctionData[lotId_]; - data.minimumPrice = minimumPrice; - data.minFilled = (lot_.capacity * minFillPercent) / ONE_HUNDRED_PERCENT; - data.minBidSize = (lot_.capacity * minBidPercent) / ONE_HUNDRED_PERCENT; - data.publicKeyModulus = publicKeyModulus; + data.minimumPrice = implParams.minimumPrice; + data.minFilled = (lot_.capacity * implParams.minFillPercent) / ONE_HUNDRED_PERCENT; + data.minBidSize = (lot_.capacity * implParams.minBidPercent) / ONE_HUNDRED_PERCENT; + data.publicKeyModulus = implParams.publicKeyModulus; // Initialize sorted bid queue lotSortedBids[lotId_].initialize(); @@ -479,6 +493,10 @@ abstract contract LocalSealedBidBatchAuction is AuctionModule { function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function getLotData(uint96 lotId_) public view returns (AuctionData memory) { + return auctionData[lotId_]; + } + // =========== ATOMIC AUCTION STUBS ========== // /// @inheritdoc AuctionModule diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol new file mode 100644 index 00000000..08ce39d0 --- /dev/null +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; + +contract LSBBACreateAuctionTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + // Function parameters + uint96 internal lotId = 1; + Auction.AuctionParams internal auctionParams; + LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + auctionDataParams = LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: new bytes(128) + }); + + // Set auction parameters + auctionParams = Auction.AuctionParams({ + start: uint48(block.timestamp), + duration: uint48(1 days), + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode(auctionDataParams) + }); + } + + // ===== Modifiers ===== // + + modifier whenStartTimeIsInPast() { + auctionParams.start = uint48(block.timestamp - 1); + _; + } + + modifier whenStartTimeIsZero() { + auctionParams.start = 0; + _; + } + + modifier whenDurationIsLessThanMinimum() { + auctionParams.duration = 1; + _; + } + + modifier whenAuctionDataParamsAreInvalid() { + auctionParams.implParams = abi.encode("invalid"); + _; + } + + modifier whenCapacityInQuoteIsEnabled() { + auctionParams.capacityInQuote = true; + _; + } + + modifier whenMinimumFillPercentageIsMoreThanMax() { + auctionDataParams.minFillPercent = 100_001; + + auctionParams.implParams = abi.encode(auctionDataParams); + _; + } + + modifier whenMinimumBidPercentageIsLessThanMin() { + auctionDataParams.minBidPercent = 999; + + auctionParams.implParams = abi.encode(auctionDataParams); + _; + } + + modifier whenMinimumBidPercentageIsMoreThanMax() { + auctionDataParams.minBidPercent = 100_001; + + auctionParams.implParams = abi.encode(auctionDataParams); + _; + } + + modifier whenPublicKeyModulusIsOfIncorrectLength() { + auctionDataParams.publicKeyModulus = new bytes(127); + + auctionParams.implParams = abi.encode(auctionDataParams); + _; + } + + // ===== Tests ===== // + + // [X] when called by a non-parent + // [X] it reverts + // [X] when start time is in the past + // [X] it reverts + // [X] when start time is zero + // [X] it sets the start time to the current block timestamp + // [X] when the duration is less than the minimum + // [X] it reverts + // [X] when the auction parameters are invalid + // [X] it reverts + // [X] when capacity in quote is enabled + // [X] it reverts + // [X] when minimum fill percentage is more than 100% + // [X] it reverts + // [X] when minimum bid percentage is less than the minimum + // [X] it reverts + // [X] when minimum bid percentage is more than 100% + // [X] it reverts + // [X] when publicKeyModulus is of incorrect length + // [X] it reverts + // [X] when called via execOnModule + // [X] it succeeds + // [X] it sets the auction parameters + + function test_notParent_reverts() external { + // Expected error + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call + auctionModule.auction(lotId, auctionParams); + } + + function test_startsInPast_reverts() external whenStartTimeIsInPast { + // Expected error + bytes memory err = abi.encodeWithSelector( + Auction.Auction_InvalidStart.selector, auctionParams.start, uint48(block.timestamp) + ); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_noStartTime() external whenStartTimeIsZero { + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + // Check values + assertEq(auctionModule.getLot(lotId).start, uint48(block.timestamp)); + } + + function test_durationLessThanMinimum_reverts() external whenDurationIsLessThanMinimum { + // Expected error + bytes memory err = abi.encodeWithSelector( + Auction.Auction_InvalidDuration.selector, + auctionParams.duration, + auctionModule.minAuctionDuration() + ); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_auctionDataParamsAreInvalid_reverts() external whenAuctionDataParamsAreInvalid { + // Expected error + vm.expectRevert(); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_capacityInQuoteIsEnabled_reverts() external whenCapacityInQuoteIsEnabled { + // Expected error + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_minimumFillPercentageIsMoreThanMax_reverts() + external + whenMinimumFillPercentageIsMoreThanMax + { + // Expected error + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_minimumBidPercentageIsLessThanMin_reverts() + external + whenMinimumBidPercentageIsLessThanMin + { + // Expected error + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_minimumBidPercentageIsMoreThanMax_reverts() + external + whenMinimumBidPercentageIsMoreThanMax + { + // Expected error + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_publicKeyModulusIsOfIncorrectLength_reverts() + external + whenPublicKeyModulusIsOfIncorrectLength + { + // Expected error + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + function test_execOnModule() external { + // Call + auctionHouse.execOnModule( + auctionModule.VEECODE(), + abi.encodeWithSelector(auctionModule.auction.selector, lotId, auctionParams) + ); + + // Check values + assertEq(auctionModule.getLot(lotId).start, auctionParams.start); + } + + function test_success() external { + // Call + vm.prank(address(auctionHouse)); + (bool prefundingRequired_, uint256 capacity_) = auctionModule.auction(lotId, auctionParams); + + // Check return values + assertEq(prefundingRequired_, true); // Always true for LSBBA + assertEq(capacity_, auctionParams.capacity); + + // Check lot data + Auction.Lot memory lot = auctionModule.getLot(lotId); + assertEq(lot.start, auctionParams.start); + assertEq(lot.conclusion, auctionParams.start + auctionParams.duration); + assertEq(lot.capacityInQuote, auctionParams.capacityInQuote); + assertEq(lot.capacity, auctionParams.capacity); + + // Check auction-specific data + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + assertEq(lotData.minimumPrice, auctionDataParams.minimumPrice); + assertEq( + lotData.minFilled, (auctionParams.capacity * auctionDataParams.minFillPercent) / 100_000 + ); + assertEq( + lotData.minBidSize, (auctionParams.capacity * auctionDataParams.minBidPercent) / 100_000 + ); + assertEq(lotData.publicKeyModulus, auctionDataParams.publicKeyModulus); + + // Check that the sorted bid queue is initialised + (uint96 nextBidId_,) = auctionModule.lotSortedBids(lotId); + assertEq(nextBidId_, 1); + } +} From 05299caac0138f1c4d7947869b3470414e822d92 Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 24 Jan 2024 17:00:34 -0600 Subject: [PATCH 062/117] feat: update and add event --- src/bases/Auctioneer.sol | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 74704fe7..b77a8148 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -41,7 +41,10 @@ abstract contract Auctioneer is WithModules { // ========= EVENTS ========= // - event AuctionCreated(uint96 id, address baseToken, address quoteToken); + event AuctionCreated( + uint96 id, Veecode indexed auctionRef, address baseToken, address quoteToken + ); + event AuctionCancelled(uint96 id, Veecode indexed auctionRef); // ========= DATA STRUCTURES ========== // @@ -290,7 +293,9 @@ abstract contract Auctioneer is WithModules { } } - emit AuctionCreated(lotId, address(routing.baseToken), address(routing.quoteToken)); + emit AuctionCreated( + lotId, auctionRef, address(routing_.baseToken), address(routing_.quoteToken) + ); } /// @notice Cancels an auction lot @@ -322,6 +327,8 @@ abstract contract Auctioneer is WithModules { Routing memory routing = lotRouting[lotId_]; routing.baseToken.safeTransfer(routing.owner, lotRemainingCapacity); } + + emit AuctionCancelled(lotId_, lotRouting[lotId_].auctionReference); } // ========== AUCTION INFORMATION ========== // From cabed458835700b51efbead061b0cbb4723f070c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 12:16:08 +0400 Subject: [PATCH 063/117] Shift auction creation to onlyInternal --- src/modules/Auction.sol | 6 ++---- test/modules/auctions/LSBBA/auction.t.sol | 21 ++++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 6d42c545..0d034194 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -202,8 +202,7 @@ abstract contract AuctionModule is Auction, Module { function auction( uint96 lotId_, AuctionParams memory params_ - ) external override onlyParent returns (bool prefundingRequired, uint256 capacity) { - // TODO onlyInternal? + ) external override onlyInternal returns (bool prefundingRequired, uint256 capacity) { // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidStart(params_.start, uint48(block.timestamp)); @@ -257,8 +256,7 @@ abstract contract AuctionModule is Auction, Module { /// - the lot is not active /// /// @param lotId_ The lot id - function cancelAuction(uint96 lotId_) external override onlyParent { - // TODO onlyInternal? + function cancelAuction(uint96 lotId_) external override onlyInternal { // Validation _revertIfLotInvalid(lotId_); _revertIfLotInactive(lotId_); diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol index 08ce39d0..c6d117d2 100644 --- a/test/modules/auctions/LSBBA/auction.t.sol +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {Permit2User} from "test/lib/permit2/Permit2User.sol"; -import {Module} from "src/modules/Modules.sol"; +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; // Auctions import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; @@ -128,7 +128,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // [X] when publicKeyModulus is of incorrect length // [X] it reverts // [X] when called via execOnModule - // [X] it succeeds + // [X] it reverts // [X] it sets the auction parameters function test_notParent_reverts() external { @@ -247,14 +247,20 @@ contract LSBBACreateAuctionTest is Test, Permit2User { } function test_execOnModule() external { + Veecode moduleVeecode = auctionModule.VEECODE(); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) + ); + vm.expectRevert(err); + // Call auctionHouse.execOnModule( - auctionModule.VEECODE(), - abi.encodeWithSelector(auctionModule.auction.selector, lotId, auctionParams) + moduleVeecode, + abi.encodeWithSelector(Auction.auction.selector, lotId, auctionParams) ); - - // Check values - assertEq(auctionModule.getLot(lotId).start, auctionParams.start); } function test_success() external { @@ -283,6 +289,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { lotData.minBidSize, (auctionParams.capacity * auctionDataParams.minBidPercent) / 100_000 ); assertEq(lotData.publicKeyModulus, auctionDataParams.publicKeyModulus); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Created)); // Check that the sorted bid queue is initialised (uint96 nextBidId_,) = auctionModule.lotSortedBids(lotId); From 3cf56590e65f37cf101aebd46842741dbc8dcd42 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 12:46:47 +0400 Subject: [PATCH 064/117] LSBBA: Add tests for cancelAuction() --- src/modules/Auction.sol | 13 +- src/modules/auctions/LSBBA/LSBBA.sol | 10 +- test/modules/auctions/LSBBA/auction.t.sol | 3 +- .../auctions/LSBBA/cancelAuction.t.sol | 209 ++++++++++++++++++ 4 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 test/modules/auctions/LSBBA/cancelAuction.t.sol diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 0d034194..ed88698e 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -253,21 +253,22 @@ abstract contract AuctionModule is Auction, Module { /// This function reverts if: /// - the caller is not the parent of the module /// - the lot id is invalid - /// - the lot is not active + /// - the lot has concluded /// /// @param lotId_ The lot id function cancelAuction(uint96 lotId_) external override onlyInternal { // Validation _revertIfLotInvalid(lotId_); - _revertIfLotInactive(lotId_); + _revertIfLotConcluded(lotId_); + + // Call internal closeAuction function to update any other required parameters + _cancelAuction(lotId_); + // Update lot Lot storage lot = lotData[lotId_]; lot.conclusion = uint48(block.timestamp); lot.capacity = 0; - - // Call internal closeAuction function to update any other required parameters - _cancelAuction(lotId_); } /// @notice Implementation-specific auction cancellation logic @@ -501,7 +502,7 @@ abstract contract AuctionModule is Auction, Module { /// @notice Checks that the lot represented by `lotId_` has started /// @dev Should revert if the lot has started function _revertIfLotStarted(uint96 lotId_) internal view virtual { - if (lotData[lotId_].start > uint48(block.timestamp)) revert Auction_MarketActive(lotId_); + if (lotData[lotId_].start <= uint48(block.timestamp)) revert Auction_MarketActive(lotId_); } /// @notice Checks that the lot represented by `lotId_` has not concluded diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index b911fe24..6b71a362 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -7,6 +7,8 @@ import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {console2} from "forge-std/console2.sol"; + // A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends // The auction occurs in three phases: // 1. Bidding - bidders submit encrypted bids @@ -466,14 +468,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { } 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 - if (lotData[lotId_].start <= block.timestamp) revert Auction_WrongState(); + _revertIfLotActive(lotId_); // Auction cannot be cancelled once it has concluded - if ( - auctionData[lotId_].status != AuctionStatus.Created - || block.timestamp < lotData[lotId_].conclusion - ) revert Auction_WrongState(); + _revertIfLotConcluded(lotId_); // Set auction status to settled so that bids can be refunded auctionData[lotId_].status = AuctionStatus.Settled; diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol index c6d117d2..cceb18da 100644 --- a/test/modules/auctions/LSBBA/auction.t.sol +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -258,8 +258,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call auctionHouse.execOnModule( - moduleVeecode, - abi.encodeWithSelector(Auction.auction.selector, lotId, auctionParams) + moduleVeecode, abi.encodeWithSelector(Auction.auction.selector, lotId, auctionParams) ); } diff --git a/test/modules/auctions/LSBBA/cancelAuction.t.sol b/test/modules/auctions/LSBBA/cancelAuction.t.sol new file mode 100644 index 00000000..4a3341ab --- /dev/null +++ b/test/modules/auctions/LSBBA/cancelAuction.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; + +contract LSBBACancelAuctionTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + // Function parameters + uint96 internal lotId = 1; + Auction.AuctionParams internal auctionParams; + LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + auctionDataParams = LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: new bytes(128) + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + + auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: 10e18, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier givenLotHasStarted() { + vm.warp(lotStart + 1); + _; + } + + modifier givenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + + modifier givenLotHasDecrypted() { + // Decrypt the bids (none) + LocalSealedBidBatchAuction.Decrypt[] memory decrypts = + new LocalSealedBidBatchAuction.Decrypt[](0); + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier givenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + // ===== Tests ===== // + + // [X] when the caller is not the parent + // [X] it reverts + // [X] when the lot id is invalid + // [X] it reverts + // [X] given the auction has already started + // [X] it reverts + // [X] given the auction decryption has started + // [X] it reverts + // [X] given the auction has concluded + // [X] it reverts + // [X] given the auction has been settled + // [X] it reverts + // [X] when the caller is using execOnModule + // [X] it reverts + // [X] it marks the auction as settled + + function test_whenCallerIsNotParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call + auctionModule.cancelAuction(lotId); + } + + function test_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + } + + function test_givenLotHasStarted_reverts() public givenLotHasStarted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + } + + function test_givenLotHasConcluded_reverts() public givenLotHasConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + } + + function test_givenLotHasDecrypted_reverts() public givenLotHasConcluded givenLotHasDecrypted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + } + + function test_givenLotHasSettled_reverts() + public + givenLotHasConcluded + givenLotHasDecrypted + givenLotHasSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + } + + function test_execOnModule_reverts() public { + Veecode moduleVeecode = auctionModule.VEECODE(); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) + ); + vm.expectRevert(err); + + // Call + auctionHouse.execOnModule( + moduleVeecode, abi.encodeWithSelector(Auction.cancelAuction.selector, lotId) + ); + } + + function test_success() public { + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + + // Assert Lot values + Auction.Lot memory lot = auctionModule.getLot(lotId); + assertEq(lot.conclusion, block.timestamp); + assertEq(lot.capacity, 0); + + // Assert Auction values + LocalSealedBidBatchAuction.AuctionData memory auctionData = auctionModule.getLotData(lotId); + assertEq(uint8(auctionData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Settled)); + } +} From bf02f3f01215808243a04bf7e0cc8b148e7a135f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 14:12:25 +0400 Subject: [PATCH 065/117] Tests for LSBBA.bid() --- src/modules/Auction.sol | 6 +- src/modules/auctions/LSBBA/LSBBA.sol | 9 +- test/modules/auctions/LSBBA/bid.t.sol | 263 ++++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 test/modules/auctions/LSBBA/bid.t.sol diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index ed88698e..56813b6f 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -328,7 +328,8 @@ abstract contract AuctionModule is Auction, Module { /// /// This function reverts if: /// - the lot id is invalid - /// - the lot is not active + /// - the lot has not started + /// - the lot has concluded /// - the lot is already settled /// - the caller is not an internal module /// @@ -345,7 +346,8 @@ abstract contract AuctionModule is Auction, Module { ) external override onlyInternal returns (uint256 bidId) { // Standard validation _revertIfLotInvalid(lotId_); - _revertIfLotInactive(lotId_); + _revertIfBeforeLotStart(lotId_); + _revertIfLotConcluded(lotId_); _revertIfLotSettled(lotId_); // Call implementation-specific logic diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 6b71a362..92a5f736 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -164,7 +164,10 @@ contract LocalSealedBidBatchAuction is AuctionModule { ) internal override returns (uint256 bidId) { // Validate inputs // Amount at least minimum bid size for lot - if (amount_ < auctionData[lotId_].minBidSize) revert Auction_WrongState(); + if (amount_ < auctionData[lotId_].minBidSize) revert Auction_AmountLessThanMinimum(); + + // Amount greater than capacity + if (amount_ > lotData[lotId_].capacity) revert Auction_NotEnoughCapacity(); // Store bid data // Auction data should just be the encrypted amount out (no decoding required) @@ -497,6 +500,10 @@ contract LocalSealedBidBatchAuction is AuctionModule { return auctionData[lotId_]; } + function getBidData(uint96 lotId_, uint256 bidId_) public view returns (EncryptedBid memory) { + return lotEncryptedBids[lotId_][bidId_]; + } + // =========== ATOMIC AUCTION STUBS ========== // /// @inheritdoc AuctionModule diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol new file mode 100644 index 00000000..ddb36748 --- /dev/null +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; + +contract LSBBABidTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + address internal constant alice = address(0x2); + address internal constant recipient = address(0x3); + address internal constant referrer = address(0x4); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + uint96 internal lotId = 1; + bytes internal auctionData; + + uint256 internal MIN_BID_SIZE; + uint256 internal bidAmount = 1e18; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = + LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: new bytes(128) + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + MIN_BID_SIZE = 1000 * LOT_CAPACITY / 100_000; + + Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + auctionData = abi.encode(1e9); // Encrypted amount out + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier givenLotHasStarted() { + vm.warp(lotStart + 1); + _; + } + + modifier givenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + + modifier givenLotHasDecrypted() { + // Decrypt the bids (none) + LocalSealedBidBatchAuction.Decrypt[] memory decrypts = + new LocalSealedBidBatchAuction.Decrypt[](0); + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier givenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + modifier whenAmountIsSmallerThanMinimumBidAmount() { + bidAmount = MIN_BID_SIZE - 1; + _; + } + + modifier whenAmountIsLargerThanCapacity() { + bidAmount = LOT_CAPACITY + 1; + _; + } + + // ===== Tests ===== // + + // [ ] when the caller is not the parent + // [ ] it reverts + // [ ] when the lot id is invalid + // [ ] it reverts + // [ ] when the lot has not started + // [ ] it reverts + // [ ] when the lot has concluded + // [ ] it reverts + // [ ] when the lot has decrypted + // [ ] it reverts + // [ ] when the lot has settled + // [ ] it reverts + // [ ] when the amount is smaller than the minimum bid amount + // [ ] it reverts + // [ ] when the amount is larger than the capacity + // [ ] it reverts + // [ ] it records the encrypted bid + + function test_whenCallerIsNotParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenLotIdIsInvalid() public whenLotIdIsInvalid givenLotHasStarted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenLotHasNotStarted() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenLotHasConcluded() public givenLotHasConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenLotHasDecrypted() public givenLotHasConcluded givenLotHasDecrypted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenLotHasSettled() + public + givenLotHasConcluded + givenLotHasDecrypted + givenLotHasSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenAmountIsSmallerThanMinimumBidAmount() + public + givenLotHasStarted + whenAmountIsSmallerThanMinimumBidAmount + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_AmountLessThanMinimum.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_whenAmountIsLargerThanCapacity() + public + givenLotHasStarted + whenAmountIsLargerThanCapacity + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotEnoughCapacity.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function test_execOnModule_reverts() public { + Veecode moduleVeecode = auctionModule.VEECODE(); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) + ); + vm.expectRevert(err); + + // Call + auctionHouse.execOnModule( + moduleVeecode, + abi.encodeWithSelector( + Auction.bid.selector, lotId, alice, recipient, referrer, bidAmount, auctionData + ) + ); + } + + function test_itRecordsTheEncryptedBid() public givenLotHasStarted { + // Call + vm.prank(address(auctionHouse)); + uint256 bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + + // Check values + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidId); + assertEq(encryptedBid.bidder, alice); + assertEq(encryptedBid.recipient, recipient); + assertEq(encryptedBid.referrer, referrer); + assertEq(encryptedBid.amount, bidAmount); + assertEq(encryptedBid.encryptedAmountOut, auctionData); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Submitted)); + } +} From ab859d12c71c4864edb298059b5f8d39678253fa Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 14:18:11 +0400 Subject: [PATCH 066/117] chore: linting --- src/modules/Auction.sol | 2 +- src/modules/auctions/LSBBA/LSBBA.sol | 36 +++++++++------------ src/modules/auctions/bases/BatchAuction.sol | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 56813b6f..ae12e356 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -70,7 +70,7 @@ abstract contract Auction { uint48 public minAuctionDuration; // 1% = 1_000 or 1e3. 100% = 100_000 or 1e5. - uint48 internal constant _ONE_HUNDRED_PERCENT = 1e5; + uint48 internal constant _ONE_HUNDRED_PERCENT = 100_000; /// @notice General information pertaining to auction lots mapping(uint256 id => Lot lot) public lotData; diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 92a5f736..ee0f19b2 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -1,14 +1,11 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -// import "src/modules/auctions/bases/BatchAuction.sol"; -import {Auction, AuctionModule} from "src/modules/Auction.sol"; -import {Veecode, toVeecode, Module} from "src/modules/Modules.sol"; +import {AuctionModule} from "src/modules/Auction.sol"; +import {Veecode, toVeecode} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; -import {console2} from "forge-std/console2.sol"; - // A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends // The auction occurs in three phases: // 1. Bidding - bidders submit encrypted bids @@ -66,8 +63,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @notice Struct containing parameters for creating a new LSBBA auction /// - /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (scale: `ONE_HUNDRED_PERCENT`) - /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (scale: `ONE_HUNDRED_PERCENT`) + /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (_SCALE: `_ONE_HUNDRED_PERCENT`) + /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (_SCALE: `_ONE_HUNDRED_PERCENT`) /// @param minimumPrice_ The minimum price that the auction can settle at /// @param publicKeyModulus_ The public key modulus used to encrypt bids struct AuctionDataParams { @@ -79,10 +76,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { // ========== STATE VARIABLES ========== // - uint256 internal constant MIN_BID_PERCENT = 1000; // 1% - uint256 internal constant ONE_HUNDRED_PERCENT = 100_000; - uint256 internal constant PUB_KEY_EXPONENT = 65_537; // TODO can be 3 to save gas, but 65537 is probably more secure - uint256 internal constant SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up + uint24 internal constant _MIN_BID_PERCENT = 1000; // 1% + uint24 internal constant _PUB_KEY_EXPONENT = 65_537; // TODO can be 3 to save gas, but 65537 is probably more secure + uint256 internal constant _SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => EncryptedBid[] bids) public lotEncryptedBids; @@ -269,7 +265,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { return RSAOAEP.encrypt( abi.encodePacked(decrypt_.amountOut), abi.encodePacked(lotId_), - abi.encodePacked(PUB_KEY_EXPONENT), + abi.encodePacked(_PUB_KEY_EXPONENT), auctionData[lotId_].publicKeyModulus, decrypt_.seed ); @@ -352,13 +348,13 @@ contract LocalSealedBidBatchAuction is AuctionModule { QueueBid storage qBid = queue.getBid(i); // Calculate bid price - uint256 price = (qBid.amountIn * SCALE) / qBid.minAmountOut; + uint256 price = (qBid.amountIn * _SCALE) / qBid.minAmountOut; // Increment total amount in totalAmountIn += qBid.amountIn; // Determine total capacity expended at this price - uint256 expended = (totalAmountIn * SCALE) / price; + uint256 expended = (totalAmountIn * _SCALE) / price; // If total capacity expended is greater than or equal to the capacity, we have found the marginal price if (expended >= capacity) { @@ -399,7 +395,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { // amountIn, and amountOut will be lower // Need to somehow refund the amountIn that wasn't used to the user // We know it will always be the last bid in the returned array, can maybe do something with that - uint256 amountOut = (qBid.amountIn * SCALE) / marginalPrice; + uint256 amountOut = (qBid.amountIn * _SCALE) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.encId]; @@ -442,13 +438,13 @@ contract LocalSealedBidBatchAuction is AuctionModule { // minFillPercent must be less than or equal to 100% // TODO should there be a minimum? - if (implParams.minFillPercent > ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); + if (implParams.minFillPercent > _ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); // minBidPercent must be greater than or equal to the global min and less than or equal to 100% // TODO should we cap this below 100%? if ( - implParams.minBidPercent < MIN_BID_PERCENT - || implParams.minBidPercent > ONE_HUNDRED_PERCENT + implParams.minBidPercent < _MIN_BID_PERCENT + || implParams.minBidPercent > _ONE_HUNDRED_PERCENT ) { revert Auction_InvalidParams(); } @@ -459,8 +455,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Store auction data AuctionData storage data = auctionData[lotId_]; data.minimumPrice = implParams.minimumPrice; - data.minFilled = (lot_.capacity * implParams.minFillPercent) / ONE_HUNDRED_PERCENT; - data.minBidSize = (lot_.capacity * implParams.minBidPercent) / ONE_HUNDRED_PERCENT; + data.minFilled = (lot_.capacity * implParams.minFillPercent) / _ONE_HUNDRED_PERCENT; + data.minBidSize = (lot_.capacity * implParams.minBidPercent) / _ONE_HUNDRED_PERCENT; data.publicKeyModulus = implParams.publicKeyModulus; // Initialize sorted bid queue diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 79a8a91c..283a54e1 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.19; // TODO may not need this file. Lot of implementation specifics. -import {Auction, AuctionModule} from "src/modules/Auction.sol"; +import {AuctionModule} from "src/modules/Auction.sol"; // Spec // - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token From ed3522b8b8a8fc64c5f535a263c58fb5aab2d107 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 14:21:44 +0400 Subject: [PATCH 067/117] Update TODOs --- test/modules/auctions/LSBBA/bid.t.sol | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index ddb36748..52f580da 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -116,23 +116,25 @@ contract LSBBABidTest is Test, Permit2User { // ===== Tests ===== // - // [ ] when the caller is not the parent - // [ ] it reverts - // [ ] when the lot id is invalid - // [ ] it reverts - // [ ] when the lot has not started - // [ ] it reverts - // [ ] when the lot has concluded - // [ ] it reverts - // [ ] when the lot has decrypted - // [ ] it reverts - // [ ] when the lot has settled - // [ ] it reverts - // [ ] when the amount is smaller than the minimum bid amount - // [ ] it reverts - // [ ] when the amount is larger than the capacity - // [ ] it reverts - // [ ] it records the encrypted bid + // [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 decrypted + // [X] it reverts + // [X] when the lot has settled + // [X] it reverts + // [X] when the amount is smaller than the minimum bid amount + // [X] it reverts + // [X] when the amount is larger than the capacity + // [X] it reverts + // [X] when the caller is using execOnModule + // [X] it reverts + // [X] it records the encrypted bid function test_whenCallerIsNotParent_reverts() public { // Expect revert From adcf9ff90ddaf03315025dc95a5f7a2a4e6d637f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 14:28:16 +0400 Subject: [PATCH 068/117] Correct suffix --- test/modules/auctions/LSBBA/bid.t.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index 52f580da..5cd08895 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -145,7 +145,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenLotIdIsInvalid() public whenLotIdIsInvalid givenLotHasStarted { + function test_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid givenLotHasStarted { // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); vm.expectRevert(err); @@ -155,7 +155,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenLotHasNotStarted() public { + function test_whenLotHasNotStarted_reverts() public { // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); @@ -165,7 +165,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenLotHasConcluded() public givenLotHasConcluded { + function test_whenLotHasConcluded_reverts() public givenLotHasConcluded { // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); @@ -175,7 +175,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenLotHasDecrypted() public givenLotHasConcluded givenLotHasDecrypted { + function test_whenLotHasDecrypted_reverts() public givenLotHasConcluded givenLotHasDecrypted { // Expect revert bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); vm.expectRevert(err); @@ -185,7 +185,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenLotHasSettled() + function test_whenLotHasSettled_reverts() public givenLotHasConcluded givenLotHasDecrypted @@ -200,7 +200,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenAmountIsSmallerThanMinimumBidAmount() + function test_whenAmountIsSmallerThanMinimumBidAmount_reverts() public givenLotHasStarted whenAmountIsSmallerThanMinimumBidAmount @@ -214,7 +214,7 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenAmountIsLargerThanCapacity() + function test_whenAmountIsLargerThanCapacity_reverts() public givenLotHasStarted whenAmountIsLargerThanCapacity From 45b59d4f07c4f79264c8f8cfe3b03662556c4863 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 14:49:45 +0400 Subject: [PATCH 069/117] LSBBA: tests for cancelBid() --- src/modules/Auction.sol | 3 +- src/modules/auctions/LSBBA/LSBBA.sol | 6 +- test/modules/auctions/LSBBA/cancelBid.t.sol | 289 ++++++++++++++++++++ 3 files changed, 295 insertions(+), 3 deletions(-) create mode 100644 test/modules/auctions/LSBBA/cancelBid.t.sol diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index ae12e356..d3ab0732 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -399,7 +399,8 @@ abstract contract AuctionModule is Auction, Module { ) external override onlyInternal returns (uint256 bidAmount) { // Standard validation _revertIfLotInvalid(lotId_); - _revertIfLotInactive(lotId_); + _revertIfBeforeLotStart(lotId_); + _revertIfLotConcluded(lotId_); _revertIfLotSettled(lotId_); _revertIfBidInvalid(lotId_, bidId_); _revertIfNotBidOwner(lotId_, bidId_, caller_); diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index ee0f19b2..6eee35fa 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -117,14 +117,16 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @dev Checks that the lot is not yet settled function _revertIfLotSettled(uint96 lotId_) internal view override { // Auction must not be settled - if (auctionData[lotId_].status == AuctionStatus.Settled) revert Auction_WrongState(); + if (auctionData[lotId_].status == AuctionStatus.Settled) { + revert Auction_MarketNotActive(lotId_); + } } /// @inheritdoc AuctionModule /// @dev Checks that the bid is valid function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view override { // Bid ID must be less than number of bids for lot - if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_BidDoesNotExist(); + if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_InvalidBidId(lotId_, bidId_); } /// @inheritdoc AuctionModule diff --git a/test/modules/auctions/LSBBA/cancelBid.t.sol b/test/modules/auctions/LSBBA/cancelBid.t.sol new file mode 100644 index 00000000..d0d874f5 --- /dev/null +++ b/test/modules/auctions/LSBBA/cancelBid.t.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {RSAOAEP} from "src/lib/RSA.sol"; + +contract LSBBACancelBidTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + address internal alice = address(0x2); + address internal constant recipient = address(0x3); + address internal constant referrer = address(0x4); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + uint96 internal lotId = 1; + bytes internal auctionData; + bytes internal constant PUBLIC_KEY_MODULUS = new bytes(128); + + uint256 internal bidId; + uint256 internal bidAmount = 1e18; + uint256 internal bidSeed = 1e9; + LocalSealedBidBatchAuction.Decrypt internal decryptedBid; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = + LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: PUBLIC_KEY_MODULUS + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + + Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + // Warp to the start of the auction + vm.warp(lotStart); + + // Encrypt the bid amount + decryptedBid = LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmount, seed: bidSeed}); + auctionData = _encrypt(decryptedBid); + + // Create a bid + vm.prank(address(auctionHouse)); + bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + } + + function _encrypt(LocalSealedBidBatchAuction.Decrypt memory decrypt_) + internal + view + returns (bytes memory) + { + return RSAOAEP.encrypt( + abi.encodePacked(decrypt_.amountOut), + abi.encodePacked(lotId), + abi.encodePacked(uint24(65_537)), + PUBLIC_KEY_MODULUS, + decrypt_.seed + ); + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier whenBidIdIsInvalid() { + bidId = 2; + _; + } + + modifier whenCallerIsNotBidder() { + alice = address(0x10); + _; + } + + modifier givenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + + modifier givenLotHasDecrypted() { + // Decrypt the bids + LocalSealedBidBatchAuction.Decrypt[] memory decrypts = + new LocalSealedBidBatchAuction.Decrypt[](1); + decrypts[0] = decryptedBid; + + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier givenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + modifier givenBidHasBeenCancelled() { + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + _; + } + + // ===== Tests ===== // + + // [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 bidder + // [X] it reverts + // [X] when the lot has concluded + // [X] it reverts + // [X] when the lot has decrypted + // [X] it reverts + // [X] when the lot has settled + // [X] it reverts + // [X] when the bid has already been cancelled + // [X] it reverts + // [X] when the caller is using execOnModule + // [X] it reverts + // [X] it updates the bid details + + function test_whenCallerIsNotParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_whenBidIdIsInvalid_reverts() public whenBidIdIsInvalid { + // Expect revert + bytes memory err = + abi.encodeWithSelector(Auction.Auction_InvalidBidId.selector, lotId, bidId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_whenCallerIsNotBidder_reverts() public whenCallerIsNotBidder { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_NotBidder.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_givenLotHasConcluded_reverts() public givenLotHasConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_givenLotHasDecrypted_reverts() public givenLotHasConcluded givenLotHasDecrypted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_givenLotHasSettled_reverts() + public + givenLotHasConcluded + givenLotHasDecrypted + givenLotHasSettled + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketNotActive.selector, lotId); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_givenBidHasBeenCancelled_reverts() public givenBidHasBeenCancelled { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_AlreadyCancelled.selector); + vm.expectRevert(err); + + // Call + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId, alice); + } + + function test_whenCallerIsUsingExecOnModule_reverts() public { + Veecode moduleVeecode = auctionModule.VEECODE(); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) + ); + vm.expectRevert(err); + + // Call + auctionHouse.execOnModule( + moduleVeecode, + abi.encodeWithSelector(auctionModule.cancelBid.selector, lotId, bidId, alice) + ); + } + + function test_itUpdatesTheBidDetails() public { + // Call + vm.prank(address(auctionHouse)); + uint256 returnedBidAmount = auctionModule.cancelBid(lotId, bidId, alice); + + // Check values + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidId); + assertEq(encryptedBid.bidder, alice); + assertEq(encryptedBid.recipient, recipient); + assertEq(encryptedBid.referrer, referrer); + assertEq(encryptedBid.amount, bidAmount); + assertEq(encryptedBid.encryptedAmountOut, auctionData); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Cancelled)); + + // Check return value + assertEq(returnedBidAmount, bidAmount); + } +} From 2fa8637b9b127bdc37cb515e605e5d19a0065414 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 17:01:25 +0400 Subject: [PATCH 070/117] LSBBA: WIP tests for bid decryption --- src/modules/auctions/LSBBA/LSBBA.sol | 11 + .../auctions/LSBBA/decryptAndSortBids.t.sol | 506 ++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 test/modules/auctions/LSBBA/decryptAndSortBids.t.sol diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 6eee35fa..13f7726d 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -502,6 +502,17 @@ contract LocalSealedBidBatchAuction is AuctionModule { return lotEncryptedBids[lotId_][bidId_]; } + function getSortedBidData( + uint96 lotId_, + uint256 bidId_ + ) public view returns (QueueBid memory) { + return lotSortedBids[lotId_].getBid(bidId_); + } + + function getSortedBidCount(uint96 lotId_) public view returns (uint256) { + return lotSortedBids[lotId_].numBids; + } + // =========== ATOMIC AUCTION STUBS ========== // /// @inheritdoc AuctionModule diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol new file mode 100644 index 00000000..bfffae4b --- /dev/null +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {RSAOAEP} from "src/lib/RSA.sol"; +import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; + +contract LSBBACancelBidTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + address internal alice = address(0x2); + address internal constant recipient = address(0x3); + address internal constant referrer = address(0x4); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + uint96 internal lotId = 1; + bytes internal auctionData; + bytes internal constant PUBLIC_KEY_MODULUS = new bytes(128); + + uint256 internal bidSeed = 1e9; + uint256 internal bidOne; + uint256 internal bidOneAmount = 1e18; + uint256 internal bidOneAmountOut = 3e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidOne; + uint256 internal bidTwo; + uint256 internal bidTwoAmount = 1e18; + uint256 internal bidTwoAmountOut = 2e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidTwo; + uint256 internal bidThree; + uint256 internal bidThreeAmount = 1e18; + uint256 internal bidThreeAmountOut = 7e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; + LocalSealedBidBatchAuction.Decrypt[] internal decrypts; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = + LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: PUBLIC_KEY_MODULUS + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + + Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + // Warp to the start of the auction + vm.warp(lotStart); + + // Create three bids + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); + (bidThree, decryptedBidThree) = _createBid(bidThreeAmount, bidThreeAmountOut); + decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); + decrypts[0] = decryptedBidOne; + decrypts[1] = decryptedBidTwo; + decrypts[2] = decryptedBidThree; + + // Go to conclusion + vm.warp(lotConclusion + 1); + } + + function _createBid( + uint256 bidAmount_, + uint256 bidAmountOut_ + ) internal returns (uint256 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid_) { + // Encrypt the bid amount + LocalSealedBidBatchAuction.Decrypt memory decryptedBid = + LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: bidSeed}); + bytes memory auctionData_ = _encrypt(decryptedBid); + + // Create a bid + vm.prank(address(auctionHouse)); + bidId_ = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount_, auctionData_); + + return (bidId_, decryptedBid); + } + + function _encrypt(LocalSealedBidBatchAuction.Decrypt memory decrypt_) + internal + view + returns (bytes memory) + { + return RSAOAEP.encrypt( + abi.encodePacked(decrypt_.amountOut), + abi.encodePacked(lotId), + abi.encodePacked(uint24(65_537)), + PUBLIC_KEY_MODULUS, + decrypt_.seed + ); + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier whenLotHasNotConcluded() { + vm.warp(lotConclusion - 1); + _; + } + + modifier whenLotDecryptionIsComplete() { + // Decrypt the bids + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier whenDecryptedBidLengthIsGreater() { + // Decrypt 1 bid + decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); + decrypts[0] = decryptedBidOne; + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Prepare to decrypt 3 bids + decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); + decrypts[0] = decryptedBidTwo; + decrypts[1] = decryptedBidThree; + decrypts[2] = decryptedBidOne; + _; + } + + modifier whenDecryptedBidLengthIsZero() { + // Empty array + decrypts = new LocalSealedBidBatchAuction.Decrypt[](0); + _; + } + + modifier whenBidsAreOutOfOrder() { + // Re-arrange the bids + decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); + decrypts[0] = decryptedBidTwo; + decrypts[1] = decryptedBidOne; + decrypts[2] = decryptedBidThree; + _; + } + + modifier whenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + modifier whenBidHasBeenCancelled(uint256 bidId_) { + vm.prank(address(auctionHouse)); + auctionModule.cancelBid(lotId, bidId_, alice); + _; + } + + modifier whenDecryptedBidDoesNotMatch() { + // Change a decrypted bid + decryptedBidOne.amountOut = decryptedBidOne.amountOut + 1; + _; + } + + // ===== Tests ===== // + + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the caller is the auction house + // [X] it succeeds + // [X] given the lot has not concluded + // [X] it reverts + // [X] given the lot has been fully decrypted + // [X] it reverts + // [X] given the lot has been settled + // [X] it reverts + // [X] when the number of decrypted bids is more than the remaining encrypted bids + // [X] it reverts + // [X] when the decrypted bids array is empty + // [X] it does nothing + // [X] when a decrypted bid does not match the encrypted bid + // [X] it reverts + // [X] when the decrypted bids are out of order + // [X] it reverts + // [X] when a cancelled bid is passed in + // [X] it reverts + // [X] given an encrypted bid has been cancelled + // [X] it does not consider the cancelled bid + // [X] when encrypted bids remain after decryption + // [X] it updates the nextDecryptIndex + // [X] it updates the lot status to decrypted + + function test_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_whenCallerIsAuctionHouse() public { + // Call + vm.prank(address(auctionHouse)); + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_givenLotHasNotConcluded_reverts() public whenLotHasNotConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_givenLotDecryptionIsComplete_reverts() public whenLotDecryptionIsComplete { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_givenLotHasSettled_reverts() + public + whenLotDecryptionIsComplete + whenLotHasSettled + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_whenDecryptedBidLengthIsGreater_reverts() + public + whenDecryptedBidLengthIsGreater + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_whenDecryptedBidsLengthIsZero() public whenDecryptedBidLengthIsZero { + // Get the index beforehand + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + uint96 nextDecryptIndexBefore = lotData.nextDecryptIndex; + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Check the index + lotData = auctionModule.getLotData(lotId); + assertEq(lotData.nextDecryptIndex, nextDecryptIndexBefore); + } + + function test_bidsOutOfOrder_reverts() public whenBidsAreOutOfOrder { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_givenBidHasBeenCancelled_reverts() public whenBidHasBeenCancelled(bidOne) { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); + vm.expectRevert(err); + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + } + + function test_givenBidHasBeenCancelled() public whenBidHasBeenCancelled(bidOne) { + // Amend the decrypts array + decrypts = new LocalSealedBidBatchAuction.Decrypt[](2); + decrypts[0] = decryptedBidTwo; + decrypts[1] = decryptedBidThree; + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Check values on auction data + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + assertEq(lotData.nextDecryptIndex, 2); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Decrypted)); + + // Check encrypted bids + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidOne); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Cancelled)); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidTwo = + auctionModule.getBidData(lotId, bidTwo); + assertEq( + uint8(encryptedBidTwo.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidThree = + auctionModule.getBidData(lotId, bidThree); + assertEq( + uint8(encryptedBidThree.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + + // Check sorted bids + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); + assertEq(sortedBidOne.bidId, 0); + assertEq(sortedBidOne.encId, bidThree); + assertEq(sortedBidOne.amountIn, bidThreeAmount); + assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidTwo.bidId, 1); + assertEq(sortedBidTwo.encId, bidTwo); + assertEq(sortedBidTwo.amountIn, bidTwoAmount); + assertEq(sortedBidTwo.minAmountOut, bidTwoAmountOut); + + assertEq(auctionModule.getSortedBidCount(lotId), 2); + } + + function test_partialDecryption() public { + // Amend the decrypts array + decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); + decrypts[0] = decryptedBidOne; + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Check values on auction data + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + assertEq(lotData.nextDecryptIndex, 1); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Decrypted)); + + // Check encrypted bids + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidOne); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted)); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidTwo = + auctionModule.getBidData(lotId, bidTwo); + assertEq( + uint8(encryptedBidTwo.status), uint8(LocalSealedBidBatchAuction.BidStatus.Submitted) + ); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidThree = + auctionModule.getBidData(lotId, bidThree); + assertEq( + uint8(encryptedBidThree.status), uint8(LocalSealedBidBatchAuction.BidStatus.Submitted) + ); + + // Check sorted bids + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); + assertEq(sortedBidOne.bidId, 0); + assertEq(sortedBidOne.encId, bidOne); + assertEq(sortedBidOne.amountIn, bidOneAmount); + assertEq(sortedBidOne.minAmountOut, bidOneAmountOut); + + assertEq(auctionModule.getSortedBidCount(lotId), 1); + } + + function test_partialDecryptionThenFull() public { + // Amend the decrypts array + decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); + decrypts[0] = decryptedBidOne; + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Decrypt the rest + decrypts = new LocalSealedBidBatchAuction.Decrypt[](2); + decrypts[0] = decryptedBidTwo; + decrypts[1] = decryptedBidThree; + + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Check values on auction data + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + assertEq(lotData.nextDecryptIndex, 3); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Decrypted)); + + // Check encrypted bids + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidOne); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted)); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidTwo = + auctionModule.getBidData(lotId, bidTwo); + assertEq( + uint8(encryptedBidTwo.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidThree = + auctionModule.getBidData(lotId, bidThree); + assertEq( + uint8(encryptedBidThree.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + + // Check sorted bids + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); + assertEq(sortedBidOne.bidId, 0); + assertEq(sortedBidOne.encId, bidThree); + assertEq(sortedBidOne.amountIn, bidThreeAmount); + assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidTwo.bidId, 1); + assertEq(sortedBidTwo.encId, bidOne); + assertEq(sortedBidTwo.amountIn, bidOneAmount); + assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); + + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); + assertEq(sortedBidThree.bidId, 2); + assertEq(sortedBidThree.encId, bidTwo); + assertEq(sortedBidThree.amountIn, bidTwoAmount); + assertEq(sortedBidThree.minAmountOut, bidTwoAmountOut); + + assertEq(auctionModule.getSortedBidCount(lotId), 3); + } + + function test_fullDecryption() public { + // Call + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Check values on auction data + LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); + assertEq(lotData.nextDecryptIndex, 3); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Decrypted)); + + // Check encrypted bids + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidOne); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted)); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidTwo = + auctionModule.getBidData(lotId, bidTwo); + assertEq( + uint8(encryptedBidTwo.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBidThree = + auctionModule.getBidData(lotId, bidThree); + assertEq( + uint8(encryptedBidThree.status), uint8(LocalSealedBidBatchAuction.BidStatus.Decrypted) + ); + + // Check sorted bids + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); + assertEq(sortedBidOne.bidId, 0); + assertEq(sortedBidOne.encId, bidThree); + assertEq(sortedBidOne.amountIn, bidThreeAmount); + assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidTwo.bidId, 1); + assertEq(sortedBidTwo.encId, bidOne); + assertEq(sortedBidTwo.amountIn, bidOneAmount); + assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); + + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); + assertEq(sortedBidThree.bidId, 2); + assertEq(sortedBidThree.encId, bidTwo); + assertEq(sortedBidThree.amountIn, bidTwoAmount); + assertEq(sortedBidThree.minAmountOut, bidTwoAmountOut); + + assertEq(auctionModule.getSortedBidCount(lotId), 3); + } +} From 6e58aa028b4e34f05cab724552c4bdb26836b8ed Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 25 Jan 2024 17:14:08 +0400 Subject: [PATCH 071/117] Fix compiler error --- .../auctions/LSBBA/decryptAndSortBids.t.sol | 101 +++++++++++------- 1 file changed, 65 insertions(+), 36 deletions(-) diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index bfffae4b..648fa63d 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -90,13 +90,11 @@ contract LSBBACancelBidTest is Test, Permit2User { (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); (bidThree, decryptedBidThree) = _createBid(bidThreeAmount, bidThreeAmountOut); - decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); - decrypts[0] = decryptedBidOne; - decrypts[1] = decryptedBidTwo; - decrypts[2] = decryptedBidThree; - // Go to conclusion - vm.warp(lotConclusion + 1); + // Set up the decrypts array + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidThree); } function _createBid( @@ -129,6 +127,14 @@ contract LSBBACancelBidTest is Test, Permit2User { ); } + function _clearDecrypts() internal { + uint256 len = decrypts.length; + // Remove all elements + for (uint256 i = 0; i < len; i++) { + delete decrypts[i]; + } + } + // ===== Modifiers ===== // modifier whenLotIdIsInvalid() { @@ -141,6 +147,11 @@ contract LSBBACancelBidTest is Test, Permit2User { _; } + modifier whenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + modifier whenLotDecryptionIsComplete() { // Decrypt the bids auctionModule.decryptAndSortBids(lotId, decrypts); @@ -149,30 +160,30 @@ contract LSBBACancelBidTest is Test, Permit2User { modifier whenDecryptedBidLengthIsGreater() { // Decrypt 1 bid - decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); - decrypts[0] = decryptedBidOne; + _clearDecrypts(); + decrypts.push(decryptedBidOne); auctionModule.decryptAndSortBids(lotId, decrypts); // Prepare to decrypt 3 bids - decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); - decrypts[0] = decryptedBidTwo; - decrypts[1] = decryptedBidThree; - decrypts[2] = decryptedBidOne; + _clearDecrypts(); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidThree); + decrypts.push(decryptedBidOne); _; } modifier whenDecryptedBidLengthIsZero() { // Empty array - decrypts = new LocalSealedBidBatchAuction.Decrypt[](0); + _clearDecrypts(); _; } modifier whenBidsAreOutOfOrder() { // Re-arrange the bids - decrypts = new LocalSealedBidBatchAuction.Decrypt[](3); - decrypts[0] = decryptedBidTwo; - decrypts[1] = decryptedBidOne; - decrypts[2] = decryptedBidThree; + _clearDecrypts(); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidThree); _; } @@ -232,7 +243,7 @@ contract LSBBACancelBidTest is Test, Permit2User { auctionModule.decryptAndSortBids(lotId, decrypts); } - function test_whenCallerIsAuctionHouse() public { + function test_whenCallerIsAuctionHouse() public whenLotHasConcluded { // Call vm.prank(address(auctionHouse)); auctionModule.decryptAndSortBids(lotId, decrypts); @@ -247,7 +258,11 @@ contract LSBBACancelBidTest is Test, Permit2User { auctionModule.decryptAndSortBids(lotId, decrypts); } - function test_givenLotDecryptionIsComplete_reverts() public whenLotDecryptionIsComplete { + function test_givenLotDecryptionIsComplete_reverts() + public + whenLotHasConcluded + whenLotDecryptionIsComplete + { // Expect revert bytes memory err = abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); @@ -259,6 +274,7 @@ contract LSBBACancelBidTest is Test, Permit2User { function test_givenLotHasSettled_reverts() public + whenLotHasConcluded whenLotDecryptionIsComplete whenLotHasSettled { @@ -273,6 +289,7 @@ contract LSBBACancelBidTest is Test, Permit2User { function test_whenDecryptedBidLengthIsGreater_reverts() public + whenLotHasConcluded whenDecryptedBidLengthIsGreater { // Expect revert @@ -284,7 +301,11 @@ contract LSBBACancelBidTest is Test, Permit2User { auctionModule.decryptAndSortBids(lotId, decrypts); } - function test_whenDecryptedBidsLengthIsZero() public whenDecryptedBidLengthIsZero { + function test_whenDecryptedBidsLengthIsZero() + public + whenLotHasConcluded + whenDecryptedBidLengthIsZero + { // Get the index beforehand LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); uint96 nextDecryptIndexBefore = lotData.nextDecryptIndex; @@ -297,7 +318,7 @@ contract LSBBACancelBidTest is Test, Permit2User { assertEq(lotData.nextDecryptIndex, nextDecryptIndexBefore); } - function test_bidsOutOfOrder_reverts() public whenBidsAreOutOfOrder { + function test_bidsOutOfOrder_reverts() public whenLotHasConcluded whenBidsAreOutOfOrder { // Expect revert bytes memory err = abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); @@ -307,7 +328,11 @@ contract LSBBACancelBidTest is Test, Permit2User { auctionModule.decryptAndSortBids(lotId, decrypts); } - function test_givenBidHasBeenCancelled_reverts() public whenBidHasBeenCancelled(bidOne) { + function test_givenBidHasBeenCancelled_reverts() + public + whenBidHasBeenCancelled(bidOne) + whenLotHasConcluded + { // Expect revert bytes memory err = abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); @@ -317,11 +342,15 @@ contract LSBBACancelBidTest is Test, Permit2User { auctionModule.decryptAndSortBids(lotId, decrypts); } - function test_givenBidHasBeenCancelled() public whenBidHasBeenCancelled(bidOne) { + function test_givenBidHasBeenCancelled() + public + whenBidHasBeenCancelled(bidOne) + whenLotHasConcluded + { // Amend the decrypts array - decrypts = new LocalSealedBidBatchAuction.Decrypt[](2); - decrypts[0] = decryptedBidTwo; - decrypts[1] = decryptedBidThree; + _clearDecrypts(); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidThree); // Call auctionModule.decryptAndSortBids(lotId, decrypts); @@ -362,10 +391,10 @@ contract LSBBACancelBidTest is Test, Permit2User { assertEq(auctionModule.getSortedBidCount(lotId), 2); } - function test_partialDecryption() public { + function test_partialDecryption() public whenLotHasConcluded { // Amend the decrypts array - decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); - decrypts[0] = decryptedBidOne; + _clearDecrypts(); + decrypts.push(decryptedBidOne); // Call auctionModule.decryptAndSortBids(lotId, decrypts); @@ -400,18 +429,18 @@ contract LSBBACancelBidTest is Test, Permit2User { assertEq(auctionModule.getSortedBidCount(lotId), 1); } - function test_partialDecryptionThenFull() public { + function test_partialDecryptionThenFull() public whenLotHasConcluded { // Amend the decrypts array - decrypts = new LocalSealedBidBatchAuction.Decrypt[](1); - decrypts[0] = decryptedBidOne; + _clearDecrypts(); + decrypts.push(decryptedBidOne); // Call auctionModule.decryptAndSortBids(lotId, decrypts); // Decrypt the rest - decrypts = new LocalSealedBidBatchAuction.Decrypt[](2); - decrypts[0] = decryptedBidTwo; - decrypts[1] = decryptedBidThree; + _clearDecrypts(); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidThree); // Call auctionModule.decryptAndSortBids(lotId, decrypts); @@ -458,7 +487,7 @@ contract LSBBACancelBidTest is Test, Permit2User { assertEq(auctionModule.getSortedBidCount(lotId), 3); } - function test_fullDecryption() public { + function test_fullDecryption() public whenLotHasConcluded { // Call auctionModule.decryptAndSortBids(lotId, decrypts); From 843362ae511321ff00e577e18c5af7c07aa4b5d9 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 15:53:12 -0600 Subject: [PATCH 072/117] fix: RSA impl errors --- src/lib/RSA.sol | 142 +++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 80 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index 72a8f136..bc8efbdc 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -27,7 +27,6 @@ library RSAOAEP { bytes memory label ) internal view returns (bytes memory message, bytes32 seed) { // Implements 7.1.2 RSAES-OAEP-DECRYPT as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 - // Error messages are intentionally vague to prevent oracle attacks // 1. Input length validation // 1. a. If the length of L is greater than the input limitation @@ -57,36 +56,39 @@ library RSAOAEP { // 3. b. Separate encoded message into Y (1 byte) | maskedSeed (32 bytes) | maskedDB (cLen - 32 - 1) bytes1 y = bytes1(encoded); - bytes32 maskedSeed; - uint256 words = (cLen - 33) / 32 + ((cLen - 33) % 32 == 0 ? 0 : 1); - bytes memory maskedDb = new bytes(cLen - 33); - - assembly { - // Load a word from the encoded string starting at the 2nd byte (also have to account for length stored in first slot) - maskedSeed := mload(add(encoded, 0x21)) + + bytes memory db; + { // Scope these local variables to avoid stack too deep later + bytes32 maskedSeed; + uint256 words = ((cLen - 33) / 32) + (((cLen - 33) % 32) == 0 ? 0 : 1); + bytes memory maskedDb = new bytes(cLen - 33); - // Store the remaining bytes into the maskedDb - for { let i := 0 } lt(i, words) { i := add(i, 1) } { - mstore( - add(add(maskedDb, 0x20), mul(i, 0x20)), - mload(add(add(encoded, 0x41), mul(i, 0x20))) - ) + assembly { + // Load a word from the encoded string starting at the 2nd byte (also have to account for length stored in first slot) + maskedSeed := mload(add(encoded, 0x21)) + + // Store the remaining bytes into the maskedDb + for { let i := 0 } lt(i, words) { i := add(i, 1) } { + mstore( + add(add(maskedDb, 0x20), mul(i, 0x20)), + mload(add(add(encoded, 0x41), mul(i, 0x20))) + ) + } } - } - // 3. c. Calculate seed mask - // 3. d. Calculate seed - { - bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); - seed = maskedSeed ^ seedMask; - } + // 3. c. Calculate seed mask + // 3. d. Calculate seed + { + bytes32 seedMask = bytes32(_mgf(maskedDb, 32)); + seed = maskedSeed ^ seedMask; + } - // 3. e. Calculate DB mask - bytes memory dbMask = _mgf(abi.encodePacked(seed), cLen - 33); + // 3. e. Calculate DB mask + bytes memory dbMask = _mgf(abi.encodePacked(seed), cLen - 33); - // 3. f. Calculate DB - bytes memory db = _xor(maskedDb, dbMask); - uint256 dbWords = db.length / 32 + db.length % 32 == 0 ? 0 : 1; + // 3. f. Calculate DB + db = _xor(maskedDb, dbMask); + } // 3. g. Separate DB into an octet string lHash' of length hLen, a // (possibly empty) padding string PS consisting of octets @@ -99,57 +101,37 @@ library RSAOAEP { // Y is nonzero, output "decryption error" and stop. bytes32 recoveredHash = bytes32(db); bytes1 one; - assembly { - // Iterate over bytes after the label hash until hitting a non-zero byte - // Skip the first word since it is the recovered hash - // Identify the start index of the message within the db byte string - let m := 0 - for { let w := 1 } lt(w, dbWords) { w := add(w, 1) } { - let word := mload(add(db, add(0x20, mul(w, 0x20)))) - // Iterate over bytes in the word - for { let i := 0 } lt(i, 0x20) { i := 0x20 } { - switch byte(i, word) - case 0x00 { continue } - case 0x01 { - one := 0x01 - m := add(add(i, 1), mul(sub(w, 1), 0x20)) - break - } - default { - // Non-zero entry found before 0x01, revert - let p := mload(0x40) - mstore(p, "decryption error") - revert(p, 0x10) - } - } - - // If the 0x01 byte has been found, exit the outer loop - switch one - case 0x01 { break } + uint256 m; + + // Iterate over bytes after the label hash until hitting a non-zero byte + // Skip the first word since it is the recovered hash + // Identify the start index of the message within the db byte string + for (uint256 i = 32; i < db.length; i++) { + if (db[i] == 0x00) { + // Padding, continue + continue; + } else if (db[i] == 0x01) { + // We found the 0x01 byte, set the one flag and store the index of the next byte + one = 0x01; + m = i + 1; + break; + } else { + // Non-zero entry found before 0x01, revert + revert("decryption error"); } + } - // Check that m is not zero, otherwise revert - switch m - case 0x00 { - let p := mload(0x40) - mstore(p, "decryption error") - revert(p, 0x10) - } + // Check that m was found, otherwise revert + if (m == 0) revert("decryption error"); - // Copy the message from the db bytes string - let len := sub(mload(db), m) - let wrds := div(len, 0x20) - switch mod(len, 0x20) - case 0x00 {} - default { wrds := add(wrds, 1) } - for { let w := 0 } lt(w, wrds) { w := add(w, 1) } { - let c := mload(add(db, add(m, mul(w, 0x20)))) - let i := add(message, mul(w, 0x20)) - mstore(i, c) - } + // Copy the message from the db bytes string + uint256 len = db.length - m; + message = new bytes(len); + for (uint256 i; i < len; i++) { + message[i] = db[m + i]; } - if (one != 0x01 || lhash != recoveredHash || y != 0x00) revert("decryption error"); + if (one != bytes1(0x01) || lhash != recoveredHash || y != bytes1(0x00)) revert("final"); // 4. Return the message and seed used for encryption } @@ -185,7 +167,7 @@ library RSAOAEP { // 2. d. Generate random byte string the same length as the hash function bytes32 rand = sha256(abi.encodePacked(seed)); - // 2. e. Let dbMask = MGF(seed, k - hLen - 1). + // 2. e. Let dbMask = MGF(seed, nLen - hLen - 1). bytes memory dbMask = _mgf(abi.encodePacked(rand), nLen - 33); // 2. f. Let maskedDB = DB \xor dbMask. @@ -221,7 +203,7 @@ library RSAOAEP { return modexp(encoded, e, n); } - function _mgf(bytes memory seed, uint256 maskLen) internal pure returns (bytes memory) { + function _mgf(bytes memory seed, uint256 maskLen) public pure returns (bytes memory) { // Implements 8.2.1 MGF1 as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 // 1. Check that the mask length is not greater than 2^32 * hash length (32 bytes in this case) @@ -240,8 +222,8 @@ library RSAOAEP { // string T: // T = T || Hash(mgfSeed || C) . - uint256 count = maskLen / 32 + (maskLen % 32 == 0 ? 0 : 1); - for (uint256 c; c < count; c++) { + uint32 count = uint32((maskLen / 32) + ((maskLen % 32) == 0 ? 0 : 1)); + for (uint32 c; c < count; c++) { bytes32 h = sha256(abi.encodePacked(seed, c)); assembly { let p := add(add(t, 0x20), mul(c, 0x20)) @@ -253,19 +235,19 @@ library RSAOAEP { return t; } - function _xor(bytes memory first, bytes memory second) internal pure returns (bytes memory) { + function _xor(bytes memory first, bytes memory second) public pure returns (bytes memory) { uint256 fLen = first.length; uint256 sLen = second.length; if (fLen != sLen) revert("xor: different lengths"); - uint256 words = (fLen / 32) + (fLen % 32 == 0 ? 0 : 1); + uint256 words = (fLen / 32) + ((fLen % 32) == 0 ? 0 : 1); bytes memory result = new bytes(fLen); // Iterate through words in the byte strings and xor them one at a time, storing the result assembly { for { let i := 0 } lt(i, words) { i := add(i, 1) } { - let f := mload(add(first, mul(i, 0x20))) - let s := mload(add(second, mul(i, 0x20))) + let f := mload(add(add(first, 0x20), mul(i, 0x20))) + let s := mload(add(add(second, 0x20), mul(i, 0x20))) mstore(add(add(result, 0x20), mul(i, 0x20)), xor(f, s)) } } From b0306f0187c6cacde2e3fb4c78b6554961293e99 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 15:54:26 -0600 Subject: [PATCH 073/117] test: initial roundtrip test --- test/lib/RSA/encrypt.t.sol | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 test/lib/RSA/encrypt.t.sol diff --git a/test/lib/RSA/encrypt.t.sol b/test/lib/RSA/encrypt.t.sol new file mode 100644 index 00000000..ce94a0d1 --- /dev/null +++ b/test/lib/RSA/encrypt.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +// RSA +import {RSAOAEP} from "src/lib/RSA.sol"; + +contract RSAOAEPTest is Test { + + bytes internal constant E = abi.encodePacked(uint32(65537)); + + function setUp() external{} + + function test_roundTrip(uint256 seed_) external { + uint256 value = 5 * 10 ** 18; + bytes memory message = abi.encodePacked(value); + bytes memory label = abi.encodePacked(uint96(1)); + + bytes memory n = abi.encodePacked( + bytes32(0xB925394F570C7C765F121826DFC8A1661921923B33408EFF62DCAC0D263952FE), + bytes32(0x158C12B2B35525F7568CB8DC7731FBC3739F22D94CB80C5622E788DB4532BD8C), + bytes32(0x8643680DA8C00A5E7C967D9D087AA1380AE9A031AC292C971EC75F9BD3296AE1), + bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) + ); + + bytes memory d = abi.encodePacked( + bytes32(0x931e0d080a77957ec9d4aaf458e627b9e54653d84ec581db55475c3fa69bee62), + bytes32(0x8fe49a06fd912f75f6842370ac163fa3f3800444ff3d503031d4215f7b00f2b4), + bytes32(0x183c2b8191ee422acad2b6b29c26d8ba6b2dba73fe839fc4a1b180f5aa7e3723), + bytes32(0x0376dbb6b9571938f796fdcc3b4687f791a1d14c1a578890cdb2f4413a413ba1) + ); + + bytes memory encrypted = RSAOAEP.encrypt(message, label, E, n, seed_); + + (bytes memory decrypted, bytes32 returnedSeed) = RSAOAEP.decrypt(encrypted, d, n, label); + + uint256 returnedValue = abi.decode(decrypted, (uint256)); + + assertEq(returnedValue, value); + assertEq(returnedSeed, sha256(abi.encodePacked(seed_))); + } +} \ No newline at end of file From e53ed4e5b7c1cabe5571d7185178afdc92ad8bb7 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 15:59:22 -0600 Subject: [PATCH 074/117] fix: minor simplification and linter --- src/lib/RSA.sol | 27 +++++++++++++-------------- test/lib/RSA/encrypt.t.sol | 11 +++++------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index bc8efbdc..a260e5fb 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -56,24 +56,23 @@ library RSAOAEP { // 3. b. Separate encoded message into Y (1 byte) | maskedSeed (32 bytes) | maskedDB (cLen - 32 - 1) bytes1 y = bytes1(encoded); - + bytes memory db; - { // Scope these local variables to avoid stack too deep later + { + // Scope these local variables to avoid stack too deep later bytes32 maskedSeed; - uint256 words = ((cLen - 33) / 32) + (((cLen - 33) % 32) == 0 ? 0 : 1); - bytes memory maskedDb = new bytes(cLen - 33); + // uint256 words = ((cLen - 33) / 32) + (((cLen - 33) % 32) == 0 ? 0 : 1); + uint256 maskLen = cLen - 33; + bytes memory maskedDb = new bytes(maskLen); + // Load a word from the encoded string starting at the 2nd byte (also have to account for length stored in first slot) assembly { - // Load a word from the encoded string starting at the 2nd byte (also have to account for length stored in first slot) maskedSeed := mload(add(encoded, 0x21)) + } - // Store the remaining bytes into the maskedDb - for { let i := 0 } lt(i, words) { i := add(i, 1) } { - mstore( - add(add(maskedDb, 0x20), mul(i, 0x20)), - mload(add(add(encoded, 0x41), mul(i, 0x20))) - ) - } + // Store the remaining bytes into the maskedDb + for (uint256 i; i < maskLen; i++) { + maskedDb[i] = encoded[i + 33]; } // 3. c. Calculate seed mask @@ -102,12 +101,12 @@ library RSAOAEP { bytes32 recoveredHash = bytes32(db); bytes1 one; uint256 m; - + // Iterate over bytes after the label hash until hitting a non-zero byte // Skip the first word since it is the recovered hash // Identify the start index of the message within the db byte string for (uint256 i = 32; i < db.length; i++) { - if (db[i] == 0x00) { + if (db[i] == 0x00) { // Padding, continue continue; } else if (db[i] == 0x01) { diff --git a/test/lib/RSA/encrypt.t.sol b/test/lib/RSA/encrypt.t.sol index ce94a0d1..2b412225 100644 --- a/test/lib/RSA/encrypt.t.sol +++ b/test/lib/RSA/encrypt.t.sol @@ -9,10 +9,9 @@ import {console2} from "forge-std/console2.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; contract RSAOAEPTest is Test { + bytes internal constant E = abi.encodePacked(uint32(65_537)); - bytes internal constant E = abi.encodePacked(uint32(65537)); - - function setUp() external{} + function setUp() external {} function test_roundTrip(uint256 seed_) external { uint256 value = 5 * 10 ** 18; @@ -32,7 +31,7 @@ contract RSAOAEPTest is Test { bytes32(0x183c2b8191ee422acad2b6b29c26d8ba6b2dba73fe839fc4a1b180f5aa7e3723), bytes32(0x0376dbb6b9571938f796fdcc3b4687f791a1d14c1a578890cdb2f4413a413ba1) ); - + bytes memory encrypted = RSAOAEP.encrypt(message, label, E, n, seed_); (bytes memory decrypted, bytes32 returnedSeed) = RSAOAEP.decrypt(encrypted, d, n, label); @@ -41,5 +40,5 @@ contract RSAOAEPTest is Test { assertEq(returnedValue, value); assertEq(returnedSeed, sha256(abi.encodePacked(seed_))); - } -} \ No newline at end of file + } +} From 50f7499f6dac27597e24785f94be327697573e9a Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 16:34:51 -0600 Subject: [PATCH 075/117] fix: sorted function ref and sort ordering --- src/modules/auctions/LSBBA/LSBBA.sol | 4 ++-- src/modules/auctions/LSBBA/MinPriorityQueue.sol | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 13f7726d..7ca68d2c 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -504,9 +504,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { function getSortedBidData( uint96 lotId_, - uint256 bidId_ + uint96 index_ ) public view returns (QueueBid memory) { - return lotSortedBids[lotId_].getBid(bidId_); + return lotSortedBids[lotId_].getBid(index_); } function getSortedBidCount(uint96 lotId_) public view returns (uint256) { diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index 761a118e..fe4acd35 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -49,6 +49,7 @@ library MinPriorityQueue { function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { require(!isEmpty(self), "nothing to return"); require(index <= self.numBids, "bid does not exist"); + require(index > 0, "cannot use 0 index"); return self.bidIdToBidMap[self.bidIdList[index]]; } @@ -114,9 +115,9 @@ library MinPriorityQueue { uint256 relI = bidI.amountIn * bidJ.minAmountOut; uint256 relJ = bidJ.amountIn * bidI.minAmountOut; if (relI == relJ) { - return iId < jId; + return iId > jId; } - return relI > relJ; + return relI < relJ; } ///@notice helper function to exchange to bids in the heap From 71fdab1a294e7cc5539bab86cd80309658ecdffe Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 16:35:02 -0600 Subject: [PATCH 076/117] test: fix decrypt and sort tests --- test/lib/RSA/encrypt.t.sol | 2 +- .../auctions/LSBBA/decryptAndSortBids.t.sol | 61 ++++++++++--------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/test/lib/RSA/encrypt.t.sol b/test/lib/RSA/encrypt.t.sol index 2b412225..62ea6194 100644 --- a/test/lib/RSA/encrypt.t.sol +++ b/test/lib/RSA/encrypt.t.sol @@ -9,7 +9,7 @@ import {console2} from "forge-std/console2.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; contract RSAOAEPTest is Test { - bytes internal constant E = abi.encodePacked(uint32(65_537)); + bytes internal constant E = abi.encodePacked(uint24(65_537)); function setUp() external {} diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 648fa63d..b61cbf14 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -14,7 +14,7 @@ import {Auction} from "src/modules/Auction.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; -contract LSBBACancelBidTest is Test, Permit2User { +contract LSBBADecryptAndSortBidsTest is Test, Permit2User { address internal constant _PROTOCOL = address(0x1); address internal alice = address(0x2); address internal constant recipient = address(0x3); @@ -31,7 +31,12 @@ contract LSBBACancelBidTest is Test, Permit2User { uint96 internal lotId = 1; bytes internal auctionData; - bytes internal constant PUBLIC_KEY_MODULUS = new bytes(128); + bytes internal constant PUBLIC_KEY_MODULUS = abi.encodePacked( + bytes32(0xB925394F570C7C765F121826DFC8A1661921923B33408EFF62DCAC0D263952FE), + bytes32(0x158C12B2B35525F7568CB8DC7731FBC3739F22D94CB80C5622E788DB4532BD8C), + bytes32(0x8643680DA8C00A5E7C967D9D087AA1380AE9A031AC292C971EC75F9BD3296AE1), + bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) + ); uint256 internal bidSeed = 1e9; uint256 internal bidOne; @@ -131,7 +136,7 @@ contract LSBBACancelBidTest is Test, Permit2User { uint256 len = decrypts.length; // Remove all elements for (uint256 i = 0; i < len; i++) { - delete decrypts[i]; + decrypts.pop(); } } @@ -402,7 +407,7 @@ contract LSBBACancelBidTest is Test, Permit2User { // Check values on auction data LocalSealedBidBatchAuction.AuctionData memory lotData = auctionModule.getLotData(lotId); assertEq(lotData.nextDecryptIndex, 1); - assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Decrypted)); + assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Created)); // Check encrypted bids LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = @@ -420,8 +425,8 @@ contract LSBBACancelBidTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); - assertEq(sortedBidOne.bidId, 0); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidOne.bidId, 1); assertEq(sortedBidOne.encId, bidOne); assertEq(sortedBidOne.amountIn, bidOneAmount); assertEq(sortedBidOne.minAmountOut, bidOneAmountOut); @@ -466,23 +471,23 @@ contract LSBBACancelBidTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); - assertEq(sortedBidOne.bidId, 0); - assertEq(sortedBidOne.encId, bidThree); - assertEq(sortedBidOne.amountIn, bidThreeAmount); - assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidOne.bidId, 2); + assertEq(sortedBidOne.encId, bidTwo); + assertEq(sortedBidOne.amountIn, bidTwoAmount); + assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); assertEq(sortedBidTwo.bidId, 1); assertEq(sortedBidTwo.encId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); - QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidThree.bidId, 2); - assertEq(sortedBidThree.encId, bidTwo); - assertEq(sortedBidThree.amountIn, bidTwoAmount); - assertEq(sortedBidThree.minAmountOut, bidTwoAmountOut); + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); + assertEq(sortedBidThree.bidId, 3); + assertEq(sortedBidThree.encId, bidThree); + assertEq(sortedBidThree.amountIn, bidThreeAmount); + assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); assertEq(auctionModule.getSortedBidCount(lotId), 3); } @@ -512,23 +517,23 @@ contract LSBBACancelBidTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); - assertEq(sortedBidOne.bidId, 0); - assertEq(sortedBidOne.encId, bidThree); - assertEq(sortedBidOne.amountIn, bidThreeAmount); - assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidOne.bidId, 2); + assertEq(sortedBidOne.encId, bidTwo); + assertEq(sortedBidOne.amountIn, bidTwoAmount); + assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); assertEq(sortedBidTwo.bidId, 1); assertEq(sortedBidTwo.encId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); - QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidThree.bidId, 2); - assertEq(sortedBidThree.encId, bidTwo); - assertEq(sortedBidThree.amountIn, bidTwoAmount); - assertEq(sortedBidThree.minAmountOut, bidTwoAmountOut); + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); + assertEq(sortedBidThree.bidId, 3); + assertEq(sortedBidThree.encId, bidThree); + assertEq(sortedBidThree.amountIn, bidThreeAmount); + assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); assertEq(auctionModule.getSortedBidCount(lotId), 3); } From 7ccb23c1c795d67cb6c28b821f7166a8abfb32db Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 16:35:35 -0600 Subject: [PATCH 077/117] chore: run linter --- src/modules/auctions/LSBBA/LSBBA.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 7ca68d2c..3d8b000b 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -502,10 +502,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { return lotEncryptedBids[lotId_][bidId_]; } - function getSortedBidData( - uint96 lotId_, - uint96 index_ - ) public view returns (QueueBid memory) { + function getSortedBidData(uint96 lotId_, uint96 index_) public view returns (QueueBid memory) { return lotSortedBids[lotId_].getBid(index_); } From 8f109ac498f3068556581915801527162b233290 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:05:28 -0600 Subject: [PATCH 078/117] feat: change encrypted bid storage design --- src/modules/auctions/LSBBA/LSBBA.sol | 62 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 3d8b000b..4488c5df 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -55,10 +55,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { struct AuctionData { AuctionStatus status; uint96 nextDecryptIndex; + uint96 nextBidId; uint256 minimumPrice; uint256 minFilled; // minimum amount of capacity that must be filled to settle the auction uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price bytes publicKeyModulus; + uint96[] bidIds; } /// @notice Struct containing parameters for creating a new LSBBA auction @@ -81,7 +83,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint256 internal constant _SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; - mapping(uint96 lotId => EncryptedBid[] bids) public lotEncryptedBids; + mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; // TODO must create and call `initialize` on it during auction creation // ========== SETUP ========== // @@ -124,16 +126,16 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @inheritdoc AuctionModule /// @dev Checks that the bid is valid - function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view override { + function _revertIfBidInvalid(uint96 lotId_, uint96 bidId_) internal view override { // Bid ID must be less than number of bids for lot - if (bidId_ >= lotEncryptedBids[lotId_].length) revert Auction_InvalidBidId(lotId_, bidId_); + if (bidId_ >= auctionData[lotId_].nextBidId) revert Auction_InvalidBidId(lotId_, bidId_); } /// @inheritdoc AuctionModule /// @dev Checks that the sender is the bidder function _revertIfNotBidOwner( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address bidder_ ) internal view override { // Check that sender is the bidder @@ -142,7 +144,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @inheritdoc AuctionModule /// @dev Checks that the bid is not already cancelled - function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view override { + function _revertIfBidCancelled(uint96 lotId_, uint96 bidId_) internal view override { // Bid must not be cancelled if (lotEncryptedBids[lotId_][bidId_].status == BidStatus.Cancelled) { revert Auction_AlreadyCancelled(); @@ -159,7 +161,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) internal override returns (uint256 bidId) { + ) internal override returns (uint96 bidId) { // Validate inputs // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_AmountLessThanMinimum(); @@ -177,11 +179,11 @@ contract LocalSealedBidBatchAuction is AuctionModule { userBid.encryptedAmountOut = auctionData_; userBid.status = BidStatus.Submitted; - // Bid ID is the next index in the lot's bid array - bidId = lotEncryptedBids[lotId_].length; + // Get next bid ID + bidId = auctionData[lotId_].nextBidId++; - // Add bid to lot - lotEncryptedBids[lotId_].push(userBid); + // Store bid in mapping + lotEncryptedBids[lotId_][bidId] = userBid; return bidId; } @@ -195,14 +197,25 @@ contract LocalSealedBidBatchAuction is AuctionModule { // This way, we can still lookup the bids by bidId for cancellation, etc. function _cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address ) internal override returns (uint256 refundAmount) { - // Validate inputs + // Inputs validated in Auction // Set bid status to cancelled lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; + // Remove bid from list of bids to decrypt + AuctionData storage data = auctionData[lotId_]; + uint256 len = data.bidIds.length; + for (uint256 i; i < len; i++) { + if (data.bidIds[i] == bidId_) { + data.bidIds[i] = data.bidIds[len - 1]; + data.bidIds.pop(); + break; + } + } + // Return the amount to be refunded return lotEncryptedBids[lotId_][bidId_].amount; } @@ -221,7 +234,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint96 len = uint96(decrypts_.length); // Check that the number of decrypts is less than or equal to the number of bids remaining to be decrypted - if (len > lotEncryptedBids[lotId_].length - nextDecryptIndex) { + uint96[] storage bidIds = auctionData[lotId_].bidIds; + if (len > bidIds.length - nextDecryptIndex) { revert Auction_InvalidDecrypt(); } @@ -231,20 +245,20 @@ contract LocalSealedBidBatchAuction is AuctionModule { bytes memory ciphertext = _encrypt(lotId_, decrypts_[i]); // Load encrypted bid - EncryptedBid storage encBid = lotEncryptedBids[lotId_][nextDecryptIndex + i]; + uint96 bidId = bidIds[nextDecryptIndex + i]; + EncryptedBid storage encBid = lotEncryptedBids[lotId_][bidId]; // Check that the encrypted bid matches the re-encrypted decrypt by hashing both if (keccak256(ciphertext) != keccak256(encBid.encryptedAmountOut)) { revert Auction_InvalidDecrypt(); } - // If the bid has been cancelled, it shouldn't be added to the queue - // TODO should this just check != Submitted? - if (encBid.status == BidStatus.Cancelled) continue; + // TODO shouldn't need this check but need to confirm + if (encBid.status != BidStatus.Submitted) continue; // Store the decrypt in the sorted bid queue lotSortedBids[lotId_].insert( - nextDecryptIndex + i, encBid.amount, decrypts_[i].amountOut + bidId, encBid.amount, decrypts_[i].amountOut ); // Set bid status to decrypted @@ -255,7 +269,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { auctionData[lotId_].nextDecryptIndex += len; // If all bids have been decrypted, set auction status to decrypted - if (auctionData[lotId_].nextDecryptIndex == lotEncryptedBids[lotId_].length) { + if (auctionData[lotId_].nextDecryptIndex == bidIds.length) { auctionData[lotId_].status = AuctionStatus.Decrypted; } } @@ -274,7 +288,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { } /// @notice View function that can be used to obtain a certain number of the next bids to decrypt off-chain - // TODO This assumes that cancelled bids have been removed, but hasn't been refactored based on the comments over `cancelBid` function getNextBidsToDecrypt( uint96 lotId_, uint256 number_ @@ -283,7 +296,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint96 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; // Load number of bids to decrypt - uint256 len = lotEncryptedBids[lotId_].length - nextDecryptIndex; + uint96[] storage bidIds = auctionData[lotId_].bidIds; + uint256 len = bidIds.length - nextDecryptIndex; if (number_ < len) len = number_; // Create array of encrypted bids @@ -291,7 +305,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Iterate over bids and add them to the array for (uint256 i; i < len; i++) { - bids[i] = lotEncryptedBids[lotId_][nextDecryptIndex + i]; + bids[i] = lotEncryptedBids[lotId_][bidIds[nextDecryptIndex + i]]; } // Return array of encrypted bids @@ -400,7 +414,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint256 amountOut = (qBid.amountIn * _SCALE) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out - EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.encId]; + EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.bidId]; Bid memory winningBid; winningBid.bidder = encBid.bidder; winningBid.recipient = encBid.recipient; @@ -498,7 +512,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { return auctionData[lotId_]; } - function getBidData(uint96 lotId_, uint256 bidId_) public view returns (EncryptedBid memory) { + function getBidData(uint96 lotId_, uint96 bidId_) public view returns (EncryptedBid memory) { return lotEncryptedBids[lotId_][bidId_]; } From d1671738e60fb517379ed42f89ae8fee4d8fbe8a Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:05:44 -0600 Subject: [PATCH 079/117] chore: change lot and bid ids to be uint96 --- src/AuctionHouse.sol | 18 ++++---- src/interfaces/IHooks.sol | 6 +-- src/modules/Auction.sol | 20 ++++---- src/modules/Derivative.sol | 2 +- src/modules/auctions/GDA.sol | 2 +- .../auctions/LSBBA/MinPriorityQueue.sol | 44 +++++++++--------- src/modules/auctions/TVGDA.sol | 8 ++-- src/modules/auctions/bases/BatchAuction.sol | 4 +- .../auctions/bases/DiscreteAuction.sol | 2 +- test/AuctionHouse/MockAuctionHouse.sol | 6 +-- test/AuctionHouse/bid.t.sol | 6 +-- test/AuctionHouse/cancelBid.t.sol | 2 +- test/AuctionHouse/collectPayment.t.sol | 2 +- test/AuctionHouse/collectPayout.t.sol | 2 +- test/AuctionHouse/sendPayment.t.sol | 2 +- test/AuctionHouse/sendPayout.t.sol | 2 +- .../Auction/MockAtomicAuctionModule.sol | 12 ++--- test/modules/Auction/MockAuctionModule.sol | 10 ++-- .../Auction/MockBatchAuctionModule.sol | 32 +++++++++---- test/modules/Auction/MockHook.sol | 6 +-- test/modules/Auction/auction.t.sol | 8 ++-- test/modules/auctions/LSBBA/bid.t.sol | 2 +- test/modules/auctions/LSBBA/cancelBid.t.sol | 2 +- .../auctions/LSBBA/decryptAndSortBids.t.sol | 46 +++++++++---------- 24 files changed, 129 insertions(+), 117 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index ca9075ad..eb569e75 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -129,7 +129,7 @@ abstract contract Router is FeeManager { /// /// @param params_ Bid parameters /// @return bidId Bid ID - function bid(BidParams memory params_) external virtual returns (uint256 bidId); + function bid(BidParams memory params_) external virtual returns (uint96 bidId); /// @notice Cancel a bid on a lot in a batch auction /// @dev The implementing function must perform the following: @@ -139,7 +139,7 @@ abstract contract Router is FeeManager { /// /// @param lotId_ Lot ID /// @param bidId_ Bid ID - function cancelBid(uint96 lotId_, uint256 bidId_) external virtual; + function cancelBid(uint96 lotId_, uint96 bidId_) external virtual; /// @notice Settle a batch auction /// @notice This function is used for versions with on-chain storage and bids and local settlement @@ -261,7 +261,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @return bool True if caller is allowed to purchase/bid on the lot function _isAllowed( IAllowlist allowlist_, - uint256 lotId_, + uint96 lotId_, address caller_, bytes memory allowlistProof_ ) internal view returns (bool) { @@ -356,7 +356,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { external override isLotValid(params_.lotId) - returns (uint256) + returns (uint96) { // Load routing data for the lot Routing memory routing = lotRouting[params_.lotId]; @@ -368,7 +368,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Record the bid on the auction module // The module will determine if the bid is valid - minimum bid size, minimum price, auction status, etc - uint256 bidId; + uint96 bidId; { AuctionModule module = _getModuleForId(params_.lotId); bidId = module.bid( @@ -398,7 +398,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @dev This function reverts if: /// - the lot ID is invalid /// - the auction module reverts when cancelling the bid - function cancelBid(uint96 lotId_, uint256 bidId_) external override isLotValid(lotId_) { + function cancelBid(uint96 lotId_, uint96 bidId_) external override isLotValid(lotId_) { // Cancel the bid on the auction module // The auction module is responsible for validating the bid and authorizing the caller AuctionModule module = _getModuleForId(lotId_); @@ -505,7 +505,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param hooks_ Hooks contract to call (optional) /// @param permit2Approval_ Permit2 approval data (optional) function _collectPayment( - uint256 lotId_, + uint96 lotId_, uint256 amount_, ERC20 quoteToken_, IHooks hooks_, @@ -574,7 +574,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param payoutAmount_ Amount of payoutToken to collect (in native decimals) /// @param routingParams_ Routing parameters for the lot function _collectPayout( - uint256 lotId_, + uint96 lotId_, uint256 paymentAmount_, uint256 payoutAmount_, Routing memory routingParams_ @@ -634,7 +634,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// @param routingParams_ Routing parameters for the lot /// @param auctionOutput_ Custom data returned by the auction module function _sendPayout( - uint256 lotId_, + uint96 lotId_, address recipient_, uint256 payoutAmount_, Routing memory routingParams_, diff --git a/src/interfaces/IHooks.sol b/src/interfaces/IHooks.sol index ab55b1bf..369a25cc 100644 --- a/src/interfaces/IHooks.sol +++ b/src/interfaces/IHooks.sol @@ -13,13 +13,13 @@ interface IHooks { /// @notice Called before payment and payout /// TODO define expected state, invariants - function pre(uint256 lotId_, uint256 amount_) external; + function pre(uint96 lotId_, uint256 amount_) external; /// @notice Called after payment and before payout /// TODO define expected state, invariants - function mid(uint256 lotId_, uint256 amount_, uint256 payout_) external; + function mid(uint96 lotId_, uint256 amount_, uint256 payout_) external; /// @notice Called after payment and after payout /// TODO define expected state, invariants - function post(uint256 lotId_, uint256 payout_) external; + function post(uint96 lotId_, uint256 payout_) external; } diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index d3ab0732..0cb353bd 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -16,7 +16,7 @@ abstract contract Auction { error Auction_InvalidLotId(uint96 lotId); - error Auction_InvalidBidId(uint96 lotId, uint256 bidId); + error Auction_InvalidBidId(uint96 lotId, uint96 bidId); error Auction_OnlyMarketOwner(); error Auction_AmountLessThanMinimum(); @@ -113,7 +113,7 @@ abstract contract Auction { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external virtual returns (uint256 bidId); + ) external virtual returns (uint96 bidId); /// @notice Cancel a bid /// @dev The implementing function should handle the following: @@ -127,7 +127,7 @@ abstract contract Auction { /// @return bidAmount The amount of quote tokens to refund function cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address bidder_ ) external virtual returns (uint256 bidAmount); @@ -343,7 +343,7 @@ abstract contract AuctionModule is Auction, Module { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) external override onlyInternal returns (uint256 bidId) { + ) external override onlyInternal returns (uint96 bidId) { // Standard validation _revertIfLotInvalid(lotId_); _revertIfBeforeLotStart(lotId_); @@ -373,7 +373,7 @@ abstract contract AuctionModule is Auction, Module { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) internal virtual returns (uint256 bidId); + ) internal virtual returns (uint96 bidId); /// @inheritdoc Auction /// @dev Implements a basic cancelBid function that: @@ -394,7 +394,7 @@ abstract contract AuctionModule is Auction, Module { /// - Updating the bid data function cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) external override onlyInternal returns (uint256 bidAmount) { // Standard validation @@ -419,7 +419,7 @@ abstract contract AuctionModule is Auction, Module { /// @return bidAmount The amount of quote tokens to refund function _cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) internal virtual returns (uint256 bidAmount); @@ -551,7 +551,7 @@ abstract contract AuctionModule is Auction, Module { /// /// @param lotId_ The lot ID /// @param bidId_ The bid ID - function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual; + function _revertIfBidInvalid(uint96 lotId_, uint96 bidId_) internal view virtual; /// @notice Checks that `caller_` is the bid owner /// @dev Should revert if `caller_` is not the bid owner @@ -562,7 +562,7 @@ abstract contract AuctionModule is Auction, Module { /// @param caller_ The caller function _revertIfNotBidOwner( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) internal view virtual; @@ -572,5 +572,5 @@ abstract contract AuctionModule is Auction, Module { /// /// @param lotId_ The lot ID /// @param bidId_ The bid ID - function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual; + function _revertIfBidCancelled(uint96 lotId_, uint96 bidId_) internal view virtual; } diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index 652cbf14..68ebc88c 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -21,7 +21,7 @@ abstract contract Derivative { // ========== STATE VARIABLES ========== // mapping(Keycode dType => address) public wrappedImplementations; mapping(uint256 tokenId => Token metadata) public tokenMetadata; - mapping(uint256 lotId => uint256[] tokenIds) public lotDerivatives; + mapping(uint96 lotId => uint256[] tokenIds) public lotDerivatives; // ========== DERIVATIVE MANAGEMENT ========== // diff --git a/src/modules/auctions/GDA.sol b/src/modules/auctions/GDA.sol index d93a4e51..a17d910d 100644 --- a/src/modules/auctions/GDA.sol +++ b/src/modules/auctions/GDA.sol @@ -26,7 +26,7 @@ pragma solidity 0.8.19; // /* ========== STATE ========== */ // SD59x18 public constant ONE = SD59x18.wrap(1e18); -// mapping(uint256 lotId => AuctionData) public auctionData; +// mapping(uint96 lotId => AuctionData) public auctionData; // } // contract GradualDutchAuctioneer is AtomicAuctionModule, GDA { diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index fe4acd35..c798ce4c 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; struct Bid { - uint96 bidId; // ID in queue - uint96 encId; // ID of encrypted bid to reference on settlement + uint96 queueId; // ID in queue + uint96 bidId; // ID of encrypted bid to reference on settlement uint256 amountIn; uint256 minAmountOut; } @@ -17,16 +17,16 @@ library MinPriorityQueue { ///@notice incrementing bid id uint96 nextBidId; ///@notice array backing priority queue - uint96[] bidIdList; + uint96[] queueIdList; ///@notice total number of bids in queue uint96 numBids; //@notice map bid ids to bids - mapping(uint96 => Bid) bidIdToBidMap; + mapping(uint96 => Bid) queueIdToBidMap; } ///@notice initialize must be called before using queue. function initialize(Queue storage self) public { - self.bidIdList.push(0); + self.queueIdList.push(0); self.nextBidId = 1; } @@ -41,8 +41,8 @@ library MinPriorityQueue { ///@notice view min bid function getMin(Queue storage self) public view returns (Bid storage) { require(!isEmpty(self), "nothing to return"); - uint96 minId = self.bidIdList[1]; - return self.bidIdToBidMap[minId]; + uint96 minId = self.queueIdList[1]; + return self.queueIdToBidMap[minId]; } ///@notice view bid by index @@ -50,7 +50,7 @@ library MinPriorityQueue { require(!isEmpty(self), "nothing to return"); require(index <= self.numBids, "bid does not exist"); require(index > 0, "cannot use 0 index"); - return self.bidIdToBidMap[self.bidIdList[index]]; + return self.queueIdToBidMap[self.queueIdList[index]]; } ///@notice move bid up heap @@ -79,17 +79,17 @@ library MinPriorityQueue { ///@notice insert bid in heap function insert( Queue storage self, - uint96 encId, + uint96 bidId, uint256 amountIn, uint256 minAmountOut ) public { - insert(self, Bid(self.nextBidId++, encId, amountIn, minAmountOut)); + insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); } ///@notice insert bid in heap function insert(Queue storage self, Bid memory bid) private { - self.bidIdList.push(bid.bidId); - self.bidIdToBidMap[bid.bidId] = bid; + self.queueIdList.push(bid.queueId); + self.queueIdToBidMap[bid.queueId] = bid; self.numBids += 1; swim(self, self.numBids); } @@ -97,10 +97,10 @@ library MinPriorityQueue { ///@notice delete min bid from heap and return function delMin(Queue storage self) public returns (Bid memory) { require(!isEmpty(self), "nothing to delete"); - Bid memory min = self.bidIdToBidMap[self.bidIdList[1]]; + Bid memory min = self.queueIdToBidMap[self.queueIdList[1]]; exchange(self, 1, self.numBids--); - self.bidIdList.pop(); - delete self.bidIdToBidMap[min.bidId]; + self.queueIdList.pop(); + delete self.queueIdToBidMap[min.queueId]; sink(self, 1); return min; } @@ -108,10 +108,10 @@ library MinPriorityQueue { ///@notice helper function to determine ordering. When two bids have the same price, give priority ///to the lower bid ID (inserted earlier) function isGreater(Queue storage self, uint256 i, uint256 j) private view returns (bool) { - uint96 iId = self.bidIdList[i]; - uint96 jId = self.bidIdList[j]; - Bid memory bidI = self.bidIdToBidMap[iId]; - Bid memory bidJ = self.bidIdToBidMap[jId]; + uint96 iId = self.queueIdList[i]; + uint96 jId = self.queueIdList[j]; + Bid memory bidI = self.queueIdToBidMap[iId]; + Bid memory bidJ = self.queueIdToBidMap[jId]; uint256 relI = bidI.amountIn * bidJ.minAmountOut; uint256 relJ = bidJ.amountIn * bidI.minAmountOut; if (relI == relJ) { @@ -122,8 +122,8 @@ library MinPriorityQueue { ///@notice helper function to exchange to bids in the heap function exchange(Queue storage self, uint256 i, uint256 j) private { - uint96 tempId = self.bidIdList[i]; - self.bidIdList[i] = self.bidIdList[j]; - self.bidIdList[j] = tempId; + uint96 tempId = self.queueIdList[i]; + self.queueIdList[i] = self.queueIdList[j]; + self.queueIdList[j] = tempId; } } diff --git a/src/modules/auctions/TVGDA.sol b/src/modules/auctions/TVGDA.sol index 19e68d6e..db82491d 100644 --- a/src/modules/auctions/TVGDA.sol +++ b/src/modules/auctions/TVGDA.sol @@ -38,7 +38,7 @@ pragma solidity 0.8.19; // /* ========== AUCTION FUNCTIONS ========== */ // function _auction( -// uint256 lotId_, +// uint96 lotId_, // Lot memory lot_, // bytes memory params_ // ) internal override { @@ -77,7 +77,7 @@ pragma solidity 0.8.19; // auction.emissionsRate = emissionsRate; // } -// function _purchase(uint256 lotId_, uint256 amount_, bytes memory variableInput_) internal override returns (uint256) { +// function _purchase(uint96 lotId_, uint256 amount_, bytes memory variableInput_) internal override returns (uint256) { // // variableInput should be a single uint256 // uint256 variableInput = abi.decode(variableInput_, (uint256)); @@ -92,7 +92,7 @@ pragma solidity 0.8.19; // return payout; // } -// function _payoutAndEmissionsFor(uint256 lotId_, uint256 amount_, uint256 variableInput_) internal view override returns (uint256) { +// function _payoutAndEmissionsFor(uint96 lotId_, uint256 amount_, uint256 variableInput_) internal view override returns (uint256) { // // Load decay types for lot // priceDecayType = auctionData[lotId_].priceDecayType; // variableDecayType = auctionData[lotId_].variableDecayType; @@ -111,7 +111,7 @@ pragma solidity 0.8.19; // // TODO problem with having a minimum price -> messes up the math and the inverse solution is not closed form // function _payoutForExpExp( -// uint256 lotId_, +// uint96 lotId_, // uint256 amount_, // uint256 variableInput_ // ) internal view returns (uint256, uint48) { diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 283a54e1..395f849c 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -36,7 +36,7 @@ abstract contract BatchAuction { abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // // ========== STATE VARIABLES ========== // -// mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; +// mapping(uint96 lotId => Auction.Bid[] bids) public lotBids; // /// @inheritdoc AuctionModule // function _bid( @@ -46,7 +46,7 @@ abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // address referrer_, // uint256 amount_, // bytes calldata auctionData_ -// ) internal override returns (uint256 bidId) { +// ) internal override returns (uint96 bidId) { // // TODO // // Validate inputs diff --git a/src/modules/auctions/bases/DiscreteAuction.sol b/src/modules/auctions/bases/DiscreteAuction.sol index 47118532..afdb2001 100644 --- a/src/modules/auctions/bases/DiscreteAuction.sol +++ b/src/modules/auctions/bases/DiscreteAuction.sol @@ -19,7 +19,7 @@ pragma solidity 0.8.19; // /// @notice Minimum deposit interval for a discrete auction // uint48 public minDepositInterval; -// mapping(uint256 lotId => StyleData style) public styleData; +// mapping(uint96 lotId => StyleData style) public styleData; // /* ========== ADMIN FUNCTIONS ========== */ diff --git a/test/AuctionHouse/MockAuctionHouse.sol b/test/AuctionHouse/MockAuctionHouse.sol index decfe966..f24c944b 100644 --- a/test/AuctionHouse/MockAuctionHouse.sol +++ b/test/AuctionHouse/MockAuctionHouse.sol @@ -15,7 +15,7 @@ contract MockAuctionHouse is AuctionHouse { // Expose the _collectPayment function for testing function collectPayment( - uint256 lotId_, + uint96 lotId_, uint256 amount_, ERC20 quoteToken_, IHooks hooks_, @@ -42,7 +42,7 @@ contract MockAuctionHouse is AuctionHouse { } function collectPayout( - uint256 lotId_, + uint96 lotId_, uint256 paymentAmount_, uint256 payoutAmount_, Auctioneer.Routing memory routingParams_ @@ -51,7 +51,7 @@ contract MockAuctionHouse is AuctionHouse { } function sendPayout( - uint256 lotId_, + uint96 lotId_, address recipient_, uint256 payoutAmount_, Auctioneer.Routing memory routingParams_, diff --git a/test/AuctionHouse/bid.t.sol b/test/AuctionHouse/bid.t.sol index 223b2d78..ee39f557 100644 --- a/test/AuctionHouse/bid.t.sol +++ b/test/AuctionHouse/bid.t.sol @@ -305,7 +305,7 @@ contract BidTest is Test, Permit2User { { // Call the function vm.prank(alice); - uint256 bidId = auctionHouse.bid(bidParams); + uint96 bidId = auctionHouse.bid(bidParams); // Check the balances assertEq(quoteToken.balanceOf(alice), 0, "alice: quote token balance mismatch"); @@ -333,7 +333,7 @@ contract BidTest is Test, Permit2User { { // Call the function vm.prank(alice); - uint256 bidId = auctionHouse.bid(bidParams); + uint96 bidId = auctionHouse.bid(bidParams); // Check the balances assertEq(quoteToken.balanceOf(alice), 0, "alice: quote token balance mismatch"); @@ -366,7 +366,7 @@ contract BidTest is Test, Permit2User { // Call the function vm.prank(alice); - uint256 bidId = auctionHouse.bid(bidParams); + uint96 bidId = auctionHouse.bid(bidParams); // Check the bid Auction.Bid memory bid = mockAuctionModule.getBid(lotId, bidId); diff --git a/test/AuctionHouse/cancelBid.t.sol b/test/AuctionHouse/cancelBid.t.sol index a8a47045..6cd001f1 100644 --- a/test/AuctionHouse/cancelBid.t.sol +++ b/test/AuctionHouse/cancelBid.t.sol @@ -51,7 +51,7 @@ contract CancelBidTest is Test, Permit2User { // Function parameters (can be modified) uint96 internal lotId; - uint256 internal bidId; + uint96 internal bidId; function setUp() external { // Set block timestamp diff --git a/test/AuctionHouse/collectPayment.t.sol b/test/AuctionHouse/collectPayment.t.sol index c1f30322..8e359b94 100644 --- a/test/AuctionHouse/collectPayment.t.sol +++ b/test/AuctionHouse/collectPayment.t.sol @@ -23,7 +23,7 @@ contract CollectPaymentTest is Test, Permit2User { address internal USER; // Function parameters - uint256 internal lotId = 1; + uint96 internal lotId = 1; uint256 internal amount = 10e18; MockFeeOnTransferERC20 internal quoteToken; MockHook internal hook; diff --git a/test/AuctionHouse/collectPayout.t.sol b/test/AuctionHouse/collectPayout.t.sol index 24e78404..231de0ee 100644 --- a/test/AuctionHouse/collectPayout.t.sol +++ b/test/AuctionHouse/collectPayout.t.sol @@ -26,7 +26,7 @@ contract CollectPayoutTest is Test, Permit2User { address internal OWNER = address(0x3); // Function parameters - uint256 internal lotId = 1; + uint96 internal lotId = 1; uint256 internal paymentAmount = 1e18; uint256 internal payoutAmount = 10e18; MockFeeOnTransferERC20 internal quoteToken; diff --git a/test/AuctionHouse/sendPayment.t.sol b/test/AuctionHouse/sendPayment.t.sol index 0dee7b3a..e62f4f35 100644 --- a/test/AuctionHouse/sendPayment.t.sol +++ b/test/AuctionHouse/sendPayment.t.sol @@ -20,7 +20,7 @@ contract SendPaymentTest is Test, Permit2User { address internal OWNER = address(0x3); // Function parameters - uint256 internal lotId = 1; + uint96 internal lotId = 1; uint256 internal paymentAmount = 1e18; MockFeeOnTransferERC20 internal quoteToken; MockHook internal hook; diff --git a/test/AuctionHouse/sendPayout.t.sol b/test/AuctionHouse/sendPayout.t.sol index 1b4ceb6b..06aabdc9 100644 --- a/test/AuctionHouse/sendPayout.t.sol +++ b/test/AuctionHouse/sendPayout.t.sol @@ -36,7 +36,7 @@ contract SendPayoutTest is Test, Permit2User { uint48 internal constant DERIVATIVE_EXPIRY = 1 days; // Function parameters - uint256 internal lotId = 1; + uint96 internal lotId = 1; uint256 internal payoutAmount = 10e18; MockFeeOnTransferERC20 internal quoteToken; MockFeeOnTransferERC20 internal payoutToken; diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index c9d9d770..af7c97ec 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -16,7 +16,7 @@ contract MockAtomicAuctionModule is AuctionModule { uint256 multiplier; } - mapping(uint256 lotId => bool isCancelled) public cancelled; + mapping(uint96 lotId => bool isCancelled) public cancelled; constructor(address _owner) AuctionModule(_owner) { minAuctionDuration = 1 days; @@ -80,11 +80,11 @@ contract MockAtomicAuctionModule is AuctionModule { address, uint256, bytes calldata - ) internal pure override returns (uint256) { + ) internal pure override returns (uint96) { revert Auction_NotImplemented(); } - function _cancelBid(uint96, uint256, address) internal virtual override returns (uint256) { + function _cancelBid(uint96, uint96, address) internal virtual override returns (uint256) { revert Auction_NotImplemented(); } @@ -111,15 +111,15 @@ contract MockAtomicAuctionModule is AuctionModule { revert Auction_NotImplemented(); } - function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override {} + function _revertIfBidInvalid(uint96 lotId_, uint96 bidId_) internal view virtual override {} function _revertIfNotBidOwner( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) internal view virtual override {} - function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} + function _revertIfBidCancelled(uint96 lotId_, uint96 bidId_) internal view virtual override {} function _revertIfLotSettled(uint96 lotId_) internal view virtual override {} } diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index 954fb000..f7310a3a 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -39,7 +39,7 @@ contract MockAuctionModule is AuctionModule { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) internal override returns (uint256) {} + ) internal override returns (uint96) {} function payoutFor( uint256 id_, @@ -63,19 +63,19 @@ contract MockAuctionModule is AuctionModule { function _cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address bidder_ ) internal virtual override returns (uint256) {} - function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override {} + function _revertIfBidInvalid(uint96 lotId_, uint96 bidId_) internal view virtual override {} function _revertIfNotBidOwner( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) internal view virtual override {} - function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override {} + function _revertIfBidCancelled(uint96 lotId_, uint96 bidId_) internal view virtual override {} function _revertIfLotSettled(uint96 lotId_) internal view virtual override {} } diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 4f584a6a..84f527d4 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -8,7 +8,9 @@ import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; import {Auction, AuctionModule} from "src/modules/Auction.sol"; contract MockBatchAuctionModule is AuctionModule { - mapping(uint96 lotId => Bid[]) public bidData; + uint96[] public bidIds; + uint96 public nextBidId; + mapping(uint96 lotId => mapping(uint96 => Bid)) public bidData; mapping(uint96 lotId => mapping(uint256 => bool)) public bidCancelled; mapping(uint96 lotId => mapping(uint256 => bool)) public bidRefunded; @@ -45,7 +47,7 @@ contract MockBatchAuctionModule is AuctionModule { address referrer_, uint256 amount_, bytes calldata auctionData_ - ) internal override returns (uint256) { + ) internal override returns (uint96) { // Create a new bid Bid memory newBid = Bid({ bidder: bidder_, @@ -56,16 +58,16 @@ contract MockBatchAuctionModule is AuctionModule { auctionParam: auctionData_ }); - uint256 bidId = bidData[lotId_].length; + uint96 bidId = nextBidId++; - bidData[lotId_].push(newBid); + bidData[lotId_][bidId] = newBid; return bidId; } function _cancelBid( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address ) internal virtual override returns (uint256 refundAmount) { // Cancel the bid @@ -74,6 +76,16 @@ contract MockBatchAuctionModule is AuctionModule { // Mark the bid as refunded bidRefunded[lotId_][bidId_] = true; + // Remove from bid id array + uint256 len = bidIds.length; + for (uint256 i = 0; i < len; i++) { + if (bidIds[i] == bidId_) { + bidIds[i] = bidIds[len - 1]; + bidIds.pop(); + break; + } + } + return bidData[lotId_][bidId_].amount; } @@ -102,20 +114,20 @@ contract MockBatchAuctionModule is AuctionModule { returns (Bid[] memory winningBids_, bytes memory auctionOutput_) {} - function getBid(uint96 lotId_, uint256 bidId_) external view returns (Bid memory bid_) { + function getBid(uint96 lotId_, uint96 bidId_) external view returns (Bid memory bid_) { bid_ = bidData[lotId_][bidId_]; } - function _revertIfBidInvalid(uint96 lotId_, uint256 bidId_) internal view virtual override { + function _revertIfBidInvalid(uint96 lotId_, uint96 bidId_) internal view virtual override { // Check that the bid exists - if (bidData[lotId_].length <= bidId_) { + if (nextBidId <= bidId_) { revert Auction.Auction_InvalidBidId(lotId_, bidId_); } } function _revertIfNotBidOwner( uint96 lotId_, - uint256 bidId_, + uint96 bidId_, address caller_ ) internal view virtual override { // Check that the bidder is the owner of the bid @@ -124,7 +136,7 @@ contract MockBatchAuctionModule is AuctionModule { } } - function _revertIfBidCancelled(uint96 lotId_, uint256 bidId_) internal view virtual override { + function _revertIfBidCancelled(uint96 lotId_, uint96 bidId_) internal view virtual override { // Check that the bid has not been cancelled if (bidCancelled[lotId_][bidId_] == true) { revert Auction.Auction_InvalidBidId(lotId_, bidId_); diff --git a/test/modules/Auction/MockHook.sol b/test/modules/Auction/MockHook.sol index dfbade79..074b5d1f 100644 --- a/test/modules/Auction/MockHook.sol +++ b/test/modules/Auction/MockHook.sol @@ -52,7 +52,7 @@ contract MockHook is IHooks { midHookMultiplier = 10_000; } - function pre(uint256, uint256) external override { + function pre(uint96, uint256) external override { if (preHookReverts) { revert("revert"); } @@ -78,7 +78,7 @@ contract MockHook is IHooks { preHookReverts = reverts_; } - function mid(uint256, uint256, uint256 payout_) external override { + function mid(uint96, uint256, uint256 payout_) external override { if (midHookReverts) { revert("revert"); } @@ -111,7 +111,7 @@ contract MockHook is IHooks { midHookMultiplier = multiplier_; } - function post(uint256, uint256) external override { + function post(uint96, uint256) external override { if (postHookReverts) { revert("revert"); } diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index 9443c0d9..d1d7b9e0 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -120,7 +120,7 @@ contract AuctionTest is Test, Permit2User { } function test_success() external { - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module ( @@ -144,7 +144,7 @@ contract AuctionTest is Test, Permit2User { // Update auction params auctionParams.start = 0; - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); @@ -159,7 +159,7 @@ contract AuctionTest is Test, Permit2User { // Update auction params auctionParams.duration = duration; - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); @@ -172,7 +172,7 @@ contract AuctionTest is Test, Permit2User { // Update auction params auctionParams.start = start; - uint256 lotId = auctionHouse.auction(routingParams, auctionParams); + uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index 5cd08895..2bafb66a 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -250,7 +250,7 @@ contract LSBBABidTest is Test, Permit2User { function test_itRecordsTheEncryptedBid() public givenLotHasStarted { // Call vm.prank(address(auctionHouse)); - uint256 bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + uint96 bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); // Check values LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = diff --git a/test/modules/auctions/LSBBA/cancelBid.t.sol b/test/modules/auctions/LSBBA/cancelBid.t.sol index d0d874f5..6a2f3b30 100644 --- a/test/modules/auctions/LSBBA/cancelBid.t.sol +++ b/test/modules/auctions/LSBBA/cancelBid.t.sol @@ -32,7 +32,7 @@ contract LSBBACancelBidTest is Test, Permit2User { bytes internal auctionData; bytes internal constant PUBLIC_KEY_MODULUS = new bytes(128); - uint256 internal bidId; + uint96 internal bidId; uint256 internal bidAmount = 1e18; uint256 internal bidSeed = 1e9; LocalSealedBidBatchAuction.Decrypt internal decryptedBid; diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index b61cbf14..6262537c 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -39,15 +39,15 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); uint256 internal bidSeed = 1e9; - uint256 internal bidOne; + uint96 internal bidOne; uint256 internal bidOneAmount = 1e18; uint256 internal bidOneAmountOut = 3e18; LocalSealedBidBatchAuction.Decrypt internal decryptedBidOne; - uint256 internal bidTwo; + uint96 internal bidTwo; uint256 internal bidTwoAmount = 1e18; uint256 internal bidTwoAmountOut = 2e18; LocalSealedBidBatchAuction.Decrypt internal decryptedBidTwo; - uint256 internal bidThree; + uint96 internal bidThree; uint256 internal bidThreeAmount = 1e18; uint256 internal bidThreeAmountOut = 7e18; LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; @@ -105,7 +105,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { function _createBid( uint256 bidAmount_, uint256 bidAmountOut_ - ) internal returns (uint256 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid_) { + ) internal returns (uint96 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid_) { // Encrypt the bid amount LocalSealedBidBatchAuction.Decrypt memory decryptedBid = LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: bidSeed}); @@ -199,7 +199,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { _; } - modifier whenBidHasBeenCancelled(uint256 bidId_) { + modifier whenBidHasBeenCancelled(uint96 bidId_) { vm.prank(address(auctionHouse)); auctionModule.cancelBid(lotId, bidId_, alice); _; @@ -382,14 +382,14 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { // Check sorted bids QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); - assertEq(sortedBidOne.bidId, 0); - assertEq(sortedBidOne.encId, bidThree); + assertEq(sortedBidOne.queueId, 0); + assertEq(sortedBidOne.bidId, bidThree); assertEq(sortedBidOne.amountIn, bidThreeAmount); assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidTwo.bidId, 1); - assertEq(sortedBidTwo.encId, bidTwo); + assertEq(sortedBidTwo.queueId, 1); + assertEq(sortedBidTwo.bidId, bidTwo); assertEq(sortedBidTwo.amountIn, bidTwoAmount); assertEq(sortedBidTwo.minAmountOut, bidTwoAmountOut); @@ -426,8 +426,8 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { // Check sorted bids QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.bidId, 1); - assertEq(sortedBidOne.encId, bidOne); + assertEq(sortedBidOne.queueId, 1); + assertEq(sortedBidOne.bidId, bidOne); assertEq(sortedBidOne.amountIn, bidOneAmount); assertEq(sortedBidOne.minAmountOut, bidOneAmountOut); @@ -472,20 +472,20 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { // Check sorted bids QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.bidId, 2); - assertEq(sortedBidOne.encId, bidTwo); + assertEq(sortedBidOne.queueId, 2); + assertEq(sortedBidOne.bidId, bidTwo); assertEq(sortedBidOne.amountIn, bidTwoAmount); assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidTwo.bidId, 1); - assertEq(sortedBidTwo.encId, bidOne); + assertEq(sortedBidTwo.queueId, 1); + assertEq(sortedBidTwo.bidId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); - assertEq(sortedBidThree.bidId, 3); - assertEq(sortedBidThree.encId, bidThree); + assertEq(sortedBidThree.queueId, 3); + assertEq(sortedBidThree.bidId, bidThree); assertEq(sortedBidThree.amountIn, bidThreeAmount); assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); @@ -518,20 +518,20 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { // Check sorted bids QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.bidId, 2); - assertEq(sortedBidOne.encId, bidTwo); + assertEq(sortedBidOne.queueId, 2); + assertEq(sortedBidOne.bidId, bidTwo); assertEq(sortedBidOne.amountIn, bidTwoAmount); assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidTwo.bidId, 1); - assertEq(sortedBidTwo.encId, bidOne); + assertEq(sortedBidTwo.queueId, 1); + assertEq(sortedBidTwo.bidId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); - assertEq(sortedBidThree.bidId, 3); - assertEq(sortedBidThree.encId, bidThree); + assertEq(sortedBidThree.queueId, 3); + assertEq(sortedBidThree.bidId, bidThree); assertEq(sortedBidThree.amountIn, bidThreeAmount); assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); From 5200937afd6e1c48776c1393a632e7ece294500c Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:17:30 -0600 Subject: [PATCH 080/117] fix: add bid Id to array in _bid --- src/modules/auctions/LSBBA/LSBBA.sol | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 4488c5df..6ce220d4 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -179,11 +179,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { userBid.encryptedAmountOut = auctionData_; userBid.status = BidStatus.Submitted; - // Get next bid ID + // Get next bid ID and increment it after assignment bidId = auctionData[lotId_].nextBidId++; - // Store bid in mapping + // Store bid in mapping and add bid ID to list of bids to decrypt lotEncryptedBids[lotId_][bidId] = userBid; + auctionData[lotId_].bidIds.push(bidId); return bidId; } @@ -200,7 +201,13 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint96 bidId_, address ) internal override returns (uint256 refundAmount) { - // Inputs validated in Auction + // Validate inputs + + // Bid must be in Submitted state + if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_WrongState(); + + // Auction must be in Created state + if (auctionData[lotId_].status != AuctionStatus.Created) revert Auction_WrongState(); // Set bid status to cancelled lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; From bb49e7340e0477be9b4c8e065ef509e805860e9b Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:31:06 -0600 Subject: [PATCH 081/117] test: decrypt test updates --- src/modules/auctions/LSBBA/LSBBA.sol | 13 ++++++++----- test/modules/auctions/LSBBA/cancelBid.t.sol | 7 ++++++- .../modules/auctions/LSBBA/decryptAndSortBids.t.sol | 5 +++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 6ce220d4..b08d47b3 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -213,12 +213,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { lotEncryptedBids[lotId_][bidId_].status = BidStatus.Cancelled; // Remove bid from list of bids to decrypt - AuctionData storage data = auctionData[lotId_]; - uint256 len = data.bidIds.length; + uint96[] storage bidIds = auctionData[lotId_].bidIds; + uint256 len = bidIds.length; for (uint256 i; i < len; i++) { - if (data.bidIds[i] == bidId_) { - data.bidIds[i] = data.bidIds[len - 1]; - data.bidIds.pop(); + if (bidIds[i] == bidId_) { + bidIds[i] = bidIds[len - 1]; + bidIds.pop(); break; } } @@ -230,6 +230,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== DECRYPTION =========== // function decryptAndSortBids(uint96 lotId_, Decrypt[] memory decrypts_) external { + // Check that lotId is valid + _revertIfLotInvalid(lotId_); + // Check that auction is in the right state for decryption if ( auctionData[lotId_].status != AuctionStatus.Created diff --git a/test/modules/auctions/LSBBA/cancelBid.t.sol b/test/modules/auctions/LSBBA/cancelBid.t.sol index 6a2f3b30..d883a677 100644 --- a/test/modules/auctions/LSBBA/cancelBid.t.sol +++ b/test/modules/auctions/LSBBA/cancelBid.t.sol @@ -30,7 +30,12 @@ contract LSBBACancelBidTest is Test, Permit2User { uint96 internal lotId = 1; bytes internal auctionData; - bytes internal constant PUBLIC_KEY_MODULUS = new bytes(128); + bytes internal constant PUBLIC_KEY_MODULUS = abi.encodePacked( + bytes32(0xB925394F570C7C765F121826DFC8A1661921923B33408EFF62DCAC0D263952FE), + bytes32(0x158C12B2B35525F7568CB8DC7731FBC3739F22D94CB80C5622E788DB4532BD8C), + bytes32(0x8643680DA8C00A5E7C967D9D087AA1380AE9A031AC292C971EC75F9BD3296AE1), + bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) + ); uint96 internal bidId; uint256 internal bidAmount = 1e18; diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 6262537c..1c109179 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -256,7 +256,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { function test_givenLotHasNotConcluded_reverts() public whenLotHasNotConcluded { // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + bytes memory err = abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); vm.expectRevert(err); // Call @@ -354,8 +354,9 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { { // Amend the decrypts array _clearDecrypts(); + decrypts.push(decryptedBidThree); // push this first since it swapped with the cancelled one decrypts.push(decryptedBidTwo); - decrypts.push(decryptedBidThree); + // Call auctionModule.decryptAndSortBids(lotId, decrypts); From 678a314a62dd64cc17bd0ebee5606ccf61a178c7 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:42:40 -0600 Subject: [PATCH 082/117] note: TODO on minpriorityqueue --- src/modules/auctions/LSBBA/MinPriorityQueue.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index c798ce4c..7e6573d1 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -107,6 +107,8 @@ library MinPriorityQueue { ///@notice helper function to determine ordering. When two bids have the same price, give priority ///to the lower bid ID (inserted earlier) + // TODO this function works in the opposite way as the original implementation + // Maybe need to rename or clarify the logic function isGreater(Queue storage self, uint256 i, uint256 j) private view returns (bool) { uint96 iId = self.queueIdList[i]; uint96 jId = self.queueIdList[j]; @@ -115,7 +117,7 @@ library MinPriorityQueue { uint256 relI = bidI.amountIn * bidJ.minAmountOut; uint256 relJ = bidJ.amountIn * bidI.minAmountOut; if (relI == relJ) { - return iId > jId; + return bidI.bidId > bidJ.bidId; } return relI < relJ; } From 11bd95a8b03024e8b18f2004a656910312c8671a Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:45:40 -0600 Subject: [PATCH 083/117] test: fix cancel test --- .../auctions/LSBBA/decryptAndSortBids.t.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 1c109179..3c5d60db 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -382,17 +382,17 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); - assertEq(sortedBidOne.queueId, 0); - assertEq(sortedBidOne.bidId, bidThree); - assertEq(sortedBidOne.amountIn, bidThreeAmount); - assertEq(sortedBidOne.minAmountOut, bidThreeAmountOut); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); + assertEq(sortedBidOne.queueId, 2); + assertEq(sortedBidOne.bidId, bidTwo); + assertEq(sortedBidOne.amountIn, bidTwoAmount); + assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); assertEq(sortedBidTwo.queueId, 1); - assertEq(sortedBidTwo.bidId, bidTwo); - assertEq(sortedBidTwo.amountIn, bidTwoAmount); - assertEq(sortedBidTwo.minAmountOut, bidTwoAmountOut); + assertEq(sortedBidTwo.bidId, bidThree); + assertEq(sortedBidTwo.amountIn, bidThreeAmount); + assertEq(sortedBidTwo.minAmountOut, bidThreeAmountOut); assertEq(auctionModule.getSortedBidCount(lotId), 2); } From a7c0fc6093414c0cb1c8884e5c70e555f58974e5 Mon Sep 17 00:00:00 2001 From: Oighty Date: Thu, 25 Jan 2024 21:46:17 -0600 Subject: [PATCH 084/117] chore: run linter --- src/modules/auctions/LSBBA/LSBBA.sol | 10 +++++----- test/modules/auctions/LSBBA/decryptAndSortBids.t.sol | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index b08d47b3..0278f962 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -202,9 +202,11 @@ contract LocalSealedBidBatchAuction is AuctionModule { address ) internal override returns (uint256 refundAmount) { // Validate inputs - + // Bid must be in Submitted state - if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) revert Auction_WrongState(); + if (lotEncryptedBids[lotId_][bidId_].status != BidStatus.Submitted) { + revert Auction_WrongState(); + } // Auction must be in Created state if (auctionData[lotId_].status != AuctionStatus.Created) revert Auction_WrongState(); @@ -267,9 +269,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { if (encBid.status != BidStatus.Submitted) continue; // Store the decrypt in the sorted bid queue - lotSortedBids[lotId_].insert( - bidId, encBid.amount, decrypts_[i].amountOut - ); + lotSortedBids[lotId_].insert(bidId, encBid.amount, decrypts_[i].amountOut); // Set bid status to decrypted encBid.status = BidStatus.Decrypted; diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 3c5d60db..245b0409 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -256,7 +256,8 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { function test_givenLotHasNotConcluded_reverts() public whenLotHasNotConcluded { // Expect revert - bytes memory err = abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); vm.expectRevert(err); // Call @@ -356,7 +357,6 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { _clearDecrypts(); decrypts.push(decryptedBidThree); // push this first since it swapped with the cancelled one decrypts.push(decryptedBidTwo); - // Call auctionModule.decryptAndSortBids(lotId, decrypts); From 269a76a0529bcbcb65d8f289e4a15728330a3464 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 15:36:38 +0400 Subject: [PATCH 085/117] Cleanup, documentation --- src/modules/auctions/LSBBA/LSBBA.sol | 123 +++++++++++------- .../auctions/LSBBA/decryptAndSortBids.t.sol | 1 + 2 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 0278f962..31fd03a9 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -6,11 +6,12 @@ import {Veecode, toVeecode} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; -// A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends -// The auction occurs in three phases: -// 1. Bidding - bidders submit encrypted bids -// 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting -// 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred +/// @title LocalSealedBidBatchAuction +/// @notice A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends +/// @dev The auction occurs in three phases: +/// 1. Bidding - bidders submit encrypted bids +/// 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting +/// 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred contract LocalSealedBidBatchAuction is AuctionModule { using MinPriorityQueue for MinPriorityQueue.Queue; @@ -38,6 +39,14 @@ contract LocalSealedBidBatchAuction is AuctionModule { Refunded } + /// @notice Struct containing encrypted bid data + /// + /// @param status The status of the bid + /// @param bidder The address of the bidder + /// @param recipient The address of the recipient + /// @param referrer The address of the referrer + /// @param amount The amount of the bid + /// @param encryptedAmountOut The encrypted amount out struct EncryptedBid { BidStatus status; address bidder; @@ -47,18 +56,32 @@ contract LocalSealedBidBatchAuction is AuctionModule { bytes encryptedAmountOut; } + /// @notice Struct containing decrypted bid data + /// + /// @param amountOut The amount out + /// @param seed The seed used to encrypt the amount out struct Decrypt { uint256 amountOut; uint256 seed; } + /// @notice Struct containing auction data + /// + /// @param status The status of the auction + /// @param nextDecryptIndex The index of the next bid to decrypt + /// @param nextBidId The ID of the next bid to be submitted + /// @param minimumPrice The minimum price that the auction can settle at + /// @param minFilled The minimum amount of capacity that must be filled to settle the auction + /// @param minBidSize The minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price + /// @param publicKeyModulus The public key modulus used to encrypt bids + /// @param bidIds The list of bid IDs to decrypt in order of submission, excluding cancelled bids struct AuctionData { AuctionStatus status; uint96 nextDecryptIndex; uint96 nextBidId; uint256 minimumPrice; - uint256 minFilled; // minimum amount of capacity that must be filled to settle the auction - uint256 minBidSize; // minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price + uint256 minFilled; + uint256 minBidSize; bytes publicKeyModulus; uint96[] bidIds; } @@ -79,12 +102,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { // ========== STATE VARIABLES ========== // uint24 internal constant _MIN_BID_PERCENT = 1000; // 1% - uint24 internal constant _PUB_KEY_EXPONENT = 65_537; // TODO can be 3 to save gas, but 65537 is probably more secure + uint24 internal constant _PUB_KEY_EXPONENT = 65_537; uint256 internal constant _SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; - mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; // TODO must create and call `initialize` on it during auction creation + mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; // ========== SETUP ========== // @@ -154,6 +177,15 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== BID =========== // /// @inheritdoc AuctionModule + /// @dev This function performs the following: + /// - Validates inputs + /// - Stores the encrypted bid + /// - Adds the bid ID to the list of bids to decrypt (in `AuctionData.bidIds`) + /// - Returns the bid ID + /// + /// This function reverts if: + /// - The amount is less than the minimum bid size for the lot + /// - The amount is greater than the capacity function _bid( uint96 lotId_, address bidder_, @@ -190,12 +222,17 @@ contract LocalSealedBidBatchAuction is AuctionModule { } /// @inheritdoc AuctionModule - // TODO need to change this to delete the bid so we don't have to decrypt it later - // Because of this, we can issue the refund immediately (needs to happen in the AuctionHouse) - // However, this will require more refactoring because, we run into a problem of using the array index as the bidId since it will change when we delete the bid - // It doesn't cost any more gas to store a uint96 bidId as part of the EncryptedBid. - // A better approach may be to create a mapping of lotId => bidId => EncryptedBid. Then, have an array of bidIds in the AuctionData struct that can be iterated over. - // This way, we can still lookup the bids by bidId for cancellation, etc. + /// @dev This function performs the following: + /// - Validates inputs + /// - Marks the bid as cancelled + /// - Removes the bid from the list of bids to decrypt + /// - Returns the amount to be refunded + /// + /// The encrypted bid is not deleted from storage, so that the details can be fetched later. + /// + /// This function reverts if: + /// - The bid is not in the Submitted state + /// - The auction is not in the Created state function _cancelBid( uint96 lotId_, uint96 bidId_, @@ -231,6 +268,30 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== DECRYPTION =========== // + /// @notice Decrypts a batch of bids and sorts them + /// This function expects a third-party with access to the lot's private key + /// to decrypt the bids off-chain (after calling `getNextBidsToDecrypt()`) and + /// submit them on-chain. + /// @dev Anyone can call this function, provided they have access to the private key to decrypt the bids. + /// + /// This function handles the following: + /// - Performs validation + /// - Iterates over the decrypted bids: + /// - Re-encrypts the decrypted bid to confirm that it matches the stored encrypted bid + /// - Stores the decrypted bid in the sorted bid queue + /// - Sets the encrypted bid status to decrypted + /// - Determines the next decrypt index + /// - Sets the auction status to decrypted if all bids have been decrypted + /// + /// This function reverts if: + /// - The lot ID is invalid + /// - The lot has not concluded + /// - The lot has already been decrypted in full + /// - The number of decrypts is greater than the number of bids remaining to be decrypted + /// - The encrypted bid does not match the re-encrypted decrypt + /// + /// @param lotId_ The lot ID of the auction to decrypt bids for + /// @param decrypts_ An array of decrypts containing the amount out and seed for each bid function decryptAndSortBids(uint96 lotId_, Decrypt[] memory decrypts_) external { // Check that lotId is valid _revertIfLotInvalid(lotId_); @@ -265,7 +326,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { revert Auction_InvalidDecrypt(); } - // TODO shouldn't need this check but need to confirm if (encBid.status != BidStatus.Submitted) continue; // Store the decrypt in the sorted bid queue @@ -284,6 +344,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { } } + /// @notice Re-encrypts a decrypt to confirm that it matches the stored encrypted bid function _encrypt( uint96 lotId_, Decrypt memory decrypt_ @@ -297,7 +358,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { ); } - /// @notice View function that can be used to obtain a certain number of the next bids to decrypt off-chain + /// @notice View function that can be used to obtain a certain number of the next bids to decrypt off-chain function getNextBidsToDecrypt( uint96 lotId_, uint256 number_ @@ -322,34 +383,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { return bids; } - // Note: we may need to remove this function due to issues with chosen plaintext attacks on RSA implementations - // /// @notice View function that can be used to obtain the amount out and seed for a given bid by providing the private key - // /// @dev This function can be used to decrypt bids off-chain if you know the private key - // function decryptBid( - // uint96 lotId_, - // uint96 bidId_, - // bytes memory privateKey_ - // ) external view returns (Decrypt memory) { - // // Load encrypted bid - // EncryptedBid memory encBid = lotEncryptedBids[lotId_][bidId_]; - - // // Decrypt the encrypted amount out - // (bytes memory amountOut, bytes32 seed) = RSAOAEP.decrypt( - // encBid.encryptedAmountOut, - // abi.encodePacked(lotId_), - // privateKey_, - // auctionData[lotId_].publicKeyModulus - // ); - - // // Cast the decrypted values - // Decrypt memory decrypt; - // decrypt.amountOut = abi.decode(amountOut, (uint256)); - // decrypt.seed = uint256(seed); - - // // Return the decrypt - // return decrypt; - // } - // =========== SETTLEMENT =========== // /// @inheritdoc AuctionModule diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 245b0409..ff0588bc 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -38,6 +38,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) ); + // bidThree > bidOne > bidTwo uint256 internal bidSeed = 1e9; uint96 internal bidOne; uint256 internal bidOneAmount = 1e18; From 56285972fab40afd760fa14e764de85eb2216c1c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 16:27:53 +0400 Subject: [PATCH 086/117] Add tests for MinPriorityQueue --- .../auctions/LSBBA/MinPriorityQueue.t.sol | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 test/modules/auctions/LSBBA/MinPriorityQueue.t.sol diff --git a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol new file mode 100644 index 00000000..2fa5c694 --- /dev/null +++ b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; + +import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; + +contract MinPriorityQueueTest is Test { + using MinPriorityQueue for MinPriorityQueue.Queue; + + MinPriorityQueue.Queue queue; + + /// @notice The initial index of the queue + uint96 internal immutable _INITIAL_INDEX = 1; + + function setUp() public { + MinPriorityQueue.initialize(queue); + } + + // [X] initial values + // [X] when a single bid is added + // [X] when a larger bid is added + // [X] it sorts in ascending order + // [X] when a bid is added in the middle + // [X] it adds it to the middle + + function test_initialize() public { + assertEq(queue.nextBidId, 1); + assertEq(queue.queueIdList.length, 1); + assertEq(queue.queueIdList[0], 0); + assertEq(queue.numBids, 0); + + // empty + assertTrue(queue.isEmpty()); + } + + function test_singleBid() public { + queue.insert(0, 1, 1); + + // get bid + QueueBid memory bid = queue.getBid(_INITIAL_INDEX); + assertEq(bid.queueId, 1, "1: queueId mismatch"); + assertEq(bid.bidId, 0); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 1); + + // get min bid + bid = queue.getMin(); + assertEq(bid.queueId, 1); + assertEq(bid.bidId, 0); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 1); + + // numBids incremented + assertEq(queue.numBids, 1); + + // not empty + assertFalse(queue.isEmpty()); + + // queueIdList + assertEq(queue.queueIdList.length, 2); + assertEq(queue.queueIdList[0], 0); + assertEq(queue.queueIdList[1], 1); + } + + function test_addLargerBid() public { + // Add the first bid + queue.insert(0, 1, 1); + + // Add a second bid that is larger + queue.insert(1, 2, 2); + + // get first sorted bid (bid id = 0) + QueueBid memory bid = queue.getBid(_INITIAL_INDEX); + assertEq(bid.queueId, 1, "1: queueId mismatch"); + assertEq(bid.bidId, 0); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 1); + + // get second sorted bid (bid id = 1) + bid = queue.getBid(_INITIAL_INDEX + 1); + assertEq(bid.queueId, 2, "2: queueId mismatch"); + assertEq(bid.bidId, 1); + assertEq(bid.amountIn, 2); + assertEq(bid.minAmountOut, 2); + + // get min bid (bid id = 0) + bid = queue.getMin(); + assertEq(bid.queueId, 1); + assertEq(bid.bidId, 0); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 1); + + // numBids incremented + assertEq(queue.numBids, 2); + + // not empty + assertFalse(queue.isEmpty()); + + // queueIdList + assertEq(queue.queueIdList.length, 3); + assertEq(queue.queueIdList[0], 0); + assertEq(queue.queueIdList[1], 1); + assertEq(queue.queueIdList[2], 2); + } + + function test_addSmallerBid() public { + // Add the first bid + queue.insert(0, 1, 1); + + // Add a second bid that is larger + queue.insert(1, 2, 2); + + // Add a third bid that is smaller than the second bid + queue.insert(2, 1, 2); + + // get first sorted bid (bid id = 0) + QueueBid memory bid = queue.getBid(_INITIAL_INDEX); + assertEq(bid.queueId, 1, "index 1: queueId mismatch"); + assertEq(bid.bidId, 0, "index 1: bidId mismatch"); + assertEq(bid.amountIn, 1, "index 1: amountIn mismatch"); + assertEq(bid.minAmountOut, 1, "index 1: minAmountOut mismatch"); + + // get second sorted bid (bid id = 2) + bid = queue.getBid(_INITIAL_INDEX + 1); + assertEq(bid.queueId, 2, "index 2: queueId mismatch"); + assertEq(bid.bidId, 2, "index 2: bidId mismatch"); + assertEq(bid.amountIn, 1, "index 2: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); + + // get third sorted bid (bid id = 1) + bid = queue.getBid(_INITIAL_INDEX + 2); + assertEq(bid.queueId, 3, "index 3: queueId mismatch"); + assertEq(bid.bidId, 1, "index 3: bidId mismatch"); + assertEq(bid.amountIn, 2, "index 3: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 3: minAmountOut mismatch"); + + // get min bid (bid id = 0) + bid = queue.getMin(); + assertEq(bid.queueId, 1); + assertEq(bid.bidId, 0); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 1); + + // numBids incremented + assertEq(queue.numBids, 3); + + // not empty + assertFalse(queue.isEmpty()); + + // queueIdList + assertEq(queue.queueIdList.length, 4); + assertEq(queue.queueIdList[0], 0); + assertEq(queue.queueIdList[1], 1); + assertEq(queue.queueIdList[2], 3); + assertEq(queue.queueIdList[3], 2); + } +} From 39a0fc60928eb9ca797282b19f600e65b47cfe97 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 16:42:29 +0400 Subject: [PATCH 087/117] Corrections --- src/modules/auctions/LSBBA/MinPriorityQueue.sol | 4 ++-- test/modules/auctions/LSBBA/MinPriorityQueue.t.sol | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index 7e6573d1..e4c1896f 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.0; struct Bid { - uint96 queueId; // ID in queue + uint96 queueId; // ID representing order of insertion uint96 bidId; // ID of encrypted bid to reference on settlement uint256 amountIn; uint256 minAmountOut; @@ -45,7 +45,7 @@ library MinPriorityQueue { return self.queueIdToBidMap[minId]; } - ///@notice view bid by index + ///@notice view bid by index in ascending order function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { require(!isEmpty(self), "nothing to return"); require(index <= self.numBids, "bid does not exist"); diff --git a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol index 2fa5c694..8fc71945 100644 --- a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol +++ b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol @@ -107,13 +107,13 @@ contract MinPriorityQueueTest is Test { function test_addSmallerBid() public { // Add the first bid - queue.insert(0, 1, 1); + queue.insert(0, 1, 1); // queueId = 1 // Add a second bid that is larger - queue.insert(1, 2, 2); + queue.insert(1, 2, 2); // queueId = 2 // Add a third bid that is smaller than the second bid - queue.insert(2, 1, 2); + queue.insert(2, 1, 2); // queueId = 3 // get first sorted bid (bid id = 0) QueueBid memory bid = queue.getBid(_INITIAL_INDEX); @@ -124,14 +124,14 @@ contract MinPriorityQueueTest is Test { // get second sorted bid (bid id = 2) bid = queue.getBid(_INITIAL_INDEX + 1); - assertEq(bid.queueId, 2, "index 2: queueId mismatch"); + assertEq(bid.queueId, 3, "index 2: queueId mismatch"); assertEq(bid.bidId, 2, "index 2: bidId mismatch"); assertEq(bid.amountIn, 1, "index 2: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); // get third sorted bid (bid id = 1) bid = queue.getBid(_INITIAL_INDEX + 2); - assertEq(bid.queueId, 3, "index 3: queueId mismatch"); + assertEq(bid.queueId, 2, "index 3: queueId mismatch"); assertEq(bid.bidId, 1, "index 3: bidId mismatch"); assertEq(bid.amountIn, 2, "index 3: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 3: minAmountOut mismatch"); From 5b3368465eb719b477b95629c25a9cc627a13025 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 16:57:20 +0400 Subject: [PATCH 088/117] LSBBA: add tests for getNextBidsToDecrypt() --- src/modules/auctions/LSBBA/LSBBA.sol | 35 ++++- .../auctions/LSBBA/decryptAndSortBids.t.sol | 143 ++++++++++++++++++ 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 31fd03a9..b065993d 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -138,12 +138,17 @@ contract LocalSealedBidBatchAuction is AuctionModule { ) revert Auction_NotLive(); } + function _revertIfLotDecrypted(uint96 lotId_) internal view { + // Check that bids are allowed to be submitted for the lot + if (auctionData[lotId_].status == AuctionStatus.Decrypted) revert Auction_WrongState(); + } + /// @inheritdoc AuctionModule /// @dev Checks that the lot is not yet settled function _revertIfLotSettled(uint96 lotId_) internal view override { // Auction must not be settled if (auctionData[lotId_].status == AuctionStatus.Settled) { - revert Auction_MarketNotActive(lotId_); + revert Auction_WrongState(); } } @@ -359,15 +364,43 @@ contract LocalSealedBidBatchAuction is AuctionModule { } /// @notice View function that can be used to obtain a certain number of the next bids to decrypt off-chain + /// @dev This function can be called by anyone, and is used by the decryptAndSortBids() function to obtain the next bids to decrypt + /// + /// This function handles the following: + /// - Validates inputs + /// - Loads the next decrypt index + /// - Loads the number of bids to decrypt + /// - Creates an array of encrypted bids + /// - Returns the array of encrypted bids + /// + /// This function reverts if: + /// - The lot ID is invalid + /// - The lot has not concluded + /// - The lot has already been decrypted in full + /// - The number of bids to decrypt is greater than the number of bids remaining to be decrypted + /// + /// @param lotId_ The lot ID of the auction to decrypt bids for + /// @param number_ The number of bids to decrypt + /// @return bids An array of encrypted bids function getNextBidsToDecrypt( uint96 lotId_, uint256 number_ ) external view returns (EncryptedBid[] memory) { + // Validation + _revertIfLotInvalid(lotId_); + _revertIfLotActive(lotId_); + _revertIfLotDecrypted(lotId_); + _revertIfLotSettled(lotId_); + // Load next decrypt index uint96 nextDecryptIndex = auctionData[lotId_].nextDecryptIndex; // Load number of bids to decrypt uint96[] storage bidIds = auctionData[lotId_].bidIds; + + // Check that the number of bids to decrypt is less than or equal to the number of bids remaining to be decrypted + if (number_ > bidIds.length - nextDecryptIndex) revert Auction_InvalidDecrypt(); + uint256 len = bidIds.length - nextDecryptIndex; if (number_ < len) len = number_; diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index ff0588bc..6d2429df 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -539,4 +539,147 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { assertEq(auctionModule.getSortedBidCount(lotId), 3); } + + // getNextBidsToDecrypt + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the number of bids to decrypt is greater than the remaining encrypted bids + // [X] it reverts + // [X] when the lot has not concluded + // [X] it reverts + // [X] when the lot has been decrypted + // [X] it reverts + // [X] when the lot has been settled + // [X] it reverts + // [X] when the number of bids to decrypt is smaller than the remaining encrypted bids + // [X] it returns the correct number of bids + // [X] when the number of bids to decrypt is equal to the remaining encrypted bids + // [X] it returns the correct number of bids + // [X] when the number of bids to decrypt is zero + // [X] it returns an empty array + // [X] when partial decryption has occurred + // [X] it returns the correct bids + + function test_getNextBidsToDecrypt_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call + auctionModule.getNextBidsToDecrypt(lotId, 1); + } + + function test_getNextBidsToDecrypt_whenNumberOfBidsToDecryptIsGreater_reverts() + public + whenLotHasConcluded + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_InvalidDecrypt.selector); + vm.expectRevert(err); + + // Call + auctionModule.getNextBidsToDecrypt(lotId, 4); + } + + function test_getNextBidsToDecrypt_whenLotHasNotConcluded_reverts() + public + whenLotHasNotConcluded + { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + + // Call + auctionModule.getNextBidsToDecrypt(lotId, 1); + } + + function test_getNextBidsToDecrypt_whenLotDecryptionIsComplete_reverts() + public + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call + auctionModule.getNextBidsToDecrypt(lotId, 1); + } + + function test_getNextBidsToDecrypt_whenLotHasSettled_reverts() + public + whenLotHasConcluded + whenLotDecryptionIsComplete + whenLotHasSettled + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call + auctionModule.getNextBidsToDecrypt(lotId, 1); + } + + function test_getNextBidsToDecrypt_whenNumberOfBidsToDecryptIsSmaller_returnsCorrectBids() + public + whenLotHasConcluded + { + // Call + LocalSealedBidBatchAuction.EncryptedBid[] memory bids = + auctionModule.getNextBidsToDecrypt(lotId, 2); + + // Check the bids + assertEq(bids.length, 2); + assertEq(bids[0].amount, bidOneAmount); + assertEq(bids[1].amount, bidTwoAmount); + } + + function test_getNextBidsToDecrypt_whenNumberOfBidsToDecryptIsEqual_returnsCorrectBids() + public + whenLotHasConcluded + { + // Call + LocalSealedBidBatchAuction.EncryptedBid[] memory bids = + auctionModule.getNextBidsToDecrypt(lotId, 3); + + // Check the bids + assertEq(bids.length, 3); + assertEq(bids[0].amount, bidOneAmount); + assertEq(bids[1].amount, bidTwoAmount); + assertEq(bids[2].amount, bidThreeAmount); + } + + function test_getNextBidsToDecrypt_whenNumberOfBidsToDecryptIsZero_returnsEmptyArray() + public + whenLotHasConcluded + { + // Call + LocalSealedBidBatchAuction.EncryptedBid[] memory bids = + auctionModule.getNextBidsToDecrypt(lotId, 0); + + // Check the bids + assertEq(bids.length, 0); + } + + function test_getNextBidsToDecrypt_whenPartialDecryptionHasOccurred_returnsCorrectBids() + public + whenLotHasConcluded + { + // Decrypt 1 bid + _clearDecrypts(); + decrypts.push(decryptedBidOne); + auctionModule.decryptAndSortBids(lotId, decrypts); + + // Call + LocalSealedBidBatchAuction.EncryptedBid[] memory bids = + auctionModule.getNextBidsToDecrypt(lotId, 2); + + // Check the bids + assertEq(bids.length, 2); + assertEq(bids[0].amount, bidTwoAmount); + assertEq(bids[1].amount, bidThreeAmount); + } } From 1c6206a76cc35a7ebc51049873bf5a399d2d10f9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 18:08:31 +0400 Subject: [PATCH 089/117] LSBBA: tests for settle() --- src/modules/auctions/LSBBA/LSBBA.sol | 11 + test/modules/auctions/LSBBA/settle.t.sol | 389 +++++++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 test/modules/auctions/LSBBA/settle.t.sol diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index b065993d..e11dd27c 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -138,6 +138,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { ) revert Auction_NotLive(); } + /// @notice Reverts if the lot has already been decrypted function _revertIfLotDecrypted(uint96 lotId_) internal view { // Check that bids are allowed to be submitted for the lot if (auctionData[lotId_].status == AuctionStatus.Decrypted) revert Auction_WrongState(); @@ -419,6 +420,16 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== SETTLEMENT =========== // /// @inheritdoc AuctionModule + /// @dev This function performs the following: + /// - Validates inputs + /// - Iterates over the bid queue to calculate the marginal clearing price of the auction + /// - Creates an array of winning bids + /// - Sets the auction status to settled + /// - Returns the array of winning bids + /// + /// This function reverts if: + /// - The auction is not in the Decrypted state + /// - The auction has already been settled function _settle(uint96 lotId_) internal override diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol new file mode 100644 index 00000000..d23c961b --- /dev/null +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; +import {RSAOAEP} from "src/lib/RSA.sol"; +import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; + +contract LSBBASettleTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + address internal alice = address(0x2); + address internal constant recipient = address(0x3); + address internal constant referrer = address(0x4); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + uint96 internal lotId = 1; + bytes internal auctionData; + bytes internal constant PUBLIC_KEY_MODULUS = abi.encodePacked( + bytes32(0xB925394F570C7C765F121826DFC8A1661921923B33408EFF62DCAC0D263952FE), + bytes32(0x158C12B2B35525F7568CB8DC7731FBC3739F22D94CB80C5622E788DB4532BD8C), + bytes32(0x8643680DA8C00A5E7C967D9D087AA1380AE9A031AC292C971EC75F9BD3296AE1), + bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) + ); + + uint256 internal bidSeed = 1e9; + uint96 internal bidOne; + uint256 internal bidOneAmount = 2e18; + uint256 internal bidOneAmountOut = 2e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidOne; + uint96 internal bidTwo; + uint256 internal bidTwoAmount = 3e18; + uint256 internal bidTwoAmountOut = 3e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidTwo; + uint96 internal bidThree; + uint256 internal bidThreeAmount = 7e18; + uint256 internal bidThreeAmountOut = 7e18; + LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; + uint96 internal bidFour; + uint256 internal bidFourAmount = 2e18; + uint256 internal bidFourAmountOut = 4e18; // Price < 1e18 (minimum price) + LocalSealedBidBatchAuction.Decrypt internal decryptedBidFour; + LocalSealedBidBatchAuction.Decrypt[] internal decrypts; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = + LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 25_000, // 25% = 2.5e18 + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: PUBLIC_KEY_MODULUS + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + + Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + // Warp to the start of the auction + vm.warp(lotStart); + } + + function _createBid( + uint256 bidAmount_, + uint256 bidAmountOut_ + ) internal returns (uint96 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid_) { + // Encrypt the bid amount + LocalSealedBidBatchAuction.Decrypt memory decryptedBid = + LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: bidSeed}); + bytes memory auctionData_ = _encrypt(decryptedBid); + + // Create a bid + vm.prank(address(auctionHouse)); + bidId_ = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount_, auctionData_); + + return (bidId_, decryptedBid); + } + + function _encrypt(LocalSealedBidBatchAuction.Decrypt memory decrypt_) + internal + view + returns (bytes memory) + { + return RSAOAEP.encrypt( + abi.encodePacked(decrypt_.amountOut), + abi.encodePacked(lotId), + abi.encodePacked(uint24(65_537)), + PUBLIC_KEY_MODULUS, + decrypt_.seed + ); + } + + function _clearDecrypts() internal { + uint256 len = decrypts.length; + // Remove all elements + for (uint256 i = 0; i < len; i++) { + decrypts.pop(); + } + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier whenLotHasNotConcluded() { + vm.warp(lotConclusion - 1); + _; + } + + modifier whenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + + modifier whenLotIsBelowMinimumFilled() { + // 2 < 2.5 + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + + // Set up the decrypts array + decrypts.push(decryptedBidOne); + _; + } + + modifier whenLotIsOverSubscribed() { + // 2 + 3 + 7 > 10 + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); + (bidThree, decryptedBidThree) = _createBid(bidThreeAmount, bidThreeAmountOut); + + // Set up the decrypts array + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidThree); + _; + } + + modifier whenMarginalPriceBelowMinimum() { + // 2 + 2 > 2.5 + // Marginal price of 2/4 = 0.5 < 1 + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + (bidFour, decryptedBidFour) = _createBid(bidFourAmount, bidFourAmountOut); + + // Set up the decrypts array + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidFour); + _; + } + + modifier whenLotIsFilled() { + // 2 + 3 > 2.5 + // Above minimum price + // Not over capacity + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); + + // Set up the decrypts array + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidTwo); + _; + } + + modifier whenLotDecryptionIsComplete() { + // Decrypt the bids + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier whenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + // ===== Tests ===== // + + // [X] when the lot id is invalid + // [X] it reverts + // [X] when the caller is not the parent + // [X] it reverts + // [X] when execOnModule is used + // [X] it reverts + // [X] when the lot has not concluded + // [X] it reverts + // [X] when the lot has not been decrypted + // [X] it reverts + // [X] when the lot has been settled already + // [X] it reverts + // [X] when the filled amount is less than the lot minimum + // [X] it returns no winning bids + // [X] when the marginal price is less than the minimum price + // [X] it returns no winning bids + // [X] given the lot is over-subscribed + // [X] it returns winning bids, with the marginal price is the price at which the lot capacity is exhausted + // [ ] given the lot is over-subscribed with a partial fill + // [ ] it returns winning bids, with the marginal price is the price at which the lot capacity is exhausted, and a partial fill for the lowest winning bid + // [X] when the filled amount is greater than the lot minimum + // [X] it returns winning bids, with the marginal price is the minimum price + + function test_whenLotIdIsInvalid_reverts() public whenLotIdIsInvalid { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + } + + function test_notParent_reverts() public { + // Expect revert + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Call for settlement + auctionModule.settle(lotId); + } + + function test_execOnModule_reverts() public { + Veecode moduleVeecode = auctionModule.VEECODE(); + + // Expect revert + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) + ); + vm.expectRevert(err); + + // Call for settlement + auctionHouse.execOnModule( + moduleVeecode, abi.encodeWithSelector(Auction.settle.selector, lotId) + ); + } + + function test_whenLotHasNotConcluded_reverts() public whenLotHasNotConcluded { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auction.Auction_MarketActive.selector, lotId); + vm.expectRevert(err); + + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + } + + function test_notDecrypted_reverts() public whenLotIsFilled whenLotHasConcluded { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + } + + function test_whenLotHasSettled_reverts() + public + whenLotIsFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + whenLotHasSettled + { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + } + + function test_whenLotIsBelowMinimumFilled_returnsNoWinningBids() + public + whenLotIsBelowMinimumFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + + function test_whenMarginalPriceBelowMinimum_returnsNoWinningBids() + public + whenMarginalPriceBelowMinimum + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + + function test_whenLotIsOverSubscribed_returnsWinningBids() + public + whenLotIsOverSubscribed + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidThreeAmount); + assertEq(winningBids[0].minAmountOut, marginalPrice); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount); + assertEq(winningBids[1].minAmountOut, marginalPrice); + + // Expect winning bids + assertEq(winningBids.length, 2); + } + + function test_whenLotIsFilled() + public + whenLotIsFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = bidOneAmount * 1e18 / bidOneAmountOut; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidTwoAmount); + assertEq(winningBids[0].minAmountOut, marginalPrice); + + // Second bid + assertEq(winningBids[1].amount, bidOneAmount); + assertEq(winningBids[1].minAmountOut, marginalPrice); + + // Expect winning bids + assertEq(winningBids.length, 2); + } +} From 54269ee91312a8f5a02b3c425614a456e5256541 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 18:27:31 +0400 Subject: [PATCH 090/117] Trying to figure out MinPriorityQueue --- .../auctions/LSBBA/MinPriorityQueue.t.sol | 66 ++++++++++--------- .../auctions/LSBBA/decryptAndSortBids.t.sol | 8 +-- test/modules/auctions/LSBBA/settle.t.sol | 7 +- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol index 8fc71945..8c4b82af 100644 --- a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol +++ b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol @@ -18,6 +18,8 @@ contract MinPriorityQueueTest is Test { MinPriorityQueue.initialize(queue); } + // Sorted by Bid.amountIn / Bid.minAmountOut + // [X] initial values // [X] when a single bid is added // [X] when a larger bid is added @@ -36,7 +38,7 @@ contract MinPriorityQueueTest is Test { } function test_singleBid() public { - queue.insert(0, 1, 1); + queue.insert(0, 1, 1); // Price = 1/1 // get bid QueueBid memory bid = queue.getBid(_INITIAL_INDEX); @@ -66,28 +68,28 @@ contract MinPriorityQueueTest is Test { function test_addLargerBid() public { // Add the first bid - queue.insert(0, 1, 1); + queue.insert(0, 1, 1); // Price = 1/1 // Add a second bid that is larger - queue.insert(1, 2, 2); + queue.insert(1, 4, 2); // Price = 4/2 = 2 - // get first sorted bid (bid id = 0) + // get first sorted bid (bid id = 1) QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 1, "1: queueId mismatch"); - assertEq(bid.bidId, 0); - assertEq(bid.amountIn, 1); - assertEq(bid.minAmountOut, 1); + assertEq(bid.queueId, 2, "1: queueId mismatch"); + assertEq(bid.bidId, 1, "1: bidId mismatch"); + assertEq(bid.amountIn, 4, "1: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "1: minAmountOut mismatch"); - // get second sorted bid (bid id = 1) + // get second sorted bid (bid id = 0) bid = queue.getBid(_INITIAL_INDEX + 1); - assertEq(bid.queueId, 2, "2: queueId mismatch"); - assertEq(bid.bidId, 1); - assertEq(bid.amountIn, 2); - assertEq(bid.minAmountOut, 2); + assertEq(bid.queueId, 1, "2: queueId mismatch"); + assertEq(bid.bidId, 0, "2: bidId mismatch"); + assertEq(bid.amountIn, 1, "2: amountIn mismatch"); + assertEq(bid.minAmountOut, 1, "2: minAmountOut mismatch"); // get min bid (bid id = 0) bid = queue.getMin(); - assertEq(bid.queueId, 1); + assertEq(bid.queueId, 1, "min: queueId mismatch"); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); assertEq(bid.minAmountOut, 1); @@ -101,40 +103,40 @@ contract MinPriorityQueueTest is Test { // queueIdList assertEq(queue.queueIdList.length, 3); assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 1); - assertEq(queue.queueIdList[2], 2); + assertEq(queue.queueIdList[1], 2); + assertEq(queue.queueIdList[2], 1); } function test_addSmallerBid() public { // Add the first bid - queue.insert(0, 1, 1); // queueId = 1 + queue.insert(0, 1, 1); // queueId = 1, price = 1/1 // Add a second bid that is larger - queue.insert(1, 2, 2); // queueId = 2 + queue.insert(1, 4, 2); // queueId = 2, price = 4/2 = 2 // Add a third bid that is smaller than the second bid - queue.insert(2, 1, 2); // queueId = 3 + queue.insert(2, 3, 2); // queueId = 3, price = 3/2 = 1.5 - // get first sorted bid (bid id = 0) + // get first sorted bid (bid id = 1) QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 1, "index 1: queueId mismatch"); - assertEq(bid.bidId, 0, "index 1: bidId mismatch"); - assertEq(bid.amountIn, 1, "index 1: amountIn mismatch"); - assertEq(bid.minAmountOut, 1, "index 1: minAmountOut mismatch"); + assertEq(bid.queueId, 2, "index 1: queueId mismatch"); + assertEq(bid.bidId, 1, "index 1: bidId mismatch"); + assertEq(bid.amountIn, 4, "index 1: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 1: minAmountOut mismatch"); // get second sorted bid (bid id = 2) bid = queue.getBid(_INITIAL_INDEX + 1); assertEq(bid.queueId, 3, "index 2: queueId mismatch"); assertEq(bid.bidId, 2, "index 2: bidId mismatch"); - assertEq(bid.amountIn, 1, "index 2: amountIn mismatch"); + assertEq(bid.amountIn, 3, "index 2: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); - // get third sorted bid (bid id = 1) + // get third sorted bid (bid id = 0) bid = queue.getBid(_INITIAL_INDEX + 2); - assertEq(bid.queueId, 2, "index 3: queueId mismatch"); - assertEq(bid.bidId, 1, "index 3: bidId mismatch"); - assertEq(bid.amountIn, 2, "index 3: amountIn mismatch"); - assertEq(bid.minAmountOut, 2, "index 3: minAmountOut mismatch"); + assertEq(bid.queueId, 1, "index 3: queueId mismatch"); + assertEq(bid.bidId, 0, "index 3: bidId mismatch"); + assertEq(bid.amountIn, 1, "index 3: amountIn mismatch"); + assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); // get min bid (bid id = 0) bid = queue.getMin(); @@ -152,8 +154,8 @@ contract MinPriorityQueueTest is Test { // queueIdList assertEq(queue.queueIdList.length, 4); assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 1); + assertEq(queue.queueIdList[1], 2); assertEq(queue.queueIdList[2], 3); - assertEq(queue.queueIdList[3], 2); + assertEq(queue.queueIdList[3], 1); } } diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 6d2429df..fca5347e 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -38,19 +38,19 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) ); - // bidThree > bidOne > bidTwo + // bidTwo > bidOne > bidThree uint256 internal bidSeed = 1e9; uint96 internal bidOne; uint256 internal bidOneAmount = 1e18; - uint256 internal bidOneAmountOut = 3e18; + uint256 internal bidOneAmountOut = 3e18; // Price = 1/3 LocalSealedBidBatchAuction.Decrypt internal decryptedBidOne; uint96 internal bidTwo; uint256 internal bidTwoAmount = 1e18; - uint256 internal bidTwoAmountOut = 2e18; + uint256 internal bidTwoAmountOut = 2e18; // Price = 1/2 LocalSealedBidBatchAuction.Decrypt internal decryptedBidTwo; uint96 internal bidThree; uint256 internal bidThreeAmount = 1e18; - uint256 internal bidThreeAmountOut = 7e18; + uint256 internal bidThreeAmountOut = 7e18; // Price = 1/7 LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; LocalSealedBidBatchAuction.Decrypt[] internal decrypts; diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index d23c961b..547d48b7 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -38,18 +38,19 @@ contract LSBBASettleTest is Test, Permit2User { bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) ); + // TODO adjust these uint256 internal bidSeed = 1e9; uint96 internal bidOne; uint256 internal bidOneAmount = 2e18; - uint256 internal bidOneAmountOut = 2e18; + uint256 internal bidOneAmountOut = 2e18; // Price = 1 LocalSealedBidBatchAuction.Decrypt internal decryptedBidOne; uint96 internal bidTwo; uint256 internal bidTwoAmount = 3e18; - uint256 internal bidTwoAmountOut = 3e18; + uint256 internal bidTwoAmountOut = 3e18; // Price = 1 LocalSealedBidBatchAuction.Decrypt internal decryptedBidTwo; uint96 internal bidThree; uint256 internal bidThreeAmount = 7e18; - uint256 internal bidThreeAmountOut = 7e18; + uint256 internal bidThreeAmountOut = 7e18; // Price = 1 LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; uint96 internal bidFour; uint256 internal bidFourAmount = 2e18; From a3db271b9d1badc126f54714d87b635bee69d5d4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 26 Jan 2024 20:53:52 +0400 Subject: [PATCH 091/117] Add more extensive tests for MinPriorityQueue --- .../auctions/LSBBA/MinPriorityQueue.sol | 1 + test/lib/RSA/encrypt.t.sol | 3 + .../auctions/LSBBA/MinPriorityQueue.t.sol | 75 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MinPriorityQueue.sol index e4c1896f..df3062c6 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MinPriorityQueue.sol @@ -12,6 +12,7 @@ struct Bid { /// @notice adapted from FrankieIsLost's implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MinPriorityQueue.sol /// @author FrankieIsLost /// @author Oighty (edits) +/// Bids in descending order library MinPriorityQueue { struct Queue { ///@notice incrementing bid id diff --git a/test/lib/RSA/encrypt.t.sol b/test/lib/RSA/encrypt.t.sol index 62ea6194..477575a2 100644 --- a/test/lib/RSA/encrypt.t.sol +++ b/test/lib/RSA/encrypt.t.sol @@ -13,6 +13,9 @@ contract RSAOAEPTest is Test { function setUp() external {} + // TODO more extensive cases + // TODO fuzz value to encrypt + function test_roundTrip(uint256 seed_) external { uint256 value = 5 * 10 ** 18; bytes memory message = abi.encodePacked(value); diff --git a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol index 8c4b82af..5a9e8019 100644 --- a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol +++ b/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.19; // Tests import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; @@ -118,6 +119,7 @@ contract MinPriorityQueueTest is Test { queue.insert(2, 3, 2); // queueId = 3, price = 3/2 = 1.5 // get first sorted bid (bid id = 1) + console2.log("getBid(1)"); QueueBid memory bid = queue.getBid(_INITIAL_INDEX); assertEq(bid.queueId, 2, "index 1: queueId mismatch"); assertEq(bid.bidId, 1, "index 1: bidId mismatch"); @@ -125,6 +127,7 @@ contract MinPriorityQueueTest is Test { assertEq(bid.minAmountOut, 2, "index 1: minAmountOut mismatch"); // get second sorted bid (bid id = 2) + console2.log("getBid(2)"); bid = queue.getBid(_INITIAL_INDEX + 1); assertEq(bid.queueId, 3, "index 2: queueId mismatch"); assertEq(bid.bidId, 2, "index 2: bidId mismatch"); @@ -132,6 +135,7 @@ contract MinPriorityQueueTest is Test { assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); // get third sorted bid (bid id = 0) + console2.log("getBid(3)"); bid = queue.getBid(_INITIAL_INDEX + 2); assertEq(bid.queueId, 1, "index 3: queueId mismatch"); assertEq(bid.bidId, 0, "index 3: bidId mismatch"); @@ -139,6 +143,7 @@ contract MinPriorityQueueTest is Test { assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); // get min bid (bid id = 0) + console2.log("getMin()"); bid = queue.getMin(); assertEq(bid.queueId, 1); assertEq(bid.bidId, 0); @@ -152,10 +157,80 @@ contract MinPriorityQueueTest is Test { assertFalse(queue.isEmpty()); // queueIdList + console2.log("queueIdList"); assertEq(queue.queueIdList.length, 4); assertEq(queue.queueIdList[0], 0); assertEq(queue.queueIdList[1], 2); assertEq(queue.queueIdList[2], 3); assertEq(queue.queueIdList[3], 1); } + + function test_fourItems() public { + // Add the first bid + queue.insert(0, 1, 1); // queueId = 1, price = 1/1 + + // Add a second bid that is larger + queue.insert(1, 4, 2); // queueId = 2, price = 4/2 = 2 + + // Add a third bid that is smaller than the second bid + queue.insert(2, 3, 2); // queueId = 3, price = 3/2 = 1.5 + + // Add a fourth bid that is smaller than the first bid + queue.insert(3, 1, 2); // queueId = 4, price = 1/2 + + // get first sorted bid (bid id = 1) + console2.log("getBid(1)"); + QueueBid memory bid = queue.getBid(_INITIAL_INDEX); + assertEq(bid.queueId, 2, "index 1: queueId mismatch"); + assertEq(bid.bidId, 1, "index 1: bidId mismatch"); + assertEq(bid.amountIn, 4, "index 1: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 1: minAmountOut mismatch"); + + // get second sorted bid (bid id = 2) + console2.log("getBid(2)"); + bid = queue.getBid(_INITIAL_INDEX + 1); + assertEq(bid.queueId, 3, "index 2: queueId mismatch"); + assertEq(bid.bidId, 2, "index 2: bidId mismatch"); + assertEq(bid.amountIn, 3, "index 2: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); + + // get third sorted bid (bid id = 0) + console2.log("getBid(3)"); + bid = queue.getBid(_INITIAL_INDEX + 2); + assertEq(bid.queueId, 1, "index 3: queueId mismatch"); + assertEq(bid.bidId, 0, "index 3: bidId mismatch"); + assertEq(bid.amountIn, 1, "index 3: amountIn mismatch"); + assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); + + // get fourth sorted bid (bid id = 3) + console2.log("getBid(4)"); + bid = queue.getBid(_INITIAL_INDEX + 3); + assertEq(bid.queueId, 4, "index 4: queueId mismatch"); + assertEq(bid.bidId, 3, "index 4: bidId mismatch"); + assertEq(bid.amountIn, 1, "index 4: amountIn mismatch"); + assertEq(bid.minAmountOut, 2, "index 4: minAmountOut mismatch"); + + // get min bid (bid id = 3) + console2.log("getMin()"); + bid = queue.getMin(); + assertEq(bid.queueId, 4); + assertEq(bid.bidId, 3); + assertEq(bid.amountIn, 1); + assertEq(bid.minAmountOut, 2); + + // numBids incremented + assertEq(queue.numBids, 3); + + // not empty + assertFalse(queue.isEmpty()); + + // queueIdList + console2.log("queueIdList"); + assertEq(queue.queueIdList.length, 4); + assertEq(queue.queueIdList[0], 0); + assertEq(queue.queueIdList[1], 2); + assertEq(queue.queueIdList[2], 3); + assertEq(queue.queueIdList[3], 1); + assertEq(queue.queueIdList[4], 4); + } } From 59956a4bf2d53d0f9dd7a1f5b1107184c404d76a Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 26 Jan 2024 11:29:03 -0600 Subject: [PATCH 092/117] fix: rename queue to max --- src/modules/auctions/LSBBA/LSBBA.sol | 10 +++--- ...PriorityQueue.sol => MaxPriorityQueue.sol} | 36 +++++++++---------- ...rityQueue.t.sol => MaxPriorityQueue.t.sol} | 30 ++++++++-------- .../auctions/LSBBA/decryptAndSortBids.t.sol | 2 +- test/modules/auctions/LSBBA/settle.t.sol | 2 +- 5 files changed, 39 insertions(+), 41 deletions(-) rename src/modules/auctions/LSBBA/{MinPriorityQueue.sol => MaxPriorityQueue.sol} (77%) rename test/modules/auctions/LSBBA/{MinPriorityQueue.t.sol => MaxPriorityQueue.t.sol} (92%) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index e11dd27c..f25a3154 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {AuctionModule} from "src/modules/Auction.sol"; import {Veecode, toVeecode} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; -import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {MaxPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; /// @title LocalSealedBidBatchAuction /// @notice A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends @@ -13,7 +13,7 @@ import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinP /// 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting /// 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred contract LocalSealedBidBatchAuction is AuctionModule { - using MinPriorityQueue for MinPriorityQueue.Queue; + using MaxPriorityQueue for MaxPriorityQueue.Queue; // ========== ERRORS ========== // error Auction_BidDoesNotExist(); @@ -107,7 +107,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; - mapping(uint96 lotId => MinPriorityQueue.Queue) public lotSortedBids; + mapping(uint96 lotId => MaxPriorityQueue.Queue) public lotSortedBids; // ========== SETUP ========== // @@ -442,7 +442,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint256 capacity = lotData[lotId_].capacity; // Iterate over bid queue to calculate the marginal clearing price of the auction - MinPriorityQueue.Queue storage queue = lotSortedBids[lotId_]; + MaxPriorityQueue.Queue storage queue = lotSortedBids[lotId_]; uint256 marginalPrice; uint256 totalAmountIn; uint256 winningBidIndex; @@ -491,7 +491,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { winningBids_ = new Bid[](winningBidIndex); for (uint256 i; i < winningBidIndex; i++) { // Load bid - QueueBid memory qBid = queue.delMin(); + QueueBid memory qBid = queue.delMax(); // Calculate amount out // TODO handle partial filling of the last winning bid diff --git a/src/modules/auctions/LSBBA/MinPriorityQueue.sol b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol similarity index 77% rename from src/modules/auctions/LSBBA/MinPriorityQueue.sol rename to src/modules/auctions/LSBBA/MaxPriorityQueue.sol index df3062c6..c0eca0c9 100644 --- a/src/modules/auctions/LSBBA/MinPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol @@ -8,12 +8,12 @@ struct Bid { uint256 minAmountOut; } -/// @notice a min priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MinPQ.java.html -/// @notice adapted from FrankieIsLost's implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MinPriorityQueue.sol +/// @notice a max priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MaxPQ.java.html +/// @notice adapted from FrankieIsLost's implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MaxPriorityQueue.sol /// @author FrankieIsLost /// @author Oighty (edits) /// Bids in descending order -library MinPriorityQueue { +library MaxPriorityQueue { struct Queue { ///@notice incrementing bid id uint96 nextBidId; @@ -39,11 +39,11 @@ library MinPriorityQueue { return self.numBids; } - ///@notice view min bid - function getMin(Queue storage self) public view returns (Bid storage) { + ///@notice view max bid + function getMax(Queue storage self) public view returns (Bid storage) { require(!isEmpty(self), "nothing to return"); - uint96 minId = self.queueIdList[1]; - return self.queueIdToBidMap[minId]; + uint96 maxId = self.queueIdList[1]; + return self.queueIdToBidMap[maxId]; } ///@notice view bid by index in ascending order @@ -56,7 +56,7 @@ library MinPriorityQueue { ///@notice move bid up heap function swim(Queue storage self, uint96 k) private { - while (k > 1 && isGreater(self, k / 2, k)) { + while (k > 1 && isLess(self, k / 2, k)) { exchange(self, k, k / 2); k = k / 2; } @@ -66,10 +66,10 @@ library MinPriorityQueue { function sink(Queue storage self, uint96 k) private { while (2 * k <= self.numBids) { uint96 j = 2 * k; - if (j < self.numBids && isGreater(self, j, j + 1)) { + if (j < self.numBids && isLess(self, j, j + 1)) { j++; } - if (!isGreater(self, k, j)) { + if (!isLess(self, k, j)) { break; } exchange(self, k, j); @@ -95,22 +95,20 @@ library MinPriorityQueue { swim(self, self.numBids); } - ///@notice delete min bid from heap and return - function delMin(Queue storage self) public returns (Bid memory) { + ///@notice delete max bid from heap and return + function delMax(Queue storage self) public returns (Bid memory) { require(!isEmpty(self), "nothing to delete"); - Bid memory min = self.queueIdToBidMap[self.queueIdList[1]]; + Bid memory max = self.queueIdToBidMap[self.queueIdList[1]]; exchange(self, 1, self.numBids--); self.queueIdList.pop(); - delete self.queueIdToBidMap[min.queueId]; + delete self.queueIdToBidMap[max.queueId]; sink(self, 1); - return min; + return max; } ///@notice helper function to determine ordering. When two bids have the same price, give priority ///to the lower bid ID (inserted earlier) - // TODO this function works in the opposite way as the original implementation - // Maybe need to rename or clarify the logic - function isGreater(Queue storage self, uint256 i, uint256 j) private view returns (bool) { + function isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { uint96 iId = self.queueIdList[i]; uint96 jId = self.queueIdList[j]; Bid memory bidI = self.queueIdToBidMap[iId]; @@ -118,7 +116,7 @@ library MinPriorityQueue { uint256 relI = bidI.amountIn * bidJ.minAmountOut; uint256 relJ = bidJ.amountIn * bidI.minAmountOut; if (relI == relJ) { - return bidI.bidId > bidJ.bidId; + return bidI.bidId < bidJ.bidId; } return relI < relJ; } diff --git a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol similarity index 92% rename from test/modules/auctions/LSBBA/MinPriorityQueue.t.sol rename to test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol index 5a9e8019..fbd9b976 100644 --- a/test/modules/auctions/LSBBA/MinPriorityQueue.t.sol +++ b/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol @@ -5,18 +5,18 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {MinPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {MaxPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; -contract MinPriorityQueueTest is Test { - using MinPriorityQueue for MinPriorityQueue.Queue; +contract MaxPriorityQueueTest is Test { + using MaxPriorityQueue for MaxPriorityQueue.Queue; - MinPriorityQueue.Queue queue; + MaxPriorityQueue.Queue queue; /// @notice The initial index of the queue uint96 internal immutable _INITIAL_INDEX = 1; function setUp() public { - MinPriorityQueue.initialize(queue); + MaxPriorityQueue.initialize(queue); } // Sorted by Bid.amountIn / Bid.minAmountOut @@ -48,8 +48,8 @@ contract MinPriorityQueueTest is Test { assertEq(bid.amountIn, 1); assertEq(bid.minAmountOut, 1); - // get min bid - bid = queue.getMin(); + // get max bid + bid = queue.getMax(); assertEq(bid.queueId, 1); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); @@ -88,8 +88,8 @@ contract MinPriorityQueueTest is Test { assertEq(bid.amountIn, 1, "2: amountIn mismatch"); assertEq(bid.minAmountOut, 1, "2: minAmountOut mismatch"); - // get min bid (bid id = 0) - bid = queue.getMin(); + // get max bid (bid id = 0) + bid = queue.getMax(); assertEq(bid.queueId, 1, "min: queueId mismatch"); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); @@ -142,9 +142,9 @@ contract MinPriorityQueueTest is Test { assertEq(bid.amountIn, 1, "index 3: amountIn mismatch"); assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); - // get min bid (bid id = 0) - console2.log("getMin()"); - bid = queue.getMin(); + // get max bid (bid id = 0) + console2.log("getMax()"); + bid = queue.getMax(); assertEq(bid.queueId, 1); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); @@ -210,9 +210,9 @@ contract MinPriorityQueueTest is Test { assertEq(bid.amountIn, 1, "index 4: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 4: minAmountOut mismatch"); - // get min bid (bid id = 3) - console2.log("getMin()"); - bid = queue.getMin(); + // get max bid (bid id = 3) + console2.log("getMax()"); + bid = queue.getMax(); assertEq(bid.queueId, 4); assertEq(bid.bidId, 3); assertEq(bid.amountIn, 1); diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index fca5347e..2bd55108 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -12,7 +12,7 @@ import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; import {Auction} from "src/modules/Auction.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; -import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; contract LSBBADecryptAndSortBidsTest is Test, Permit2User { address internal constant _PROTOCOL = address(0x1); diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index 547d48b7..cf76e0a8 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -12,7 +12,7 @@ import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; import {AuctionHouse} from "src/AuctionHouse.sol"; import {Auction} from "src/modules/Auction.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; -import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MinPriorityQueue.sol"; +import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; contract LSBBASettleTest is Test, Permit2User { address internal constant _PROTOCOL = address(0x1); From 835ec70ca88f345fd42ea1c6acdc87b03d973f1c Mon Sep 17 00:00:00 2001 From: Oighty Date: Fri, 26 Jan 2024 15:15:45 -0600 Subject: [PATCH 093/117] feat: replace priority queue implementation --- src/modules/auctions/LSBBA/LSBBA.sol | 37 ++-- .../auctions/LSBBA/MaxPriorityQueue.sol | 169 +++++++----------- .../auctions/LSBBA/OldMaxPriorityQueue.sol | 130 ++++++++++++++ .../auctions/LSBBA/MaxPriorityQueue.t.sol | 140 +++++---------- test/modules/auctions/LSBBA/auction.t.sol | 4 - .../auctions/LSBBA/decryptAndSortBids.t.sol | 27 +-- test/modules/auctions/LSBBA/settle.t.sol | 12 +- 7 files changed, 278 insertions(+), 241 deletions(-) create mode 100644 src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index f25a3154..48259fc8 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -4,7 +4,11 @@ pragma solidity 0.8.19; import {AuctionModule} from "src/modules/Auction.sol"; import {Veecode, toVeecode} from "src/modules/Modules.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; -import {MaxPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; +import { + MaxPriorityQueue, + Queue, + Bid as QueueBid +} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; /// @title LocalSealedBidBatchAuction /// @notice A completely on-chain sealed bid batch auction that uses RSA encryption to hide bids until after the auction ends @@ -13,7 +17,7 @@ import {MaxPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxP /// 2. Decryption - anyone with the private key can decrypt bids off-chain and submit them on-chain for validation and sorting /// 3. Settlement - once all bids are decryped, the auction can be settled and proceeds transferred contract LocalSealedBidBatchAuction is AuctionModule { - using MaxPriorityQueue for MaxPriorityQueue.Queue; + using MaxPriorityQueue for Queue; // ========== ERRORS ========== // error Auction_BidDoesNotExist(); @@ -107,7 +111,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; - mapping(uint96 lotId => MaxPriorityQueue.Queue) public lotSortedBids; + mapping(uint96 lotId => Queue) public lotSortedBids; // ========== SETUP ========== // @@ -442,13 +446,14 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint256 capacity = lotData[lotId_].capacity; // Iterate over bid queue to calculate the marginal clearing price of the auction - MaxPriorityQueue.Queue storage queue = lotSortedBids[lotId_]; + Queue storage queue = lotSortedBids[lotId_]; + uint256 numBids = queue.getNumBids(); uint256 marginalPrice; uint256 totalAmountIn; - uint256 winningBidIndex; - for (uint256 i = 1; i <= queue.numBids; i++) { + uint256 numWinningBids; + for (uint256 i = 0; i < numBids; i++) { // Load bid - QueueBid storage qBid = queue.getBid(i); + QueueBid storage qBid = queue.getBid(uint96(i)); // Calculate bid price uint256 price = (qBid.amountIn * _SCALE) / qBid.minAmountOut; @@ -462,19 +467,19 @@ contract LocalSealedBidBatchAuction is AuctionModule { // If total capacity expended is greater than or equal to the capacity, we have found the marginal price if (expended >= capacity) { marginalPrice = price; - winningBidIndex = i; + numWinningBids = i + 1; break; } // If we have reached the end of the queue, we have found the marginal price and the maximum capacity that can be filled - if (i == queue.numBids) { + if (i == numBids - 1) { // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) if (expended < auctionData[lotId_].minFilled) { auctionData[lotId_].status = AuctionStatus.Settled; return (winningBids_, bytes("")); } else { marginalPrice = price; - winningBidIndex = i; + numWinningBids = numBids; } } } @@ -488,10 +493,10 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Auction can be settled at the marginal price if we reach this point // Create winning bid array using marginal price to set amounts out - winningBids_ = new Bid[](winningBidIndex); - for (uint256 i; i < winningBidIndex; i++) { + winningBids_ = new Bid[](numWinningBids); + for (uint256 i; i < numWinningBids; i++) { // Load bid - QueueBid memory qBid = queue.delMax(); + QueueBid memory qBid = queue.popMax(); // Calculate amount out // TODO handle partial filling of the last winning bid @@ -562,8 +567,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { data.minBidSize = (lot_.capacity * implParams.minBidPercent) / _ONE_HUNDRED_PERCENT; data.publicKeyModulus = implParams.publicKeyModulus; - // Initialize sorted bid queue - lotSortedBids[lotId_].initialize(); + // // Initialize sorted bid queue + // lotSortedBids[lotId_].initialize(); // This auction type requires pre-funding return (true); @@ -608,7 +613,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { } function getSortedBidCount(uint96 lotId_) public view returns (uint256) { - return lotSortedBids[lotId_].numBids; + return lotSortedBids[lotId_].getNumBids(); } // =========== ATOMIC AUCTION STUBS ========== // diff --git a/src/modules/auctions/LSBBA/MaxPriorityQueue.sol b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol index c0eca0c9..69440242 100644 --- a/src/modules/auctions/LSBBA/MaxPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol @@ -2,129 +2,92 @@ pragma solidity ^0.8.0; struct Bid { - uint96 queueId; // ID representing order of insertion uint96 bidId; // ID of encrypted bid to reference on settlement uint256 amountIn; uint256 minAmountOut; } -/// @notice a max priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MaxPQ.java.html -/// @notice adapted from FrankieIsLost's implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MaxPriorityQueue.sol -/// @author FrankieIsLost -/// @author Oighty (edits) -/// Bids in descending order -library MaxPriorityQueue { - struct Queue { - ///@notice incrementing bid id - uint96 nextBidId; - ///@notice array backing priority queue - uint96[] queueIdList; - ///@notice total number of bids in queue - uint96 numBids; - //@notice map bid ids to bids - mapping(uint96 => Bid) queueIdToBidMap; - } - - ///@notice initialize must be called before using queue. - function initialize(Queue storage self) public { - self.queueIdList.push(0); - self.nextBidId = 1; - } +struct Queue { + bool flag; // need a non-dynamic field to avoid recursive type error, this is never used + uint96[] sortedIds; + mapping(uint96 => Bid) bids; +} - function isEmpty(Queue storage self) public view returns (bool) { - return self.numBids == 0; - } +/// @notice This library implements a max priority queue using an ordered array. +/// @dev Insert operations are less efficient than a heap implementation, but +/// the queue is sorted after an insert and can be inspected in place. +/// Binary heap implementations only guarantee the top element is sorted. +library MaxPriorityQueue { + // ========== INSERTION ========== // - function getNumBids(Queue storage self) public view returns (uint256) { - return self.numBids; + function insert( + Queue storage self, + uint96 bidId_, + uint256 amountIn_, + uint256 minAmountOut_ + ) public { + Bid memory bid = Bid(bidId_, amountIn_, minAmountOut_); + self.bids[bidId_] = bid; + uint256 n = self.sortedIds.length; + self.sortedIds.push(bidId_); + while (n > 0 && isLessThan(self, n, n - 1)) { + uint96 temp = self.sortedIds[n]; + self.sortedIds[n] = self.sortedIds[n - 1]; + self.sortedIds[n - 1] = temp; + n--; + } } - ///@notice view max bid - function getMax(Queue storage self) public view returns (Bid storage) { - require(!isEmpty(self), "nothing to return"); - uint96 maxId = self.queueIdList[1]; - return self.queueIdToBidMap[maxId]; - } + // ========== REMOVAL ========== // - ///@notice view bid by index in ascending order - function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { - require(!isEmpty(self), "nothing to return"); - require(index <= self.numBids, "bid does not exist"); - require(index > 0, "cannot use 0 index"); - return self.queueIdToBidMap[self.queueIdList[index]]; + /// @notice Remove the max bid from the queue and return it. + function popMax(Queue storage self) public returns (Bid memory) { + uint96 maxId = self.sortedIds[self.sortedIds.length - 1]; + Bid memory maxBid = self.bids[maxId]; + delete self.bids[maxId]; + self.sortedIds.pop(); + return maxBid; } - ///@notice move bid up heap - function swim(Queue storage self, uint96 k) private { - while (k > 1 && isLess(self, k / 2, k)) { - exchange(self, k, k / 2); - k = k / 2; - } - } + // ========== INSPECTION ========== // - ///@notice move bid down heap - function sink(Queue storage self, uint96 k) private { - while (2 * k <= self.numBids) { - uint96 j = 2 * k; - if (j < self.numBids && isLess(self, j, j + 1)) { - j++; - } - if (!isLess(self, k, j)) { - break; - } - exchange(self, k, j); - k = j; - } + /// @notice Return the max bid from the queue without removing it. + function getMax(Queue storage self) public view returns (Bid memory) { + uint96 maxId = self.sortedIds[self.sortedIds.length - 1]; + return self.bids[maxId]; } - ///@notice insert bid in heap - function insert( - Queue storage self, - uint96 bidId, - uint256 amountIn, - uint256 minAmountOut - ) public { - insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); + /// @notice Return the number of bids in the queue. + function getNumBids(Queue storage self) public view returns (uint256) { + return self.sortedIds.length; } - ///@notice insert bid in heap - function insert(Queue storage self, Bid memory bid) private { - self.queueIdList.push(bid.queueId); - self.queueIdToBidMap[bid.queueId] = bid; - self.numBids += 1; - swim(self, self.numBids); + /// @notice Return the bid at the given priority, zero indexed. + function getBid(Queue storage self, uint96 priority) public view returns (Bid storage) { + uint96 maxIndex = uint96(self.sortedIds.length - 1); + require(priority <= maxIndex, "bid does not exist"); + uint96 index = maxIndex - priority; + uint96 bidId = self.sortedIds[index]; + return self.bids[bidId]; } - ///@notice delete max bid from heap and return - function delMax(Queue storage self) public returns (Bid memory) { - require(!isEmpty(self), "nothing to delete"); - Bid memory max = self.queueIdToBidMap[self.queueIdList[1]]; - exchange(self, 1, self.numBids--); - self.queueIdList.pop(); - delete self.queueIdToBidMap[max.queueId]; - sink(self, 1); - return max; - } + // ========= UTILITIES ========= // - ///@notice helper function to determine ordering. When two bids have the same price, give priority - ///to the lower bid ID (inserted earlier) - function isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { - uint96 iId = self.queueIdList[i]; - uint96 jId = self.queueIdList[j]; - Bid memory bidI = self.queueIdToBidMap[iId]; - Bid memory bidJ = self.queueIdToBidMap[jId]; - uint256 relI = bidI.amountIn * bidJ.minAmountOut; - uint256 relJ = bidJ.amountIn * bidI.minAmountOut; - if (relI == relJ) { - return bidI.bidId < bidJ.bidId; + function isLessThan( + Queue storage self, + uint256 alpha, + uint256 beta + ) private view returns (bool) { + uint96 alphaId = self.sortedIds[alpha]; + uint96 betaId = self.sortedIds[beta]; + Bid memory a = self.bids[alphaId]; + Bid memory b = self.bids[betaId]; + uint256 relA = a.amountIn * b.minAmountOut; + uint256 relB = b.amountIn * a.minAmountOut; + if (relA == relB) { + return alphaId < betaId; + } else { + return relA < relB; } - return relI < relJ; - } - - ///@notice helper function to exchange to bids in the heap - function exchange(Queue storage self, uint256 i, uint256 j) private { - uint96 tempId = self.queueIdList[i]; - self.queueIdList[i] = self.queueIdList[j]; - self.queueIdList[j] = tempId; } } diff --git a/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol b/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol new file mode 100644 index 00000000..230cb27d --- /dev/null +++ b/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol @@ -0,0 +1,130 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +struct Bid { + uint96 queueId; // ID representing order of insertion + uint96 bidId; // ID of encrypted bid to reference on settlement + uint256 amountIn; + uint256 minAmountOut; +} + +/// @notice a max priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MaxPQ.java.html +/// @notice adapted from FrankieIsLost's min priority queue implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MinPriorityQueue.sol +/// @author FrankieIsLost +/// @author Oighty (edits) +/// Bids in descending order +library MaxPriorityQueue { + struct Queue { + ///@notice incrementing bid id + uint96 nextBidId; + ///@notice array backing priority queue + uint96[] queueIdList; + ///@notice total number of bids in queue + uint96 numBids; + //@notice map bid ids to bids + mapping(uint96 => Bid) queueIdToBidMap; + } + + ///@notice initialize must be called before using queue. + function initialize(Queue storage self) public { + self.queueIdList.push(0); + self.nextBidId = 1; + } + + function isEmpty(Queue storage self) public view returns (bool) { + return self.numBids == 0; + } + + function getNumBids(Queue storage self) public view returns (uint256) { + return self.numBids; + } + + ///@notice view max bid + function getMax(Queue storage self) public view returns (Bid storage) { + require(!isEmpty(self), "nothing to return"); + uint96 maxId = self.queueIdList[1]; + return self.queueIdToBidMap[maxId]; + } + + ///@notice view bid by index in ascending order + function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { + require(!isEmpty(self), "nothing to return"); + require(index <= self.numBids, "bid does not exist"); + require(index > 0, "cannot use 0 index"); + return self.queueIdToBidMap[self.queueIdList[index]]; + } + + ///@notice move bid up heap + function swim(Queue storage self, uint96 k) private { + while (k > 1 && isLess(self, k / 2, k)) { + exchange(self, k, k / 2); + k = k / 2; + } + } + + ///@notice move bid down heap + function sink(Queue storage self, uint96 k) private { + while (2 * k <= self.numBids) { + uint96 j = 2 * k; + if (j < self.numBids && isLess(self, j, j + 1)) { + j++; + } + if (!isLess(self, k, j)) { + break; + } + exchange(self, k, j); + k = j; + } + } + + ///@notice insert bid in heap + function insert( + Queue storage self, + uint96 bidId, + uint256 amountIn, + uint256 minAmountOut + ) public { + insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); + } + + ///@notice insert bid in heap + function insert(Queue storage self, Bid memory bid) private { + self.queueIdList.push(bid.queueId); + self.queueIdToBidMap[bid.queueId] = bid; + self.numBids += 1; + swim(self, self.numBids); + } + + ///@notice delete max bid from heap and return + function delMax(Queue storage self) public returns (Bid memory) { + require(!isEmpty(self), "nothing to delete"); + Bid memory max = self.queueIdToBidMap[self.queueIdList[1]]; + exchange(self, 1, self.numBids--); + self.queueIdList.pop(); + delete self.queueIdToBidMap[max.queueId]; + sink(self, 1); + return max; + } + + ///@notice helper function to determine ordering. When two bids have the same price, give priority + ///to the lower bid ID (inserted earlier) + function isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { + uint96 iId = self.queueIdList[i]; + uint96 jId = self.queueIdList[j]; + Bid memory bidI = self.queueIdToBidMap[iId]; + Bid memory bidJ = self.queueIdToBidMap[jId]; + uint256 relI = bidI.amountIn * bidJ.minAmountOut; + uint256 relJ = bidJ.amountIn * bidI.minAmountOut; + if (relI == relJ) { + return bidI.bidId < bidJ.bidId; + } + return relI < relJ; + } + + ///@notice helper function to exchange to bids in the heap + function exchange(Queue storage self, uint256 i, uint256 j) private { + uint96 tempId = self.queueIdList[i]; + self.queueIdList[i] = self.queueIdList[j]; + self.queueIdList[j] = tempId; + } +} diff --git a/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol b/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol index fbd9b976..962e548d 100644 --- a/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol +++ b/test/modules/auctions/LSBBA/MaxPriorityQueue.t.sol @@ -5,66 +5,53 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {MaxPriorityQueue, Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; +import { + MaxPriorityQueue, + Queue, + Bid as QueueBid +} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; contract MaxPriorityQueueTest is Test { - using MaxPriorityQueue for MaxPriorityQueue.Queue; + using MaxPriorityQueue for Queue; - MaxPriorityQueue.Queue queue; + Queue queue; /// @notice The initial index of the queue - uint96 internal immutable _INITIAL_INDEX = 1; + uint96 internal immutable _INITIAL_INDEX = 0; function setUp() public { - MaxPriorityQueue.initialize(queue); + // MaxPriorityQueue.initialize(queue); } // Sorted by Bid.amountIn / Bid.minAmountOut - // [X] initial values // [X] when a single bid is added // [X] when a larger bid is added // [X] it sorts in ascending order // [X] when a bid is added in the middle // [X] it adds it to the middle - function test_initialize() public { - assertEq(queue.nextBidId, 1); - assertEq(queue.queueIdList.length, 1); - assertEq(queue.queueIdList[0], 0); - assertEq(queue.numBids, 0); - - // empty - assertTrue(queue.isEmpty()); - } - function test_singleBid() public { queue.insert(0, 1, 1); // Price = 1/1 // get bid QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 1, "1: queueId mismatch"); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); assertEq(bid.minAmountOut, 1); // get max bid bid = queue.getMax(); - assertEq(bid.queueId, 1); assertEq(bid.bidId, 0); assertEq(bid.amountIn, 1); assertEq(bid.minAmountOut, 1); // numBids incremented - assertEq(queue.numBids, 1); - - // not empty - assertFalse(queue.isEmpty()); + assertEq(queue.getNumBids(), 1); - // queueIdList - assertEq(queue.queueIdList.length, 2); - assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 1); + // id list + assertEq(queue.sortedIds.length, 1); + assertEq(queue.sortedIds[0], 0); } function test_addLargerBid() public { @@ -76,36 +63,29 @@ contract MaxPriorityQueueTest is Test { // get first sorted bid (bid id = 1) QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 2, "1: queueId mismatch"); assertEq(bid.bidId, 1, "1: bidId mismatch"); assertEq(bid.amountIn, 4, "1: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "1: minAmountOut mismatch"); // get second sorted bid (bid id = 0) bid = queue.getBid(_INITIAL_INDEX + 1); - assertEq(bid.queueId, 1, "2: queueId mismatch"); assertEq(bid.bidId, 0, "2: bidId mismatch"); assertEq(bid.amountIn, 1, "2: amountIn mismatch"); assertEq(bid.minAmountOut, 1, "2: minAmountOut mismatch"); // get max bid (bid id = 0) bid = queue.getMax(); - assertEq(bid.queueId, 1, "min: queueId mismatch"); - assertEq(bid.bidId, 0); - assertEq(bid.amountIn, 1); - assertEq(bid.minAmountOut, 1); + assertEq(bid.bidId, 1); + assertEq(bid.amountIn, 4); + assertEq(bid.minAmountOut, 2); // numBids incremented - assertEq(queue.numBids, 2); + assertEq(queue.getNumBids(), 2); - // not empty - assertFalse(queue.isEmpty()); - - // queueIdList - assertEq(queue.queueIdList.length, 3); - assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 2); - assertEq(queue.queueIdList[2], 1); + // sorted ids (reverse order) + assertEq(queue.sortedIds.length, 2); + assertEq(queue.sortedIds[0], 0); + assertEq(queue.sortedIds[1], 1); } function test_addSmallerBid() public { @@ -121,7 +101,6 @@ contract MaxPriorityQueueTest is Test { // get first sorted bid (bid id = 1) console2.log("getBid(1)"); QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 2, "index 1: queueId mismatch"); assertEq(bid.bidId, 1, "index 1: bidId mismatch"); assertEq(bid.amountIn, 4, "index 1: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 1: minAmountOut mismatch"); @@ -129,7 +108,6 @@ contract MaxPriorityQueueTest is Test { // get second sorted bid (bid id = 2) console2.log("getBid(2)"); bid = queue.getBid(_INITIAL_INDEX + 1); - assertEq(bid.queueId, 3, "index 2: queueId mismatch"); assertEq(bid.bidId, 2, "index 2: bidId mismatch"); assertEq(bid.amountIn, 3, "index 2: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); @@ -137,7 +115,6 @@ contract MaxPriorityQueueTest is Test { // get third sorted bid (bid id = 0) console2.log("getBid(3)"); bid = queue.getBid(_INITIAL_INDEX + 2); - assertEq(bid.queueId, 1, "index 3: queueId mismatch"); assertEq(bid.bidId, 0, "index 3: bidId mismatch"); assertEq(bid.amountIn, 1, "index 3: amountIn mismatch"); assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); @@ -145,24 +122,19 @@ contract MaxPriorityQueueTest is Test { // get max bid (bid id = 0) console2.log("getMax()"); bid = queue.getMax(); - assertEq(bid.queueId, 1); - assertEq(bid.bidId, 0); - assertEq(bid.amountIn, 1); - assertEq(bid.minAmountOut, 1); + assertEq(bid.bidId, 1); + assertEq(bid.amountIn, 4); + assertEq(bid.minAmountOut, 2); // numBids incremented - assertEq(queue.numBids, 3); - - // not empty - assertFalse(queue.isEmpty()); - - // queueIdList - console2.log("queueIdList"); - assertEq(queue.queueIdList.length, 4); - assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 2); - assertEq(queue.queueIdList[2], 3); - assertEq(queue.queueIdList[3], 1); + assertEq(queue.getNumBids(), 3); + + // sorted ids (reverse order) + console2.log("sorted id list"); + assertEq(queue.sortedIds.length, 3); + assertEq(queue.sortedIds[0], 0); + assertEq(queue.sortedIds[1], 2); + assertEq(queue.sortedIds[2], 1); } function test_fourItems() public { @@ -178,59 +150,35 @@ contract MaxPriorityQueueTest is Test { // Add a fourth bid that is smaller than the first bid queue.insert(3, 1, 2); // queueId = 4, price = 1/2 - // get first sorted bid (bid id = 1) - console2.log("getBid(1)"); - QueueBid memory bid = queue.getBid(_INITIAL_INDEX); - assertEq(bid.queueId, 2, "index 1: queueId mismatch"); + // numBids incremented + assertEq(queue.getNumBids(), 4); + + // get first sorted bid + QueueBid memory bid = queue.popMax(); + console2.log(bid.bidId); assertEq(bid.bidId, 1, "index 1: bidId mismatch"); assertEq(bid.amountIn, 4, "index 1: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 1: minAmountOut mismatch"); // get second sorted bid (bid id = 2) - console2.log("getBid(2)"); - bid = queue.getBid(_INITIAL_INDEX + 1); - assertEq(bid.queueId, 3, "index 2: queueId mismatch"); + bid = queue.popMax(); + console2.log(bid.bidId); assertEq(bid.bidId, 2, "index 2: bidId mismatch"); assertEq(bid.amountIn, 3, "index 2: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 2: minAmountOut mismatch"); // get third sorted bid (bid id = 0) - console2.log("getBid(3)"); - bid = queue.getBid(_INITIAL_INDEX + 2); - assertEq(bid.queueId, 1, "index 3: queueId mismatch"); + bid = queue.popMax(); + console2.log(bid.bidId); assertEq(bid.bidId, 0, "index 3: bidId mismatch"); assertEq(bid.amountIn, 1, "index 3: amountIn mismatch"); assertEq(bid.minAmountOut, 1, "index 3: minAmountOut mismatch"); // get fourth sorted bid (bid id = 3) - console2.log("getBid(4)"); - bid = queue.getBid(_INITIAL_INDEX + 3); - assertEq(bid.queueId, 4, "index 4: queueId mismatch"); + bid = queue.popMax(); + console2.log(bid.bidId); assertEq(bid.bidId, 3, "index 4: bidId mismatch"); assertEq(bid.amountIn, 1, "index 4: amountIn mismatch"); assertEq(bid.minAmountOut, 2, "index 4: minAmountOut mismatch"); - - // get max bid (bid id = 3) - console2.log("getMax()"); - bid = queue.getMax(); - assertEq(bid.queueId, 4); - assertEq(bid.bidId, 3); - assertEq(bid.amountIn, 1); - assertEq(bid.minAmountOut, 2); - - // numBids incremented - assertEq(queue.numBids, 3); - - // not empty - assertFalse(queue.isEmpty()); - - // queueIdList - console2.log("queueIdList"); - assertEq(queue.queueIdList.length, 4); - assertEq(queue.queueIdList[0], 0); - assertEq(queue.queueIdList[1], 2); - assertEq(queue.queueIdList[2], 3); - assertEq(queue.queueIdList[3], 1); - assertEq(queue.queueIdList[4], 4); } } diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol index cceb18da..23ba1bdf 100644 --- a/test/modules/auctions/LSBBA/auction.t.sol +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -289,9 +289,5 @@ contract LSBBACreateAuctionTest is Test, Permit2User { ); assertEq(lotData.publicKeyModulus, auctionDataParams.publicKeyModulus); assertEq(uint8(lotData.status), uint8(LocalSealedBidBatchAuction.AuctionStatus.Created)); - - // Check that the sorted bid queue is initialised - (uint96 nextBidId_,) = auctionModule.lotSortedBids(lotId); - assertEq(nextBidId_, 1); } } diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index 2bd55108..d6e96f40 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -383,14 +383,12 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.queueId, 2); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); assertEq(sortedBidOne.bidId, bidTwo); assertEq(sortedBidOne.amountIn, bidTwoAmount); assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidTwo.queueId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); assertEq(sortedBidTwo.bidId, bidThree); assertEq(sortedBidTwo.amountIn, bidThreeAmount); assertEq(sortedBidTwo.minAmountOut, bidThreeAmountOut); @@ -427,8 +425,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.queueId, 1); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); assertEq(sortedBidOne.bidId, bidOne); assertEq(sortedBidOne.amountIn, bidOneAmount); assertEq(sortedBidOne.minAmountOut, bidOneAmountOut); @@ -473,20 +470,17 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.queueId, 2); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); assertEq(sortedBidOne.bidId, bidTwo); assertEq(sortedBidOne.amountIn, bidTwoAmount); assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidTwo.queueId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); assertEq(sortedBidTwo.bidId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); - QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); - assertEq(sortedBidThree.queueId, 3); + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); assertEq(sortedBidThree.bidId, bidThree); assertEq(sortedBidThree.amountIn, bidThreeAmount); assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); @@ -519,20 +513,17 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { ); // Check sorted bids - QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 1); - assertEq(sortedBidOne.queueId, 2); + QueueBid memory sortedBidOne = auctionModule.getSortedBidData(lotId, 0); assertEq(sortedBidOne.bidId, bidTwo); assertEq(sortedBidOne.amountIn, bidTwoAmount); assertEq(sortedBidOne.minAmountOut, bidTwoAmountOut); - QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 2); - assertEq(sortedBidTwo.queueId, 1); + QueueBid memory sortedBidTwo = auctionModule.getSortedBidData(lotId, 1); assertEq(sortedBidTwo.bidId, bidOne); assertEq(sortedBidTwo.amountIn, bidOneAmount); assertEq(sortedBidTwo.minAmountOut, bidOneAmountOut); - QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 3); - assertEq(sortedBidThree.queueId, 3); + QueueBid memory sortedBidThree = auctionModule.getSortedBidData(lotId, 2); assertEq(sortedBidThree.bidId, bidThree); assertEq(sortedBidThree.amountIn, bidThreeAmount); assertEq(sortedBidThree.minAmountOut, bidThreeAmountOut); diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index cf76e0a8..371b0c43 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -351,13 +351,15 @@ contract LSBBASettleTest is Test, Permit2User { // Calculate the marginal price uint256 marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + bidThreeAmountOut = bidThreeAmount * 1e18 / marginalPrice; + // First bid - largest amount out assertEq(winningBids[0].amount, bidThreeAmount); - assertEq(winningBids[0].minAmountOut, marginalPrice); + assertEq(winningBids[0].minAmountOut, bidThreeAmountOut); // Second bid assertEq(winningBids[1].amount, bidTwoAmount); - assertEq(winningBids[1].minAmountOut, marginalPrice); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut); // Expect winning bids assertEq(winningBids.length, 2); @@ -376,13 +378,15 @@ contract LSBBASettleTest is Test, Permit2User { // Calculate the marginal price uint256 marginalPrice = bidOneAmount * 1e18 / bidOneAmountOut; + bidTwoAmountOut = bidTwoAmount * 1e18 / marginalPrice; + // First bid - largest amount out assertEq(winningBids[0].amount, bidTwoAmount); - assertEq(winningBids[0].minAmountOut, marginalPrice); + assertEq(winningBids[0].minAmountOut, bidTwoAmountOut); // Second bid assertEq(winningBids[1].amount, bidOneAmount); - assertEq(winningBids[1].minAmountOut, marginalPrice); + assertEq(winningBids[1].minAmountOut, bidOneAmountOut); // Expect winning bids assertEq(winningBids.length, 2); From d62aa3729688cf292f50ae2f51108463d2e9b9be Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 11:47:03 +0400 Subject: [PATCH 094/117] Add test for partial fill --- test/modules/auctions/LSBBA/settle.t.sol | 52 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index 371b0c43..b2986bd5 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -38,7 +38,6 @@ contract LSBBASettleTest is Test, Permit2User { bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) ); - // TODO adjust these uint256 internal bidSeed = 1e9; uint96 internal bidOne; uint256 internal bidOneAmount = 2e18; @@ -56,6 +55,10 @@ contract LSBBASettleTest is Test, Permit2User { uint256 internal bidFourAmount = 2e18; uint256 internal bidFourAmountOut = 4e18; // Price < 1e18 (minimum price) LocalSealedBidBatchAuction.Decrypt internal decryptedBidFour; + uint96 internal bidFive; + uint256 internal bidFiveAmount = 6e18; + uint256 internal bidFiveAmountOut = 5e18; // Price = 1.2 + LocalSealedBidBatchAuction.Decrypt internal decryptedBidFive; LocalSealedBidBatchAuction.Decrypt[] internal decrypts; function setUp() public { @@ -174,6 +177,19 @@ contract LSBBASettleTest is Test, Permit2User { _; } + modifier whenLotIsOverSubscribedPartialFill() { + // 2 + 3 + 6 > 10 + (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); + (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); + (bidFive, decryptedBidFive) = _createBid(bidFiveAmount, bidFiveAmountOut); + + // Set up the decrypts array + decrypts.push(decryptedBidOne); + decrypts.push(decryptedBidTwo); + decrypts.push(decryptedBidFive); + _; + } + modifier whenMarginalPriceBelowMinimum() { // 2 + 2 > 2.5 // Marginal price of 2/4 = 0.5 < 1 @@ -338,7 +354,7 @@ contract LSBBASettleTest is Test, Permit2User { assertEq(winningBids.length, 0); } - function test_whenLotIsOverSubscribed_returnsWinningBids() + function test_whenLotIsOverSubscribed() public whenLotIsOverSubscribed whenLotHasConcluded @@ -365,6 +381,38 @@ contract LSBBASettleTest is Test, Permit2User { assertEq(winningBids.length, 2); } + function test_whenLotIsOverSubscribed_partialFill() + public + whenLotIsOverSubscribedPartialFill + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + + bidThreeAmountOut = bidThreeAmount * 1e18 / marginalPrice; + bidFiveAmountOut = bidFiveAmount * 1e18 / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); + assertEq(winningBids[0].minAmountOut, bidFiveAmountOut, "bid 1: minAmountOut mismatch"); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount, "bid 2: amount mismatch"); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut, "bid 2: minAmountOut mismatch"); + + // Third bid - will be a partial fill and recognised by the AuctionHouse + assertEq(winningBids[2].amount, bidOneAmount, "bid 3: amount mismatch"); + assertEq(winningBids[2].minAmountOut, bidOneAmountOut, "bid 3: minAmountOut mismatch"); + + // Expect winning bids + assertEq(winningBids.length, 3); + } + function test_whenLotIsFilled() public whenLotIsFilled From def3a85418af8cbe24fc578fe2a0b0c1465b3283 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 12:06:57 +0400 Subject: [PATCH 095/117] Function documentation. Tests for isLive() --- src/modules/Auction.sol | 38 ++++-- src/modules/auctions/LSBBA/LSBBA.sol | 2 +- test/modules/auctions/LSBBA/isLive.t.sol | 150 +++++++++++++++++++++++ 3 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 test/modules/auctions/LSBBA/isLive.t.sol diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 0cb353bd..4de77260 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -147,9 +147,6 @@ abstract contract Auction { // ========== AUCTION MANAGEMENT ========== // - // TODO NatSpec comments - // TODO validate function - /// @notice Create an auction lot /// /// @param lotId_ The lot id @@ -180,9 +177,22 @@ abstract contract Auction { function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); - function isLive(uint256 id_) public view virtual returns (bool); + /// @notice Determines if the lot is active + /// @dev The implementing function should handle the following: + /// - Return true if the lot is active + /// - Return false if the lot is inactive + /// + /// @param lotId_ The lot id + /// @return bool Whether or not the lot is active + function isLive(uint256 lotId_) public view virtual returns (bool); - function remainingCapacity(uint256 id_) external view virtual returns (uint256); + /// @notice Get the remaining capacity of a lot + /// @dev The implementing function should handle the following: + /// - Return the remaining capacity of the lot + /// + /// @param lotId_ The lot id + /// @return uint256 The remaining capacity of the lot + function remainingCapacity(uint256 lotId_) external view virtual returns (uint256); } abstract contract AuctionModule is Auction, Module { @@ -466,14 +476,24 @@ abstract contract AuctionModule is Auction, Module { // ========== AUCTION INFORMATION ========== // - // TODO does this need to change for batch auctions? - function isLive(uint256 id_) public view override returns (bool) { + /// @inheritdoc Auction + /// @dev A lot is active if: + /// - The lot has not concluded + /// - The lot has started + /// - The lot has not sold out or been cancelled (capacity > 0) + /// + /// Inheriting contracts should override this to implement auction-specific logic + /// + /// @param lotId_ The lot ID + /// @return bool Whether or not the lot is active + function isLive(uint256 lotId_) public view override returns (bool) { return ( - lotData[id_].capacity != 0 && lotData[id_].conclusion > uint48(block.timestamp) - && lotData[id_].start <= uint48(block.timestamp) + lotData[lotId_].capacity != 0 && lotData[lotId_].conclusion > uint48(block.timestamp) + && lotData[lotId_].start <= uint48(block.timestamp) ); } + /// @inheritdoc Auction function remainingCapacity(uint256 id_) external view override returns (uint256) { return lotData[id_].capacity; } diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 48259fc8..42305563 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -107,7 +107,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint24 internal constant _MIN_BID_PERCENT = 1000; // 1% uint24 internal constant _PUB_KEY_EXPONENT = 65_537; - uint256 internal constant _SCALE = 1e18; // TODO maybe set this per auction if decimals mess us up + uint256 internal constant _SCALE = 1e18; mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; diff --git a/test/modules/auctions/LSBBA/isLive.t.sol b/test/modules/auctions/LSBBA/isLive.t.sol new file mode 100644 index 00000000..7bac62d1 --- /dev/null +++ b/test/modules/auctions/LSBBA/isLive.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Tests +import {Test} from "forge-std/Test.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +import {Module, Veecode, WithModules} from "src/modules/Modules.sol"; + +// Auctions +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; +import {AuctionHouse} from "src/AuctionHouse.sol"; +import {Auction} from "src/modules/Auction.sol"; + +contract LSBBAIsLiveTest is Test, Permit2User { + address internal constant _PROTOCOL = address(0x1); + address internal constant alice = address(0x2); + address internal constant recipient = address(0x3); + address internal constant referrer = address(0x4); + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint256 internal constant LOT_CAPACITY = 10e18; + + uint48 internal lotStart; + uint48 internal lotDuration; + uint48 internal lotConclusion; + + // Function parameters + uint96 internal lotId = 1; + Auction.AuctionParams internal auctionParams; + LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + + function setUp() public { + // Ensure the block timestamp is a sane value + vm.warp(1_000_000); + + // Set up and install the auction module + auctionHouse = new AuctionHouse(_PROTOCOL, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set auction data parameters + auctionDataParams = LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, + minBidPercent: 1000, + minimumPrice: 1e18, + publicKeyModulus: new bytes(128) + }); + + // Set auction parameters + lotStart = uint48(block.timestamp) + 1; + lotDuration = uint48(1 days); + lotConclusion = lotStart + lotDuration; + + auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: lotDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + + // Create the auction + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + } + + // ===== Modifiers ===== // + + modifier whenLotIdIsInvalid() { + lotId = 2; + _; + } + + modifier givenLotHasStarted() { + vm.warp(lotStart + 1); + _; + } + + modifier givenLotIsCancelled() { + vm.prank(address(auctionHouse)); + auctionModule.cancelAuction(lotId); + _; + } + + modifier givenLotHasConcluded() { + vm.warp(lotConclusion + 1); + _; + } + + modifier givenLotHasDecrypted() { + // Decrypt the bids (none) + LocalSealedBidBatchAuction.Decrypt[] memory decrypts = + new LocalSealedBidBatchAuction.Decrypt[](0); + auctionModule.decryptAndSortBids(lotId, decrypts); + _; + } + + modifier givenLotHasSettled() { + // Call for settlement + vm.prank(address(auctionHouse)); + auctionModule.settle(lotId); + _; + } + + // ===== Tests ===== // + + // [X] false if lot id is invalid + // [X] false if lot has not started + // [X] false if lot has been cancelled + // [X] false if lot has concluded + // [X] false if lot has been decrypted + // [X] false if lot has been settled + // [X] true if lot is live + + function test_lotIdIsInvalid() public whenLotIdIsInvalid givenLotHasStarted { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotHasNotStarted() public { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotIsCancelled() public givenLotIsCancelled { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotHasConcluded() public givenLotHasConcluded { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotHasDecrypted() public givenLotHasConcluded givenLotHasDecrypted { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotHasSettled() + public + givenLotHasConcluded + givenLotHasDecrypted + givenLotHasSettled + { + assertEq(auctionModule.isLive(lotId), false); + } + + function test_lotIsActive() public givenLotHasStarted { + assertEq(auctionModule.isLive(lotId), true); + } +} From 7a9fb9746df758b213d011bfc3910a1fc7cd7b85 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 12:11:13 +0400 Subject: [PATCH 096/117] Remove remaining uint256 lotId references --- src/modules/Auction.sol | 20 +++++++++---------- src/modules/auctions/LSBBA/LSBBA.sol | 8 ++++---- .../Auction/MockAtomicAuctionModule.sol | 14 ++++++------- test/modules/Auction/MockAuctionModule.sol | 8 ++++---- .../Auction/MockBatchAuctionModule.sol | 10 +++++----- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 4de77260..6784edff 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -73,7 +73,7 @@ abstract contract Auction { uint48 internal constant _ONE_HUNDRED_PERCENT = 100_000; /// @notice General information pertaining to auction lots - mapping(uint256 id => Lot lot) public lotData; + mapping(uint96 id => Lot lot) public lotData; // ========== ATOMIC AUCTIONS ========== // @@ -169,13 +169,13 @@ abstract contract Auction { // ========== AUCTION INFORMATION ========== // - function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); + function payoutFor(uint96 lotId_, uint256 amount_) public view virtual returns (uint256); - function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); + function priceFor(uint96 lotId_, uint256 payout_) public view virtual returns (uint256); - function maxPayout(uint256 id_) public view virtual returns (uint256); + function maxPayout(uint96 lotId_) public view virtual returns (uint256); - function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); + function maxAmountAccepted(uint96 lotId_) public view virtual returns (uint256); /// @notice Determines if the lot is active /// @dev The implementing function should handle the following: @@ -184,7 +184,7 @@ abstract contract Auction { /// /// @param lotId_ The lot id /// @return bool Whether or not the lot is active - function isLive(uint256 lotId_) public view virtual returns (bool); + function isLive(uint96 lotId_) public view virtual returns (bool); /// @notice Get the remaining capacity of a lot /// @dev The implementing function should handle the following: @@ -192,7 +192,7 @@ abstract contract Auction { /// /// @param lotId_ The lot id /// @return uint256 The remaining capacity of the lot - function remainingCapacity(uint256 lotId_) external view virtual returns (uint256); + function remainingCapacity(uint96 lotId_) external view virtual returns (uint256); } abstract contract AuctionModule is Auction, Module { @@ -486,7 +486,7 @@ abstract contract AuctionModule is Auction, Module { /// /// @param lotId_ The lot ID /// @return bool Whether or not the lot is active - function isLive(uint256 lotId_) public view override returns (bool) { + function isLive(uint96 lotId_) public view override returns (bool) { return ( lotData[lotId_].capacity != 0 && lotData[lotId_].conclusion > uint48(block.timestamp) && lotData[lotId_].start <= uint48(block.timestamp) @@ -494,8 +494,8 @@ abstract contract AuctionModule is Auction, Module { } /// @inheritdoc Auction - function remainingCapacity(uint256 id_) external view override returns (uint256) { - return lotData[id_].capacity; + function remainingCapacity(uint96 lotId_) external view override returns (uint256) { + return lotData[lotId_].capacity; } /// @notice Get the lot data for a given lot ID diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 42305563..c8672142 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -587,18 +587,18 @@ contract LocalSealedBidBatchAuction is AuctionModule { } function payoutFor( - uint256 id_, + uint96 lotId_, uint256 amount_ ) public view virtual override returns (uint256) {} function priceFor( - uint256 id_, + uint96 lotId_, uint256 payout_ ) public view virtual override returns (uint256) {} - function maxPayout(uint256 id_) public view virtual override returns (uint256) {} + function maxPayout(uint96 lotId_) public view virtual override returns (uint256) {} - function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function maxAmountAccepted(uint96 lotId_) public view virtual override returns (uint256) {} function getLotData(uint96 lotId_) public view returns (AuctionData memory) { return auctionData[lotId_]; diff --git a/test/modules/Auction/MockAtomicAuctionModule.sol b/test/modules/Auction/MockAtomicAuctionModule.sol index af7c97ec..4b38b3ad 100644 --- a/test/modules/Auction/MockAtomicAuctionModule.sol +++ b/test/modules/Auction/MockAtomicAuctionModule.sol @@ -65,8 +65,8 @@ contract MockAtomicAuctionModule is AuctionModule { auctionOutput = abi.encode(output); } - function setPayoutMultiplier(uint256 id_, uint256 multiplier_) external virtual { - payoutData[id_] = multiplier_; + function setPayoutMultiplier(uint96 lotId_, uint256 multiplier_) external virtual { + payoutData[lotId_] = multiplier_; } function setPurchaseReverts(bool reverts_) external virtual { @@ -89,23 +89,23 @@ contract MockAtomicAuctionModule is AuctionModule { } function settle( - uint256 id_, + uint96 lotId_, Bid[] memory bids_ ) external virtual returns (uint256[] memory amountsOut) {} function payoutFor( - uint256 id_, + uint96 lotId_, uint256 amount_ ) public view virtual override returns (uint256) {} function priceFor( - uint256 id_, + uint96 lotId_, uint256 payout_ ) public view virtual override returns (uint256) {} - function maxPayout(uint256 id_) public view virtual override returns (uint256) {} + function maxPayout(uint96 lotId_) public view virtual override returns (uint256) {} - function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function maxAmountAccepted(uint96 lotId_) public view virtual override returns (uint256) {} function _settle(uint96) internal pure override returns (Bid[] memory, bytes memory) { revert Auction_NotImplemented(); diff --git a/test/modules/Auction/MockAuctionModule.sol b/test/modules/Auction/MockAuctionModule.sol index f7310a3a..c99db0f4 100644 --- a/test/modules/Auction/MockAuctionModule.sol +++ b/test/modules/Auction/MockAuctionModule.sol @@ -42,18 +42,18 @@ contract MockAuctionModule is AuctionModule { ) internal override returns (uint96) {} function payoutFor( - uint256 id_, + uint96 lotId_, uint256 amount_ ) public view virtual override returns (uint256) {} function priceFor( - uint256 id_, + uint96 lotId_, uint256 payout_ ) public view virtual override returns (uint256) {} - function maxPayout(uint256 id_) public view virtual override returns (uint256) {} + function maxPayout(uint96 lotId_) public view virtual override returns (uint256) {} - function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function maxAmountAccepted(uint96 lotId_) public view virtual override returns (uint256) {} function _settle(uint96 lotId_) internal diff --git a/test/modules/Auction/MockBatchAuctionModule.sol b/test/modules/Auction/MockBatchAuctionModule.sol index 84f527d4..ef315fd2 100644 --- a/test/modules/Auction/MockBatchAuctionModule.sol +++ b/test/modules/Auction/MockBatchAuctionModule.sol @@ -90,23 +90,23 @@ contract MockBatchAuctionModule is AuctionModule { } function settle( - uint256 id_, + uint96 lotId_, Bid[] memory bids_ ) external virtual returns (uint256[] memory amountsOut) {} function payoutFor( - uint256 id_, + uint96 lotId_, uint256 amount_ ) public view virtual override returns (uint256) {} function priceFor( - uint256 id_, + uint96 lotId_, uint256 payout_ ) public view virtual override returns (uint256) {} - function maxPayout(uint256 id_) public view virtual override returns (uint256) {} + function maxPayout(uint96 lotId_) public view virtual override returns (uint256) {} - function maxAmountAccepted(uint256 id_) public view virtual override returns (uint256) {} + function maxAmountAccepted(uint96 lotId_) public view virtual override returns (uint256) {} function _settle(uint96 lotId_) internal From cba176b38e86fecf2cf9ac68790c2619cf971398 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 12:14:06 +0400 Subject: [PATCH 097/117] chore: linting --- src/lib/RSA.sol | 4 +-- .../auctions/LSBBA/MaxPriorityQueue.sol | 4 +-- .../auctions/LSBBA/OldMaxPriorityQueue.sol | 28 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lib/RSA.sol b/src/lib/RSA.sol index a260e5fb..025e2930 100644 --- a/src/lib/RSA.sol +++ b/src/lib/RSA.sol @@ -202,7 +202,7 @@ library RSAOAEP { return modexp(encoded, e, n); } - function _mgf(bytes memory seed, uint256 maskLen) public pure returns (bytes memory) { + function _mgf(bytes memory seed, uint256 maskLen) private pure returns (bytes memory) { // Implements 8.2.1 MGF1 as defined in RFC8017: https://www.rfc-editor.org/rfc/rfc8017 // 1. Check that the mask length is not greater than 2^32 * hash length (32 bytes in this case) @@ -234,7 +234,7 @@ library RSAOAEP { return t; } - function _xor(bytes memory first, bytes memory second) public pure returns (bytes memory) { + function _xor(bytes memory first, bytes memory second) private pure returns (bytes memory) { uint256 fLen = first.length; uint256 sLen = second.length; if (fLen != sLen) revert("xor: different lengths"); diff --git a/src/modules/auctions/LSBBA/MaxPriorityQueue.sol b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol index 69440242..5497043f 100644 --- a/src/modules/auctions/LSBBA/MaxPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/MaxPriorityQueue.sol @@ -30,7 +30,7 @@ library MaxPriorityQueue { self.bids[bidId_] = bid; uint256 n = self.sortedIds.length; self.sortedIds.push(bidId_); - while (n > 0 && isLessThan(self, n, n - 1)) { + while (n > 0 && _isLessThan(self, n, n - 1)) { uint96 temp = self.sortedIds[n]; self.sortedIds[n] = self.sortedIds[n - 1]; self.sortedIds[n - 1] = temp; @@ -73,7 +73,7 @@ library MaxPriorityQueue { // ========= UTILITIES ========= // - function isLessThan( + function _isLessThan( Queue storage self, uint256 alpha, uint256 beta diff --git a/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol b/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol index 230cb27d..6ef67572 100644 --- a/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol +++ b/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol @@ -55,24 +55,24 @@ library MaxPriorityQueue { } ///@notice move bid up heap - function swim(Queue storage self, uint96 k) private { - while (k > 1 && isLess(self, k / 2, k)) { - exchange(self, k, k / 2); + function _swim(Queue storage self, uint96 k) private { + while (k > 1 && _isLess(self, k / 2, k)) { + _exchange(self, k, k / 2); k = k / 2; } } ///@notice move bid down heap - function sink(Queue storage self, uint96 k) private { + function _sink(Queue storage self, uint96 k) private { while (2 * k <= self.numBids) { uint96 j = 2 * k; - if (j < self.numBids && isLess(self, j, j + 1)) { + if (j < self.numBids && _isLess(self, j, j + 1)) { j++; } - if (!isLess(self, k, j)) { + if (!_isLess(self, k, j)) { break; } - exchange(self, k, j); + _exchange(self, k, j); k = j; } } @@ -84,31 +84,31 @@ library MaxPriorityQueue { uint256 amountIn, uint256 minAmountOut ) public { - insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); + _insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); } ///@notice insert bid in heap - function insert(Queue storage self, Bid memory bid) private { + function _insert(Queue storage self, Bid memory bid) private { self.queueIdList.push(bid.queueId); self.queueIdToBidMap[bid.queueId] = bid; self.numBids += 1; - swim(self, self.numBids); + _swim(self, self.numBids); } ///@notice delete max bid from heap and return function delMax(Queue storage self) public returns (Bid memory) { require(!isEmpty(self), "nothing to delete"); Bid memory max = self.queueIdToBidMap[self.queueIdList[1]]; - exchange(self, 1, self.numBids--); + _exchange(self, 1, self.numBids--); self.queueIdList.pop(); delete self.queueIdToBidMap[max.queueId]; - sink(self, 1); + _sink(self, 1); return max; } ///@notice helper function to determine ordering. When two bids have the same price, give priority ///to the lower bid ID (inserted earlier) - function isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { + function _isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { uint96 iId = self.queueIdList[i]; uint96 jId = self.queueIdList[j]; Bid memory bidI = self.queueIdToBidMap[iId]; @@ -122,7 +122,7 @@ library MaxPriorityQueue { } ///@notice helper function to exchange to bids in the heap - function exchange(Queue storage self, uint256 i, uint256 j) private { + function _exchange(Queue storage self, uint256 i, uint256 j) private { uint96 tempId = self.queueIdList[i]; self.queueIdList[i] = self.queueIdList[j]; self.queueIdList[j] = tempId; From 99dc9f0e91d30c2d6c2dea58c59734e07e279bff Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 16:41:38 +0400 Subject: [PATCH 098/117] Tests for settle() on AuctionHouse. partial fill remaining. --- src/AuctionHouse.sol | 61 ++- src/modules/auctions/LSBBA/LSBBA.sol | 7 +- test/AuctionHouse/settle.t.sol | 540 ++++++++++++++++++++++++++ test/modules/auctions/LSBBA/bid.t.sol | 18 +- 4 files changed, 601 insertions(+), 25 deletions(-) create mode 100644 test/AuctionHouse/settle.t.sol diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index eb569e75..14085878 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -19,6 +19,8 @@ import {Veecode, fromVeecode, WithModules} from "src/modules/Modules.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; import {IAllowlist} from "src/interfaces/IAllowlist.sol"; +import {console2} from "forge-std/console2.sol"; + // TODO define purpose abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized @@ -204,6 +206,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { ) internal returns (uint256 totalAmountIn, uint256 totalFees) { // Calculate fees for purchase uint256 bidCount = bids_.length; + uint256 totalProtocolFees; for (uint256 i; i < bidCount; i++) { // Calculate fees from bid amount (uint256 toReferrer, uint256 toProtocol) = @@ -213,6 +216,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { if (toReferrer > 0) { rewards[bids_[i].referrer][quoteToken_] += toReferrer; } + totalProtocolFees += toProtocol; totalFees += toReferrer + toProtocol; // Increment total amount in @@ -220,7 +224,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Update protocol fee if not zero - if (totalFees > 0) rewards[_PROTOCOL][quoteToken_] += totalFees; + if (totalProtocolFees > 0) rewards[_PROTOCOL][quoteToken_] += totalProtocolFees; } function _calculateFees( @@ -430,6 +434,11 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // 3. Minimum bid size is enforced // 4. Minimum capacity sold is enforced AuctionModule module = _getModuleForId(lotId_); + + // Store the capacity remaining before settling + uint256 remainingCapacity = module.remainingCapacity(lotId_); + + // Settle the auction (Auction.Bid[] memory winningBids, bytes memory auctionOutput) = module.settle(lotId_); // Load routing data for the lot @@ -445,11 +454,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Assumes that payment has already been collected for each bid - // Send payment in bulk to auction owner - _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); - - // TODO send last winning bidder partial refund if it is a partial fill. - // Collect payout in bulk from the auction owner { // Calculate amount out @@ -468,16 +472,47 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders { uint256 bidCount = winningBids.length; + uint256 payoutRemaining = remainingCapacity; for (uint256 i; i < bidCount; i++) { + console2.log("bidder", winningBids[i].bidder); + console2.log("minAmountOut", winningBids[i].minAmountOut); + console2.log("balance", routing.baseToken.balanceOf(address(this))); + console2.log("payoutRemaining", payoutRemaining); + + uint256 payoutAmount = winningBids[i].minAmountOut; + + // If the bid is the last and is a partial fill, then calculate the amount to send + if (i == bidCount - 1 && payoutAmount > payoutRemaining) { + payoutAmount = payoutRemaining; + } + // Send payout to each bidder - _sendPayout( - lotId_, - winningBids[i].bidder, - winningBids[i].minAmountOut, - routing, - auctionOutput - ); + _sendPayout(lotId_, winningBids[i].bidder, payoutAmount, routing, auctionOutput); + + // Calculate the refund amount for a partial fill + if (i == bidCount - 1 && winningBids[i].minAmountOut > payoutRemaining) { + uint256 payoutFulfilled = + 1e18 - payoutAmount * 1e18 / winningBids[i].minAmountOut; + + // Calculate the refund amount in terms of the quote token + console2.log("payoutFulfilled", payoutFulfilled); + console2.log("winningBids[i].amount", winningBids[i].amount); + uint256 refundAmount = winningBids[i].amount * payoutFulfilled / 1e18; + console2.log("refundAmount", refundAmount); + + // Send refund to last winning bidder + routing.quoteToken.safeTransfer(winningBids[i].bidder, refundAmount); + + // Reduce the total amount to return + totalAmountInLessFees -= refundAmount; + } + + // Decrement payout remaining + payoutRemaining -= payoutAmount; } + + // Send payment in bulk to auction owner + _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); } } diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index c8672142..ea301205 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -195,7 +195,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// /// This function reverts if: /// - The amount is less than the minimum bid size for the lot - /// - The amount is greater than the capacity function _bid( uint96 lotId_, address bidder_, @@ -208,8 +207,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Amount at least minimum bid size for lot if (amount_ < auctionData[lotId_].minBidSize) revert Auction_AmountLessThanMinimum(); - // Amount greater than capacity - if (amount_ > lotData[lotId_].capacity) revert Auction_NotEnoughCapacity(); + // Does not check that the bid amount (in terms of the quote token) is greater than the lot capacity (in terms of the base token), because they are different units // Store bid data // Auction data should just be the encrypted amount out (no decoding required) @@ -567,9 +565,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { data.minBidSize = (lot_.capacity * implParams.minBidPercent) / _ONE_HUNDRED_PERCENT; data.publicKeyModulus = implParams.publicKeyModulus; - // // Initialize sorted bid queue - // lotSortedBids[lotId_].initialize(); - // This auction type requires pre-funding return (true); } diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol new file mode 100644 index 00000000..3df097bd --- /dev/null +++ b/test/AuctionHouse/settle.t.sol @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; + +// Mocks +import {MockERC20} from "lib/solmate/src/test/utils/mocks/MockERC20.sol"; +import {Permit2User} from "test/lib/permit2/Permit2User.sol"; + +// Auctions +import {AuctionHouse, Router} from "src/AuctionHouse.sol"; +import {Auction, AuctionModule} from "src/modules/Auction.sol"; +import {IHooks, IAllowlist, Auctioneer} from "src/bases/Auctioneer.sol"; +import {RSAOAEP} from "src/lib/RSA.sol"; +import {LocalSealedBidBatchAuction} from "src/modules/auctions/LSBBA/LSBBA.sol"; + +// Modules +import { + Keycode, + toKeycode, + Veecode, + wrapVeecode, + unwrapVeecode, + fromVeecode, + WithModules, + Module +} from "src/modules/Modules.sol"; + +import {console2} from "forge-std/console2.sol"; + +contract SettleTest is Test, Permit2User { + MockERC20 internal baseToken; + MockERC20 internal quoteToken; + + AuctionHouse internal auctionHouse; + LocalSealedBidBatchAuction internal auctionModule; + + uint96 internal lotId; + uint48 internal lotStart; + uint48 internal auctionDuration = 1 days; + uint48 internal lotConclusion; + bytes internal constant PUBLIC_KEY_MODULUS = abi.encodePacked( + bytes32(0xB925394F570C7C765F121826DFC8A1661921923B33408EFF62DCAC0D263952FE), + bytes32(0x158C12B2B35525F7568CB8DC7731FBC3739F22D94CB80C5622E788DB4532BD8C), + bytes32(0x8643680DA8C00A5E7C967D9D087AA1380AE9A031AC292C971EC75F9BD3296AE1), + bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) + ); + + address internal immutable protocol = address(0x2); + address internal immutable referrer = address(0x4); + address internal immutable auctionOwner = address(0x5); + address internal immutable recipient = address(0x6); + address internal immutable bidderOne = address(0x7); + address internal immutable bidderTwo = address(0x8); + + uint48 internal constant protocolFee = 1000; // 1% + uint48 internal constant referrerFee = 500; // 0.5% + + uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal bidSeed = 1e9; + + uint256 internal constant bidOneAmount = 4e18; + uint256 internal constant bidOneAmountOut = 4e18; // Price = 1 + uint256 internal constant bidTwoAmount = 6e18; + uint256 internal constant bidTwoAmountOut = 6e18; // Price = 1 + uint256 internal constant bidThreeAmount = 7e18; + uint256 internal constant bidThreeAmountOut = 7e18; // Price = 1 + uint256 internal constant bidFourAmount = 8e18; + uint256 internal constant bidFourAmountOut = 2e18; // Price = 4 + uint256 internal constant bidFiveAmount = 8e18; + uint256 internal constant bidFiveAmountOut = 4e18; // Price = 2 + + function setUp() external { + // Set block timestamp + vm.warp(1_000_000); + + baseToken = new MockERC20("Base Token", "BASE", 18); + quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + + auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); + auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); + auctionHouse.installModule(auctionModule); + + // Set fees + auctionHouse.setProtocolFee(protocolFee); + auctionHouse.setReferrerFee(referrer, referrerFee); + + // Auction parameters + LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = + LocalSealedBidBatchAuction.AuctionDataParams({ + minFillPercent: 1000, // 1% + minBidPercent: 1000, // 1% + minimumPrice: 5e17, // 0.5e18 + publicKeyModulus: PUBLIC_KEY_MODULUS + }); + + lotStart = uint48(block.timestamp) + 1; + Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + start: lotStart, + duration: auctionDuration, + capacityInQuote: false, + capacity: LOT_CAPACITY, + implParams: abi.encode(auctionDataParams) + }); + lotConclusion = auctionParams.start + auctionParams.duration; + + (Keycode moduleKeycode,) = unwrapVeecode(auctionModule.VEECODE()); + Auctioneer.RoutingParams memory routingParams = Auctioneer.RoutingParams({ + auctionType: moduleKeycode, + baseToken: baseToken, + quoteToken: quoteToken, + hooks: IHooks(address(0)), + allowlist: IAllowlist(address(0)), + allowlistParams: abi.encode(""), + payoutData: abi.encode(""), + derivativeType: toKeycode(""), + derivativeParams: abi.encode("") + }); + + // Set up pre-funding + baseToken.mint(auctionOwner, LOT_CAPACITY); + vm.prank(auctionOwner); + baseToken.approve(address(auctionHouse), LOT_CAPACITY); + + // Create an auction lot + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + } + + function _createBid( + address bidder_, + uint256 bidAmount_, + uint256 bidAmountOut_ + ) internal returns (uint96 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid) { + // Encrypt the bid amount + decryptedBid = LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: bidSeed}); + bytes memory auctionData_ = _encrypt(decryptedBid); + + Router.BidParams memory bidParams = Router.BidParams({ + lotId: lotId, + recipient: recipient, + referrer: referrer, + amount: bidAmount_, + auctionData: auctionData_, + allowlistProof: bytes(""), + permit2Data: bytes("") + }); + + // Create a bid + vm.prank(bidder_); + uint96 bidId = auctionHouse.bid(bidParams); + + return (bidId, decryptedBid); + } + + function _encrypt(LocalSealedBidBatchAuction.Decrypt memory decrypt_) + internal + view + returns (bytes memory) + { + return RSAOAEP.encrypt( + abi.encodePacked(decrypt_.amountOut), + abi.encodePacked(lotId), + abi.encodePacked(uint24(65_537)), + PUBLIC_KEY_MODULUS, + decrypt_.seed + ); + } + + // ======== Modifiers ======== // + + LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; + uint256 internal marginalPrice; + + modifier givenLotHasSufficientBids() { + // Mint quote tokens to the bidders + quoteToken.mint(bidderOne, bidOneAmount); + quoteToken.mint(bidderTwo, bidTwoAmount); + + // Authorise spending + vm.prank(bidderOne); + quoteToken.approve(address(auctionHouse), bidOneAmount); + vm.prank(bidderTwo); + quoteToken.approve(address(auctionHouse), bidTwoAmount); + + // Create bids + // 4 + 6 = 10 + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidOne) = + _createBid(bidderOne, bidOneAmount, bidOneAmountOut); + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidTwo) = + _createBid(bidderTwo, bidTwoAmount, bidTwoAmountOut); + decryptedBids.push(decryptedBidOne); + decryptedBids.push(decryptedBidTwo); + + marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + _; + } + + modifier givenLotHasPartialFill() { + // Mint quote tokens to the bidders + quoteToken.mint(bidderOne, bidOneAmount); + quoteToken.mint(bidderTwo, bidThreeAmount); + + // Authorise spending + vm.prank(bidderOne); + quoteToken.approve(address(auctionHouse), bidOneAmount); + vm.prank(bidderTwo); + quoteToken.approve(address(auctionHouse), bidThreeAmount); + + // Create bids + // 4 + 7 = 11 (over-subscribed) + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidOne) = + _createBid(bidderOne, bidOneAmount, bidOneAmountOut); + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidThree) = + _createBid(bidderTwo, bidThreeAmount, bidThreeAmountOut); + decryptedBids.push(decryptedBidOne); + decryptedBids.push(decryptedBidThree); + + marginalPrice = bidThreeAmount * 1e18 / bidThreeAmountOut; + _; + } + + modifier givenLotHasSufficientBids_differentMarginalPrice() { + // Mint quote tokens to the bidders + quoteToken.mint(bidderOne, bidFourAmount); + quoteToken.mint(bidderTwo, bidFiveAmount); + + // Authorise spending + vm.prank(bidderOne); + quoteToken.approve(address(auctionHouse), bidFourAmount); + vm.prank(bidderTwo); + quoteToken.approve(address(auctionHouse), bidFiveAmount); + + // Create bids + // 4 + 2 = 6 + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidOne) = + _createBid(bidderOne, bidFourAmount, bidFourAmountOut); + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidTwo) = + _createBid(bidderTwo, bidFiveAmount, bidFiveAmountOut); + decryptedBids.push(decryptedBidOne); + decryptedBids.push(decryptedBidTwo); + + // bidFour first (price = 4), then bidFive (price = 3) + marginalPrice = bidFiveAmount * 1e18 / bidFiveAmountOut; + _; + } + + modifier givenLotIdIsInvalid() { + lotId = 255; + _; + } + + modifier givenLotHasStarted() { + // Warp to the start of the auction + vm.warp(lotStart); + _; + } + + modifier givenAuctionModuleReverts() { + // Cancel the auction + vm.prank(auctionOwner); + auctionHouse.cancel(lotId); + _; + } + + modifier givenLotHasConcluded() { + // Warp to the end of the auction + vm.warp(lotConclusion + 1); + _; + } + + modifier givenLotHasDecrypted() { + // Decrypt the bids + auctionModule.decryptAndSortBids(lotId, decryptedBids); + _; + } + + modifier givenAuctionHouseHasInsufficientQuoteTokenBalance() { + // Approve spending + vm.prank(address(auctionHouse)); + quoteToken.approve(address(this), quoteToken.balanceOf(address(auctionHouse))); + + // Burn quote tokens + quoteToken.burn(address(auctionHouse), quoteToken.balanceOf(address(auctionHouse))); + _; + } + + modifier givenAuctionHouseHasInsufficientBaseTokenBalance() { + // Approve spending + vm.prank(address(auctionHouse)); + baseToken.approve(address(this), baseToken.balanceOf(address(auctionHouse))); + + // Burn base tokens + baseToken.burn(address(auctionHouse), baseToken.balanceOf(address(auctionHouse))); + _; + } + + // ======== Tests ======== // + + // [X] when the lot id is invalid + // [X] it reverts + // [ ] when the caller is not authorized to settle + // [ ] it reverts + // [X] when the auction module reverts + // [X] it reverts + // [X] given the auction house has insufficient balance of the quote token + // [X] it reverts + // [X] given the auction house has insufficient balance of the base token + // [X] it reverts + // [X] given the last bidder has a partial fill + // [X] it succeeds - last bidder receives the partial fill and is returned excess quote tokens + // [X] given the auction bids have different prices + // [X] it succeeds + // [ ] given that the quote token decimals differ from the base token decimals + // [ ] it succeeds + // [X] it succeeds - auction owner receives quote tokens (minus fees), bidders receive base tokens and fees accrued + + function test_invalidLotId() external givenLotIdIsInvalid givenLotHasStarted { + // Expect revert + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidLotId.selector, lotId); + vm.expectRevert(err); + + // Attempt to settle the lot + auctionHouse.settle(lotId); + } + + function test_auctionModuleReverts_reverts() external givenAuctionModuleReverts { + // Expect revert + bytes memory err = + abi.encodeWithSelector(LocalSealedBidBatchAuction.Auction_WrongState.selector); + vm.expectRevert(err); + + // Attempt to settle the lot + auctionHouse.settle(lotId); + } + + function test_insufficientQuoteToken_reverts() + external + givenLotHasStarted + givenLotHasSufficientBids + givenLotHasConcluded + givenLotHasDecrypted + givenAuctionHouseHasInsufficientQuoteTokenBalance + { + // Expect revert + vm.expectRevert("TRANSFER_FAILED"); + + // Attempt to settle the lot + auctionHouse.settle(lotId); + } + + function test_insufficientBaseTokenBalance_reverts() + external + givenLotHasStarted + givenLotHasSufficientBids + givenLotHasConcluded + givenLotHasDecrypted + givenAuctionHouseHasInsufficientBaseTokenBalance + { + // Expect revert + vm.expectRevert("TRANSFER_FAILED"); + + // Attempt to settle the lot + auctionHouse.settle(lotId); + } + + function test_success() + external + givenLotHasStarted + givenLotHasSufficientBids + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidTwoAmountOut, + "bidderTwo: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmount + bidTwoAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + bidTwoAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + bidTwoAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_partialFill() + external + givenLotHasStarted + givenLotHasPartialFill + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidOneAmountOutActual = 3e18; + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOutActual, + "bidderOne: incorrect balance of base token" + ); // Received partial payout. 10 - 7 = 3 + assertEq( + baseToken.balanceOf(bidderTwo), + bidThreeAmountOut, + "bidderTwo: incorrect balance of base token" + ); + + // Check quote token balances + uint256 bidOnePercentageUnfilled = + (bidOneAmountOut - bidOneAmountOutActual) * 1e18 / bidOneAmountOut; + uint256 bidOneAmountActual = bidOneAmount * bidOnePercentageUnfilled / 1e18; + assertEq( + quoteToken.balanceOf(bidderOne), + bidOneAmountActual, + "bidderOne: incorrect balance of quote token" + ); // Remainder received as quote tokens. + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmountActual + bidThreeAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmountActual + bidThreeAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + bidThreeAmount - totalFeeAmount - bidOneAmountActual, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_marginalPrice() + external + givenLotHasStarted + givenLotHasSufficientBids_differentMarginalPrice + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidFourAmountOutActual = bidFourAmount * 1e18 / marginalPrice; + uint256 bidFiveAmountOutActual = bidFiveAmountOut; // since it set the marginal price + assertEq( + baseToken.balanceOf(bidderOne), + bidFourAmountOutActual, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidFiveAmountOutActual, + "bidderTwo: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidFourAmount + bidFiveAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidFourAmount + bidFiveAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidFourAmount + bidFiveAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } +} diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index 2bafb66a..0106c247 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -214,18 +214,24 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenAmountIsLargerThanCapacity_reverts() + function test_whenAmountIsLargerThanCapacity() public givenLotHasStarted whenAmountIsLargerThanCapacity { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_NotEnoughCapacity.selector); - vm.expectRevert(err); - // Call vm.prank(address(auctionHouse)); - auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + uint96 bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + + // Check values + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidId); + assertEq(encryptedBid.bidder, alice); + assertEq(encryptedBid.recipient, recipient); + assertEq(encryptedBid.referrer, referrer); + assertEq(encryptedBid.amount, bidAmount); + assertEq(encryptedBid.encryptedAmountOut, auctionData); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Submitted)); } function test_execOnModule_reverts() public { From b2b9c77f42423cdf7e883cb9921ce3823479400d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 17:26:46 +0400 Subject: [PATCH 099/117] Handle partial fills during settlement --- src/AuctionHouse.sol | 107 +++++++++++++++++++++------------ test/AuctionHouse/settle.t.sol | 13 ++-- 2 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 14085878..74526029 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -19,8 +19,6 @@ import {Veecode, fromVeecode, WithModules} from "src/modules/Modules.sol"; import {IHooks} from "src/interfaces/IHooks.sol"; import {IAllowlist} from "src/interfaces/IAllowlist.sol"; -import {console2} from "forge-std/console2.sol"; - // TODO define purpose abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized @@ -169,6 +167,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { error InvalidBidder(address bidder_); + error Broken_Invariant(); + // ========== EVENTS ========== // event Purchase(uint256 id, address buyer, address referrer, uint256 amount, uint256 payout); @@ -415,7 +415,17 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } /// @inheritdoc Router - /// @dev This function reverts if: + /// @dev This function handles the following: + /// - Settles the auction on the auction module + /// - Calculates the payout amount, taking partial fill into consideration + /// - Calculates the fees taken on the quote token + /// - Collects the payout from the auction owner (if necessary) + /// - Sends the payout to each bidder + /// - Sends the payment to the auction owner + /// - Sends the refund to the bidder if the last bid was a partial fill + /// - Refunds any unused base token to the auction owner + /// + /// This function reverts if: /// - the lot ID is invalid /// - the caller is not authorized to settle the auction /// - the auction module reverts when settling the auction @@ -444,6 +454,42 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Load routing data for the lot Routing memory routing = lotRouting[lotId_]; + // Calculate the payout amount, handling partial fills + uint256[] memory paymentRefunds = new uint256[](winningBids.length); + { + uint256 bidCount = winningBids.length; + uint256 payoutRemaining = remainingCapacity; + for (uint256 i; i < bidCount; i++) { + uint256 payoutAmount = winningBids[i].minAmountOut; + + // If the bid is the last and is a partial fill, then calculate the amount to send + if (i == bidCount - 1 && payoutAmount > payoutRemaining) { + // Amend the bid to the amount remaining + winningBids[i].minAmountOut = payoutRemaining; + + // Calculate the refund amount in terms of the quote token + uint256 payoutUnfulfilled = 1e18 - payoutRemaining * 1e18 / payoutAmount; + uint256 refundAmount = winningBids[i].amount * payoutUnfulfilled / 1e18; + paymentRefunds[i] = refundAmount; + + // Check that the refund amount is not greater than the bid amount + if (refundAmount > winningBids[i].amount) { + revert Broken_Invariant(); + } + + // Adjust the payment amount (otherwise fees will be charged) + winningBids[i].amount = winningBids[i].amount - refundAmount; + + // Decrement the remaining payout + payoutRemaining = 0; + break; + } else { + // Decrement the remaining payout + payoutRemaining -= payoutAmount; + } + } + } + // Calculate fees uint256 totalAmountInLessFees; { @@ -472,48 +518,33 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders { uint256 bidCount = winningBids.length; - uint256 payoutRemaining = remainingCapacity; for (uint256 i; i < bidCount; i++) { - console2.log("bidder", winningBids[i].bidder); - console2.log("minAmountOut", winningBids[i].minAmountOut); - console2.log("balance", routing.baseToken.balanceOf(address(this))); - console2.log("payoutRemaining", payoutRemaining); - - uint256 payoutAmount = winningBids[i].minAmountOut; - - // If the bid is the last and is a partial fill, then calculate the amount to send - if (i == bidCount - 1 && payoutAmount > payoutRemaining) { - payoutAmount = payoutRemaining; - } - // Send payout to each bidder - _sendPayout(lotId_, winningBids[i].bidder, payoutAmount, routing, auctionOutput); - - // Calculate the refund amount for a partial fill - if (i == bidCount - 1 && winningBids[i].minAmountOut > payoutRemaining) { - uint256 payoutFulfilled = - 1e18 - payoutAmount * 1e18 / winningBids[i].minAmountOut; - - // Calculate the refund amount in terms of the quote token - console2.log("payoutFulfilled", payoutFulfilled); - console2.log("winningBids[i].amount", winningBids[i].amount); - uint256 refundAmount = winningBids[i].amount * payoutFulfilled / 1e18; - console2.log("refundAmount", refundAmount); - - // Send refund to last winning bidder - routing.quoteToken.safeTransfer(winningBids[i].bidder, refundAmount); - - // Reduce the total amount to return - totalAmountInLessFees -= refundAmount; - } - - // Decrement payout remaining - payoutRemaining -= payoutAmount; + _sendPayout( + lotId_, + winningBids[i].bidder, + winningBids[i].minAmountOut, + routing, + auctionOutput + ); } // Send payment in bulk to auction owner _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); } + + // Handle the refund to the bidder is the last bid was a partial fill + { + uint256 bidCount = winningBids.length; + for (uint256 i; i < bidCount; i++) { + // Send refund to each bidder + if (paymentRefunds[i] > 0) { + routing.quoteToken.safeTransfer(winningBids[i].bidder, paymentRefunds[i]); + } + } + } + + // TODO payout refund } // ========== TOKEN TRANSFERS ========== // diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index 3df097bd..a7ebb897 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -28,8 +28,6 @@ import { Module } from "src/modules/Modules.sol"; -import {console2} from "forge-std/console2.sol"; - contract SettleTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; @@ -309,6 +307,8 @@ contract SettleTest is Test, Permit2User { // [X] it reverts // [X] given the auction house has insufficient balance of the base token // [X] it reverts + // [ ] given that the capacity is not filled + // [ ] it succeeds - transfers remaining base tokens back to the owner // [X] given the last bidder has a partial fill // [X] it succeeds - last bidder receives the partial fill and is returned excess quote tokens // [X] given the auction bids have different prices @@ -444,12 +444,11 @@ contract SettleTest is Test, Permit2User { ); // Check quote token balances - uint256 bidOnePercentageUnfilled = - (bidOneAmountOut - bidOneAmountOutActual) * 1e18 / bidOneAmountOut; - uint256 bidOneAmountActual = bidOneAmount * bidOnePercentageUnfilled / 1e18; + uint256 bidOnePercentageFilled = bidOneAmountOutActual * 1e18 / bidOneAmountOut; + uint256 bidOneAmountActual = bidOneAmount * bidOnePercentageFilled / 1e18; assertEq( quoteToken.balanceOf(bidderOne), - bidOneAmountActual, + bidOneAmount - bidOneAmountActual, "bidderOne: incorrect balance of quote token" ); // Remainder received as quote tokens. assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); @@ -462,7 +461,7 @@ contract SettleTest is Test, Permit2User { // Auction owner should have received quote tokens minus fees assertEq( quoteToken.balanceOf(auctionOwner), - bidOneAmount + bidThreeAmount - totalFeeAmount - bidOneAmountActual, + bidOneAmountActual + bidThreeAmount - totalFeeAmount, "auction owner: incorrect balance of quote token" ); From 9c2849d34061d9aab0266f646f2683effdccb9a6 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 17:45:59 +0400 Subject: [PATCH 100/117] Add refund of base token --- src/AuctionHouse.sol | 39 +++++++---- test/AuctionHouse/settle.t.sol | 121 +++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 16 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 74526029..21b493bd 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -432,17 +432,12 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { /// - transferring the quote token to the auction owner fails /// - collecting the payout from the auction owner fails /// - sending the payout to each bidder fails - function settle(uint96 lotId_) external override isLotValid(lotId_) { + function settle(uint96 lotId_) external override onlyOwner isLotValid(lotId_) { // Validation - // TODO check the caller is authorised + // No additional validation needed // Settle the lot on the auction module and get the winning bids // Reverts if the auction cannot be settled yet - // Some common things to check: - // 1. Total of amounts out is not greater than capacity - // 2. Minimum price is enforced - // 3. Minimum bid size is enforced - // 4. Minimum capacity sold is enforced AuctionModule module = _getModuleForId(lotId_); // Store the capacity remaining before settling @@ -483,10 +478,15 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Decrement the remaining payout payoutRemaining = 0; break; - } else { - // Decrement the remaining payout - payoutRemaining -= payoutAmount; } + + // Make sure the invariant isn't broken + if (payoutAmount > payoutRemaining) { + revert Broken_Invariant(); + } + + // Decrement the remaining payout + payoutRemaining -= payoutAmount; } } @@ -517,6 +517,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Handle payouts to bidders { + uint256 payoutRemaining = remainingCapacity; uint256 bidCount = winningBids.length; for (uint256 i; i < bidCount; i++) { // Send payout to each bidder @@ -527,8 +528,24 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { routing, auctionOutput ); + + // Make sure the invariant isn't broken + if (winningBids[i].minAmountOut > payoutRemaining) { + revert Broken_Invariant(); + } + + // Decrement the remaining payout + payoutRemaining -= winningBids[i].minAmountOut; + } + + // Handle the refund to the auction owner for any unused base token capacity + if (payoutRemaining > 0) { + routing.baseToken.safeTransfer(routing.owner, payoutRemaining); } + } + // Handle payment to the auction owner + { // Send payment in bulk to auction owner _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); } @@ -543,8 +560,6 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } } } - - // TODO payout refund } // ========== TOKEN TRANSFERS ========== // diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index a7ebb897..7e5ddf4c 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -172,6 +172,24 @@ contract SettleTest is Test, Permit2User { LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; uint256 internal marginalPrice; + modifier givenLotHasBidsLessThanCapacity() { + // Mint quote tokens to the bidders + quoteToken.mint(bidderOne, bidOneAmount); + + // Authorise spending + vm.prank(bidderOne); + quoteToken.approve(address(auctionHouse), bidOneAmount); + + // Create bids + // 4 < 10 + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBidOne) = + _createBid(bidderOne, bidOneAmount, bidOneAmountOut); + decryptedBids.push(decryptedBidOne); + + marginalPrice = bidOneAmount * 1e18 / bidOneAmountOut; + _; + } + modifier givenLotHasSufficientBids() { // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); @@ -299,16 +317,16 @@ contract SettleTest is Test, Permit2User { // [X] when the lot id is invalid // [X] it reverts - // [ ] when the caller is not authorized to settle - // [ ] it reverts + // [X] when the caller is not authorized to settle + // [X] it reverts // [X] when the auction module reverts // [X] it reverts // [X] given the auction house has insufficient balance of the quote token // [X] it reverts // [X] given the auction house has insufficient balance of the base token // [X] it reverts - // [ ] given that the capacity is not filled - // [ ] it succeeds - transfers remaining base tokens back to the owner + // [X] given that the capacity is not filled + // [X] it succeeds - transfers remaining base tokens back to the owner // [X] given the last bidder has a partial fill // [X] it succeeds - last bidder receives the partial fill and is returned excess quote tokens // [X] given the auction bids have different prices @@ -326,6 +344,15 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); } + function test_unauthorized() external { + // Expect revert + vm.expectRevert("UNAUTHORIZED"); + + // Attempt to settle the lot + vm.prank(bidderOne); + auctionHouse.settle(lotId); + } + function test_auctionModuleReverts_reverts() external givenAuctionModuleReverts { // Expect revert bytes memory err = @@ -387,6 +414,14 @@ contract SettleTest is Test, Permit2User { bidTwoAmountOut, "bidderTwo: incorrect balance of base token" ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); // Check quote token balances assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); @@ -442,6 +477,14 @@ contract SettleTest is Test, Permit2User { bidThreeAmountOut, "bidderTwo: incorrect balance of base token" ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); // Check quote token balances uint256 bidOnePercentageFilled = bidOneAmountOutActual * 1e18 / bidOneAmountOut; @@ -504,6 +547,16 @@ contract SettleTest is Test, Permit2User { bidFiveAmountOutActual, "bidderTwo: incorrect balance of base token" ); + assertEq( + baseToken.balanceOf(auctionOwner), + LOT_CAPACITY - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens + "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); // Check quote token balances assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); @@ -536,4 +589,64 @@ contract SettleTest is Test, Permit2User { auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" ); } + + function test_lessThanCapacity() + external + givenLotHasStarted + givenLotHasBidsLessThanCapacity + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq(baseToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of base token"); + assertEq( + baseToken.balanceOf(auctionOwner), + LOT_CAPACITY - bidOneAmountOut, + "auction owner: incorrect balance of base token" + ); // Returned remaining base tokens + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmount + 0) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + 0) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + 0 - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } } From 5a344a7c89bcacb002bea22c079606ebb2f858be Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 18:20:43 +0400 Subject: [PATCH 101/117] Test for decimals --- src/modules/auctions/LSBBA/LSBBA.sol | 6 +- test/AuctionHouse/settle.t.sol | 112 +++++++++++++++++++++++++- test/modules/auctions/LSBBA/bid.t.sol | 18 +++-- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index ea301205..2f8ff9be 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -193,6 +193,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// - Adds the bid ID to the list of bids to decrypt (in `AuctionData.bidIds`) /// - Returns the bid ID /// + /// Typically, the `_bid()` function would check whether the bid is of a minimum size and less than the capacity. As `Bid.minAmountOut` is encrypted, it is not possible to check this here. Instead, this is checked in `_settle()`. + /// /// This function reverts if: /// - The amount is less than the minimum bid size for the lot function _bid( @@ -204,8 +206,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { bytes calldata auctionData_ ) internal override returns (uint96 bidId) { // Validate inputs - // Amount at least minimum bid size for lot - if (amount_ < auctionData[lotId_].minBidSize) revert Auction_AmountLessThanMinimum(); + + // Does not check that the bid amount (in terms of the quote token) is less than the minimum bid size (in terms of the base token), because they are different units // Does not check that the bid amount (in terms of the quote token) is greater than the lot capacity (in terms of the base token), because they are different units diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index 7e5ddf4c..857bbf96 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -70,6 +70,9 @@ contract SettleTest is Test, Permit2User { uint256 internal constant bidFiveAmount = 8e18; uint256 internal constant bidFiveAmountOut = 4e18; // Price = 2 + Auction.AuctionParams internal auctionParams; + Auctioneer.RoutingParams internal routingParams; + function setUp() external { // Set block timestamp vm.warp(1_000_000); @@ -95,7 +98,7 @@ contract SettleTest is Test, Permit2User { }); lotStart = uint48(block.timestamp) + 1; - Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + auctionParams = Auction.AuctionParams({ start: lotStart, duration: auctionDuration, capacityInQuote: false, @@ -105,7 +108,7 @@ contract SettleTest is Test, Permit2User { lotConclusion = auctionParams.start + auctionParams.duration; (Keycode moduleKeycode,) = unwrapVeecode(auctionModule.VEECODE()); - Auctioneer.RoutingParams memory routingParams = Auctioneer.RoutingParams({ + routingParams = Auctioneer.RoutingParams({ auctionType: moduleKeycode, baseToken: baseToken, quoteToken: quoteToken, @@ -172,6 +175,30 @@ contract SettleTest is Test, Permit2User { LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; uint256 internal marginalPrice; + modifier givenTokensHaveDifferentDecimals() { + // Set up tokens + baseToken = new MockERC20("Base Token", "BASE", 17); + quoteToken = new MockERC20("Quote Token", "QUOTE", 13); + + // Update parameters + uint256 auctionCapacity = 10e17; + + auctionParams.capacity = auctionCapacity; + + routingParams.baseToken = baseToken; + routingParams.quoteToken = quoteToken; + + // Set up pre-funding + baseToken.mint(auctionOwner, auctionCapacity); + vm.prank(auctionOwner); + baseToken.approve(address(auctionHouse), auctionCapacity); + + // Create a new auction + vm.prank(auctionOwner); + lotId = auctionHouse.auction(routingParams, auctionParams); + _; + } + modifier givenLotHasBidsLessThanCapacity() { // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); @@ -649,4 +676,85 @@ contract SettleTest is Test, Permit2User { auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" ); } + + function test_lessThanCapacity_differentDecimals() + external + givenTokensHaveDifferentDecimals + givenLotHasStarted + { + uint256 bidAmount = 4e13; + uint256 bidAmountOut = 8e17; + + // Mint quote tokens + quoteToken.mint(bidderOne, bidAmount); + + // Authorise spending + vm.prank(bidderOne); + quoteToken.approve(address(auctionHouse), bidAmount); + + // Create the bid + (, LocalSealedBidBatchAuction.Decrypt memory decryptedBid) = + _createBid(bidderOne, bidAmount, bidAmountOut); + decryptedBids.push(decryptedBid); + + // Finish the auction lot + vm.warp(lotConclusion + 1); + + // Decrypt bids + auctionModule.decryptAndSortBids(lotId, decryptedBids); + + // Attempt to settle + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq(baseToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of base token"); + assertEq( + baseToken.balanceOf(auctionOwner), + 10e17 - bidAmountOut, + "auction owner: incorrect balance of base token" + ); // Returned remaining base tokens + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidAmountOut + 0) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidAmountOut + 0) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidAmountOut + 0 - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + // TODO partial fill, different decimals } diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index 0106c247..faa80323 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -200,18 +200,24 @@ contract LSBBABidTest is Test, Permit2User { auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); } - function test_whenAmountIsSmallerThanMinimumBidAmount_reverts() + function test_whenAmountIsSmallerThanMinimumBidAmount() public givenLotHasStarted whenAmountIsSmallerThanMinimumBidAmount { - // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_AmountLessThanMinimum.selector); - vm.expectRevert(err); - // Call vm.prank(address(auctionHouse)); - auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + uint96 bidId = auctionModule.bid(lotId, alice, recipient, referrer, bidAmount, auctionData); + + // Check values + LocalSealedBidBatchAuction.EncryptedBid memory encryptedBid = + auctionModule.getBidData(lotId, bidId); + assertEq(encryptedBid.bidder, alice); + assertEq(encryptedBid.recipient, recipient); + assertEq(encryptedBid.referrer, referrer); + assertEq(encryptedBid.amount, bidAmount); + assertEq(encryptedBid.encryptedAmountOut, auctionData); + assertEq(uint8(encryptedBid.status), uint8(LocalSealedBidBatchAuction.BidStatus.Submitted)); } function test_whenAmountIsLargerThanCapacity() From fc5d3393d50cd31f00146fb1737805fa4ce8ac7e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 29 Jan 2024 20:34:09 +0400 Subject: [PATCH 102/117] Add tests for different decimals --- test/AuctionHouse/settle.t.sol | 256 +++++++++++++++++++++++++-------- 1 file changed, 194 insertions(+), 62 deletions(-) diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index 857bbf96..e8b9f0fc 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -28,6 +28,8 @@ import { Module } from "src/modules/Modules.sol"; +import {console2} from "forge-std/console2.sol"; + contract SettleTest is Test, Permit2User { MockERC20 internal baseToken; MockERC20 internal quoteToken; @@ -56,19 +58,20 @@ contract SettleTest is Test, Permit2User { uint48 internal constant protocolFee = 1000; // 1% uint48 internal constant referrerFee = 500; // 0.5% - uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal _lotCapacity = 10e18; + uint256 internal constant SCALE = 1e18; uint256 internal bidSeed = 1e9; - uint256 internal constant bidOneAmount = 4e18; - uint256 internal constant bidOneAmountOut = 4e18; // Price = 1 - uint256 internal constant bidTwoAmount = 6e18; - uint256 internal constant bidTwoAmountOut = 6e18; // Price = 1 - uint256 internal constant bidThreeAmount = 7e18; - uint256 internal constant bidThreeAmountOut = 7e18; // Price = 1 - uint256 internal constant bidFourAmount = 8e18; - uint256 internal constant bidFourAmountOut = 2e18; // Price = 4 - uint256 internal constant bidFiveAmount = 8e18; - uint256 internal constant bidFiveAmountOut = 4e18; // Price = 2 + uint256 internal bidOneAmount = 4e18; + uint256 internal bidOneAmountOut = 4e18; // Price = 1 + uint256 internal bidTwoAmount = 6e18; + uint256 internal bidTwoAmountOut = 6e18; // Price = 1 + uint256 internal bidThreeAmount = 7e18; + uint256 internal bidThreeAmountOut = 7e18; // Price = 1 + uint256 internal bidFourAmount = 8e18; + uint256 internal bidFourAmountOut = 2e18; // Price = 4 + uint256 internal bidFiveAmount = 8e18; + uint256 internal bidFiveAmountOut = 4e18; // Price = 2 Auction.AuctionParams internal auctionParams; Auctioneer.RoutingParams internal routingParams; @@ -102,7 +105,7 @@ contract SettleTest is Test, Permit2User { start: lotStart, duration: auctionDuration, capacityInQuote: false, - capacity: LOT_CAPACITY, + capacity: _lotCapacity, implParams: abi.encode(auctionDataParams) }); lotConclusion = auctionParams.start + auctionParams.duration; @@ -121,9 +124,9 @@ contract SettleTest is Test, Permit2User { }); // Set up pre-funding - baseToken.mint(auctionOwner, LOT_CAPACITY); + baseToken.mint(auctionOwner, _lotCapacity); vm.prank(auctionOwner); - baseToken.approve(address(auctionHouse), LOT_CAPACITY); + baseToken.approve(address(auctionHouse), _lotCapacity); // Create an auction lot vm.prank(auctionOwner); @@ -175,23 +178,23 @@ contract SettleTest is Test, Permit2User { LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; uint256 internal marginalPrice; - modifier givenTokensHaveDifferentDecimals() { + modifier givenTokensHaveDifferentDecimals(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { // Set up tokens baseToken = new MockERC20("Base Token", "BASE", 17); quoteToken = new MockERC20("Quote Token", "QUOTE", 13); // Update parameters - uint256 auctionCapacity = 10e17; + _lotCapacity = _lotCapacity * 10 ** baseTokenDecimals_ / SCALE; - auctionParams.capacity = auctionCapacity; + auctionParams.capacity = _lotCapacity; routingParams.baseToken = baseToken; routingParams.quoteToken = quoteToken; // Set up pre-funding - baseToken.mint(auctionOwner, auctionCapacity); + baseToken.mint(auctionOwner, _lotCapacity); vm.prank(auctionOwner); - baseToken.approve(address(auctionHouse), auctionCapacity); + baseToken.approve(address(auctionHouse), _lotCapacity); // Create a new auction vm.prank(auctionOwner); @@ -199,7 +202,10 @@ contract SettleTest is Test, Permit2User { _; } - modifier givenLotHasBidsLessThanCapacity() { + modifier givenLotHasBidsLessThanCapacity(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { + bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; + // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); @@ -213,7 +219,7 @@ contract SettleTest is Test, Permit2User { _createBid(bidderOne, bidOneAmount, bidOneAmountOut); decryptedBids.push(decryptedBidOne); - marginalPrice = bidOneAmount * 1e18 / bidOneAmountOut; + marginalPrice = bidOneAmount * 10 ** baseTokenDecimals_ / bidOneAmountOut; _; } @@ -241,7 +247,12 @@ contract SettleTest is Test, Permit2User { _; } - modifier givenLotHasPartialFill() { + modifier givenLotHasPartialFill(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { + bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidThreeAmount = bidThreeAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidThreeAmountOut = bidThreeAmountOut * 10 ** baseTokenDecimals_ / SCALE; + // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); quoteToken.mint(bidderTwo, bidThreeAmount); @@ -261,11 +272,19 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidOne); decryptedBids.push(decryptedBidThree); - marginalPrice = bidThreeAmount * 1e18 / bidThreeAmountOut; + marginalPrice = bidThreeAmount * 10 ** baseTokenDecimals_ / bidThreeAmountOut; _; } - modifier givenLotHasSufficientBids_differentMarginalPrice() { + modifier givenLotHasSufficientBids_differentMarginalPrice( + uint8 baseTokenDecimals_, + uint8 quoteTokenDecimals_ + ) { + bidFourAmount = bidFourAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFourAmountOut = bidFourAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidFiveAmount = bidFiveAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; + // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidFourAmount); quoteToken.mint(bidderTwo, bidFiveAmount); @@ -286,7 +305,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidTwo); // bidFour first (price = 4), then bidFive (price = 3) - marginalPrice = bidFiveAmount * 1e18 / bidFiveAmountOut; + marginalPrice = bidFiveAmount * 10 ** baseTokenDecimals_ / bidFiveAmountOut; _; } @@ -485,7 +504,7 @@ contract SettleTest is Test, Permit2User { function test_partialFill() external givenLotHasStarted - givenLotHasPartialFill + givenLotHasPartialFill(18, 18) givenLotHasConcluded givenLotHasDecrypted { @@ -514,8 +533,75 @@ contract SettleTest is Test, Permit2User { ); // Check quote token balances - uint256 bidOnePercentageFilled = bidOneAmountOutActual * 1e18 / bidOneAmountOut; - uint256 bidOneAmountActual = bidOneAmount * bidOnePercentageFilled / 1e18; + uint256 bidOneAmountActual = 3e18; // 3 + assertEq( + quoteToken.balanceOf(bidderOne), + bidOneAmount - bidOneAmountActual, + "bidderOne: incorrect balance of quote token" + ); // Remainder received as quote tokens. + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmountActual + bidThreeAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmountActual + bidThreeAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmountActual + bidThreeAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_partialFill_differentDecimals() + external + givenLotHasStarted + givenLotHasPartialFill(17, 13) + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidOneAmountOutActual = 3e13; + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOutActual, + "bidderOne: incorrect balance of base token" + ); // Received partial payout. 10 - 7 = 3 + assertEq( + baseToken.balanceOf(bidderTwo), + bidThreeAmountOut, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + uint256 bidOneAmountActual = 3e17; // 3 assertEq( quoteToken.balanceOf(bidderOne), bidOneAmount - bidOneAmountActual, @@ -554,7 +640,7 @@ contract SettleTest is Test, Permit2User { function test_marginalPrice() external givenLotHasStarted - givenLotHasSufficientBids_differentMarginalPrice + givenLotHasSufficientBids_differentMarginalPrice(18, 18) givenLotHasConcluded givenLotHasDecrypted { @@ -576,7 +662,73 @@ contract SettleTest is Test, Permit2User { ); assertEq( baseToken.balanceOf(auctionOwner), - LOT_CAPACITY - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens + _lotCapacity - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens + "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidFourAmount + bidFiveAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidFourAmount + bidFiveAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidFourAmount + bidFiveAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_marginalPrice_differentDecimals() + external + givenLotHasStarted + givenLotHasSufficientBids_differentMarginalPrice(17, 13) + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidFourAmountOutActual = bidFourAmount * 1e13 / marginalPrice; + uint256 bidFiveAmountOutActual = bidFiveAmountOut; // since it set the marginal price + assertEq( + baseToken.balanceOf(bidderOne), + bidFourAmountOutActual, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidFiveAmountOutActual, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), + _lotCapacity - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens "auction owner: incorrect balance of base token" ); assertEq( @@ -620,7 +772,7 @@ contract SettleTest is Test, Permit2User { function test_lessThanCapacity() external givenLotHasStarted - givenLotHasBidsLessThanCapacity + givenLotHasBidsLessThanCapacity(18, 18) givenLotHasConcluded givenLotHasDecrypted { @@ -636,7 +788,7 @@ contract SettleTest is Test, Permit2User { assertEq(baseToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of base token"); assertEq( baseToken.balanceOf(auctionOwner), - LOT_CAPACITY - bidOneAmountOut, + _lotCapacity - bidOneAmountOut, "auction owner: incorrect balance of base token" ); // Returned remaining base tokens assertEq( @@ -679,43 +831,25 @@ contract SettleTest is Test, Permit2User { function test_lessThanCapacity_differentDecimals() external - givenTokensHaveDifferentDecimals + givenTokensHaveDifferentDecimals(17, 13) givenLotHasStarted + givenLotHasBidsLessThanCapacity(17, 13) + givenLotHasConcluded + givenLotHasDecrypted { - uint256 bidAmount = 4e13; - uint256 bidAmountOut = 8e17; - - // Mint quote tokens - quoteToken.mint(bidderOne, bidAmount); - - // Authorise spending - vm.prank(bidderOne); - quoteToken.approve(address(auctionHouse), bidAmount); - - // Create the bid - (, LocalSealedBidBatchAuction.Decrypt memory decryptedBid) = - _createBid(bidderOne, bidAmount, bidAmountOut); - decryptedBids.push(decryptedBid); - - // Finish the auction lot - vm.warp(lotConclusion + 1); - - // Decrypt bids - auctionModule.decryptAndSortBids(lotId, decryptedBids); - - // Attempt to settle + // Attempt to settle the lot auctionHouse.settle(lotId); // Check base token balances assertEq( baseToken.balanceOf(bidderOne), - bidAmountOut, + bidOneAmountOut, "bidderOne: incorrect balance of base token" ); assertEq(baseToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of base token"); assertEq( baseToken.balanceOf(auctionOwner), - 10e17 - bidAmountOut, + _lotCapacity - bidOneAmountOut, "auction owner: incorrect balance of base token" ); // Returned remaining base tokens assertEq( @@ -729,14 +863,14 @@ contract SettleTest is Test, Permit2User { assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); // Calculate fees on quote tokens - uint256 protocolFeeAmount = (bidAmountOut + 0) * protocolFee / 1e5; - uint256 referrerFeeAmount = (bidAmountOut + 0) * referrerFee / 1e5; + uint256 protocolFeeAmount = (bidOneAmount + 0) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + 0) * referrerFee / 1e5; uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; // Auction owner should have received quote tokens minus fees assertEq( quoteToken.balanceOf(auctionOwner), - bidAmountOut + 0 - totalFeeAmount, + bidOneAmount + 0 - totalFeeAmount, "auction owner: incorrect balance of quote token" ); @@ -755,6 +889,4 @@ contract SettleTest is Test, Permit2User { auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" ); } - - // TODO partial fill, different decimals } From 5d3115c416884fb0d09d17b99ace172e1959c8c7 Mon Sep 17 00:00:00 2001 From: Oighty Date: Mon, 29 Jan 2024 14:40:47 -0600 Subject: [PATCH 103/117] chore: cleanup TODOs in LSBBA --- src/modules/auctions/LSBBA/LSBBA.sol | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 2f8ff9be..e3d55e83 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -116,8 +116,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { // ========== SETUP ========== // constructor(address auctionHouse_) AuctionModule(auctionHouse_) { - // Set the minimum auction duration to 1 day - // TODO is this a good default? + // Set the minimum auction duration to 1 day initially minAuctionDuration = 1 days; } @@ -499,10 +498,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { QueueBid memory qBid = queue.popMax(); // Calculate amount out - // TODO handle partial filling of the last winning bid - // amountIn, and amountOut will be lower - // Need to somehow refund the amountIn that wasn't used to the user - // We know it will always be the last bid in the returned array, can maybe do something with that + // For partial bids, this will be the amount they would get at full value + // The auction house handles reduction of payouts for partial bids uint256 amountOut = (qBid.amountIn * _SCALE) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out @@ -545,11 +542,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { if (lot_.capacityInQuote) revert Auction_InvalidParams(); // minFillPercent must be less than or equal to 100% - // TODO should there be a minimum? if (implParams.minFillPercent > _ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); // minBidPercent must be greater than or equal to the global min and less than or equal to 100% - // TODO should we cap this below 100%? if ( implParams.minBidPercent < _MIN_BID_PERCENT || implParams.minBidPercent > _ONE_HUNDRED_PERCENT From 92f090bfe5a9b395c1f17d81902da9bfc36db3a7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 12:58:54 +0400 Subject: [PATCH 104/117] Add tests for smaller and larger decimals --- test/AuctionHouse/settle.t.sol | 447 ++++++++++++++++++++--- test/modules/auctions/LSBBA/settle.t.sol | 316 +++++++++++++++- 2 files changed, 701 insertions(+), 62 deletions(-) diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index e8b9f0fc..117165a7 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -58,9 +58,10 @@ contract SettleTest is Test, Permit2User { uint48 internal constant protocolFee = 1000; // 1% uint48 internal constant referrerFee = 500; // 0.5% - uint256 internal _lotCapacity = 10e18; + uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal _lotCapacity = LOT_CAPACITY; uint256 internal constant SCALE = 1e18; - uint256 internal bidSeed = 1e9; + uint256 internal constant BID_SEED = 1e9; uint256 internal bidOneAmount = 4e18; uint256 internal bidOneAmountOut = 4e18; // Price = 1 @@ -73,6 +74,9 @@ contract SettleTest is Test, Permit2User { uint256 internal bidFiveAmount = 8e18; uint256 internal bidFiveAmountOut = 4e18; // Price = 2 + uint8 internal quoteTokenDecimals = 18; + uint8 internal baseTokenDecimals = 18; + Auction.AuctionParams internal auctionParams; Auctioneer.RoutingParams internal routingParams; @@ -80,8 +84,8 @@ contract SettleTest is Test, Permit2User { // Set block timestamp vm.warp(1_000_000); - baseToken = new MockERC20("Base Token", "BASE", 18); - quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + baseToken = new MockERC20("Base Token", "BASE", baseTokenDecimals); + quoteToken = new MockERC20("Quote Token", "QUOTE", quoteTokenDecimals); auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); auctionModule = new LocalSealedBidBatchAuction(address(auctionHouse)); @@ -139,7 +143,8 @@ contract SettleTest is Test, Permit2User { uint256 bidAmountOut_ ) internal returns (uint96 bidId_, LocalSealedBidBatchAuction.Decrypt memory decryptedBid) { // Encrypt the bid amount - decryptedBid = LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: bidSeed}); + decryptedBid = + LocalSealedBidBatchAuction.Decrypt({amountOut: bidAmountOut_, seed: BID_SEED}); bytes memory auctionData_ = _encrypt(decryptedBid); Router.BidParams memory bidParams = Router.BidParams({ @@ -173,15 +178,34 @@ contract SettleTest is Test, Permit2User { ); } + /// @notice Calculates the marginal price, given the amount in and out + /// + /// @param bidAmount_ The amount of the bid + /// @param bidAmountOut_ The amount of the bid out + /// @return uint256 The marginal price (18 dp) + function _getMarginalPrice( + uint256 bidAmount_, + uint256 bidAmountOut_ + ) internal view returns (uint256) { + // Adjust all amounts to the scale + uint256 bidAmountScaled = bidAmount_ * SCALE / 10 ** quoteTokenDecimals; + uint256 bidAmountOutScaled = bidAmountOut_ * SCALE / 10 ** baseTokenDecimals; + + return bidAmountScaled * SCALE / bidAmountOutScaled; + } + // ======== Modifiers ======== // LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; uint256 internal marginalPrice; - modifier givenTokensHaveDifferentDecimals(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { + modifier givenLotHasDecimals(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { // Set up tokens - baseToken = new MockERC20("Base Token", "BASE", 17); - quoteToken = new MockERC20("Quote Token", "QUOTE", 13); + baseToken = new MockERC20("Base Token", "BASE", baseTokenDecimals_); + quoteToken = new MockERC20("Quote Token", "QUOTE", quoteTokenDecimals_); + + quoteTokenDecimals = quoteTokenDecimals_; + baseTokenDecimals = baseTokenDecimals_; // Update parameters _lotCapacity = _lotCapacity * 10 ** baseTokenDecimals_ / SCALE; @@ -191,6 +215,18 @@ contract SettleTest is Test, Permit2User { routingParams.baseToken = baseToken; routingParams.quoteToken = quoteToken; + // Update bid scale + bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidTwoAmount = bidTwoAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidTwoAmountOut = bidTwoAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidThreeAmount = bidThreeAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidThreeAmountOut = bidThreeAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidFourAmount = bidFourAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFourAmountOut = bidFourAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidFiveAmount = bidFiveAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; + // Set up pre-funding baseToken.mint(auctionOwner, _lotCapacity); vm.prank(auctionOwner); @@ -202,10 +238,7 @@ contract SettleTest is Test, Permit2User { _; } - modifier givenLotHasBidsLessThanCapacity(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { - bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; - bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; - + modifier givenLotHasBidsLessThanCapacity() { // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); @@ -219,7 +252,8 @@ contract SettleTest is Test, Permit2User { _createBid(bidderOne, bidOneAmount, bidOneAmountOut); decryptedBids.push(decryptedBidOne); - marginalPrice = bidOneAmount * 10 ** baseTokenDecimals_ / bidOneAmountOut; + // bidOne first (price = 1) + marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); _; } @@ -243,16 +277,12 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidOne); decryptedBids.push(decryptedBidTwo); - marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + // bidOne first (price = 1), then bidTwo (price = 1) + marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); _; } - modifier givenLotHasPartialFill(uint8 baseTokenDecimals_, uint8 quoteTokenDecimals_) { - bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; - bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; - bidThreeAmount = bidThreeAmount * 10 ** quoteTokenDecimals_ / SCALE; - bidThreeAmountOut = bidThreeAmountOut * 10 ** baseTokenDecimals_ / SCALE; - + modifier givenLotHasPartialFill() { // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidOneAmount); quoteToken.mint(bidderTwo, bidThreeAmount); @@ -272,19 +302,12 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidOne); decryptedBids.push(decryptedBidThree); - marginalPrice = bidThreeAmount * 10 ** baseTokenDecimals_ / bidThreeAmountOut; + // bidOne first (price = 1), then bidThree (price = 1) + marginalPrice = _getMarginalPrice(bidThreeAmount, bidThreeAmountOut); _; } - modifier givenLotHasSufficientBids_differentMarginalPrice( - uint8 baseTokenDecimals_, - uint8 quoteTokenDecimals_ - ) { - bidFourAmount = bidFourAmount * 10 ** quoteTokenDecimals_ / SCALE; - bidFourAmountOut = bidFourAmountOut * 10 ** baseTokenDecimals_ / SCALE; - bidFiveAmount = bidFiveAmount * 10 ** quoteTokenDecimals_ / SCALE; - bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; - + modifier givenLotHasSufficientBids_differentMarginalPrice() { // Mint quote tokens to the bidders quoteToken.mint(bidderOne, bidFourAmount); quoteToken.mint(bidderTwo, bidFiveAmount); @@ -305,7 +328,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidTwo); // bidFour first (price = 4), then bidFive (price = 3) - marginalPrice = bidFiveAmount * 10 ** baseTokenDecimals_ / bidFiveAmountOut; + marginalPrice = _getMarginalPrice(bidFiveAmount, bidFiveAmountOut); _; } @@ -501,10 +524,136 @@ contract SettleTest is Test, Permit2User { ); } + function test_success_quoteTokenDecimalsLarger() + external + givenLotHasDecimals(17, 13) + givenLotHasStarted + givenLotHasSufficientBids + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidTwoAmountOut, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmount + bidTwoAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + bidTwoAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + bidTwoAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_success_quoteTokenDecimalsSmaller() + external + givenLotHasDecimals(13, 17) + givenLotHasStarted + givenLotHasSufficientBids + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidTwoAmountOut, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmount + bidTwoAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + bidTwoAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + bidTwoAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + function test_partialFill() external givenLotHasStarted - givenLotHasPartialFill(18, 18) + givenLotHasPartialFill givenLotHasConcluded givenLotHasDecrypted { @@ -569,10 +718,11 @@ contract SettleTest is Test, Permit2User { ); } - function test_partialFill_differentDecimals() + function test_partialFill_quoteTokenDecimalsLarger() external + givenLotHasDecimals(17, 13) givenLotHasStarted - givenLotHasPartialFill(17, 13) + givenLotHasPartialFill givenLotHasConcluded givenLotHasDecrypted { @@ -580,7 +730,7 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); // Check base token balances - uint256 bidOneAmountOutActual = 3e13; + uint256 bidOneAmountOutActual = 3 * 10 ** baseTokenDecimals; assertEq( baseToken.balanceOf(bidderOne), bidOneAmountOutActual, @@ -601,7 +751,76 @@ contract SettleTest is Test, Permit2User { ); // Check quote token balances - uint256 bidOneAmountActual = 3e17; // 3 + uint256 bidOneAmountActual = 3 * 10 ** quoteTokenDecimals; // 3 + assertEq( + quoteToken.balanceOf(bidderOne), + bidOneAmount - bidOneAmountActual, + "bidderOne: incorrect balance of quote token" + ); // Remainder received as quote tokens. + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmountActual + bidThreeAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmountActual + bidThreeAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmountActual + bidThreeAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_partialFill_quoteTokenDecimalsSmaller() + external + givenLotHasDecimals(13, 17) + givenLotHasStarted + givenLotHasPartialFill + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidOneAmountOutActual = 3 * 10 ** baseTokenDecimals; + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOutActual, + "bidderOne: incorrect balance of base token" + ); // Received partial payout. 10 - 7 = 3 + assertEq( + baseToken.balanceOf(bidderTwo), + bidThreeAmountOut, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), 0, "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + uint256 bidOneAmountActual = 3 * 10 ** quoteTokenDecimals; // 3 assertEq( quoteToken.balanceOf(bidderOne), bidOneAmount - bidOneAmountActual, @@ -640,7 +859,73 @@ contract SettleTest is Test, Permit2User { function test_marginalPrice() external givenLotHasStarted - givenLotHasSufficientBids_differentMarginalPrice(18, 18) + givenLotHasSufficientBids_differentMarginalPrice + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + uint256 bidFourAmountOutActual = bidFourAmount * SCALE / marginalPrice; + assertEq( + baseToken.balanceOf(bidderOne), + bidFourAmountOutActual, + "bidderOne: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(bidderTwo), + bidFiveAmountOut, + "bidderTwo: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(auctionOwner), + _lotCapacity - bidFourAmountOutActual - bidFiveAmountOut, // Returned remaining base tokens + "auction owner: incorrect balance of base token" + ); + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidFourAmount + bidFiveAmount) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidFourAmount + bidFiveAmount) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidFourAmount + bidFiveAmount - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_marginalPrice_quoteTokenDecimalsLarger() + external + givenLotHasDecimals(17, 13) + givenLotHasStarted + givenLotHasSufficientBids_differentMarginalPrice givenLotHasConcluded givenLotHasDecrypted { @@ -648,8 +933,7 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); // Check base token balances - uint256 bidFourAmountOutActual = bidFourAmount * 1e18 / marginalPrice; - uint256 bidFiveAmountOutActual = bidFiveAmountOut; // since it set the marginal price + uint256 bidFourAmountOutActual = bidFourAmount * SCALE / marginalPrice; assertEq( baseToken.balanceOf(bidderOne), bidFourAmountOutActual, @@ -657,12 +941,12 @@ contract SettleTest is Test, Permit2User { ); assertEq( baseToken.balanceOf(bidderTwo), - bidFiveAmountOutActual, + bidFiveAmountOut, "bidderTwo: incorrect balance of base token" ); assertEq( baseToken.balanceOf(auctionOwner), - _lotCapacity - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens + _lotCapacity - bidFourAmountOutActual - bidFiveAmountOut, // Returned remaining base tokens "auction owner: incorrect balance of base token" ); assertEq( @@ -703,10 +987,11 @@ contract SettleTest is Test, Permit2User { ); } - function test_marginalPrice_differentDecimals() + function test_marginalPrice_quoteTokenDecimalsSmaller() external + givenLotHasDecimals(13, 17) givenLotHasStarted - givenLotHasSufficientBids_differentMarginalPrice(17, 13) + givenLotHasSufficientBids_differentMarginalPrice givenLotHasConcluded givenLotHasDecrypted { @@ -714,8 +999,7 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); // Check base token balances - uint256 bidFourAmountOutActual = bidFourAmount * 1e13 / marginalPrice; - uint256 bidFiveAmountOutActual = bidFiveAmountOut; // since it set the marginal price + uint256 bidFourAmountOutActual = bidFourAmount * SCALE / marginalPrice; assertEq( baseToken.balanceOf(bidderOne), bidFourAmountOutActual, @@ -723,12 +1007,12 @@ contract SettleTest is Test, Permit2User { ); assertEq( baseToken.balanceOf(bidderTwo), - bidFiveAmountOutActual, + bidFiveAmountOut, "bidderTwo: incorrect balance of base token" ); assertEq( baseToken.balanceOf(auctionOwner), - _lotCapacity - bidFourAmountOutActual - bidFiveAmountOutActual, // Returned remaining base tokens + _lotCapacity - bidFourAmountOutActual - bidFiveAmountOut, // Returned remaining base tokens "auction owner: incorrect balance of base token" ); assertEq( @@ -772,7 +1056,68 @@ contract SettleTest is Test, Permit2User { function test_lessThanCapacity() external givenLotHasStarted - givenLotHasBidsLessThanCapacity(18, 18) + givenLotHasBidsLessThanCapacity + givenLotHasConcluded + givenLotHasDecrypted + { + // Attempt to settle the lot + auctionHouse.settle(lotId); + + // Check base token balances + assertEq( + baseToken.balanceOf(bidderOne), + bidOneAmountOut, + "bidderOne: incorrect balance of base token" + ); + assertEq(baseToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of base token"); + assertEq( + baseToken.balanceOf(auctionOwner), + _lotCapacity - bidOneAmountOut, + "auction owner: incorrect balance of base token" + ); // Returned remaining base tokens + assertEq( + baseToken.balanceOf(address(auctionHouse)), + 0, + "auctionHouse: incorrect balance of base token" + ); + + // Check quote token balances + assertEq(quoteToken.balanceOf(bidderOne), 0, "bidderOne: incorrect balance of quote token"); + assertEq(quoteToken.balanceOf(bidderTwo), 0, "bidderTwo: incorrect balance of quote token"); + + // Calculate fees on quote tokens + uint256 protocolFeeAmount = (bidOneAmount + 0) * protocolFee / 1e5; + uint256 referrerFeeAmount = (bidOneAmount + 0) * referrerFee / 1e5; + uint256 totalFeeAmount = protocolFeeAmount + referrerFeeAmount; + + // Auction owner should have received quote tokens minus fees + assertEq( + quoteToken.balanceOf(auctionOwner), + bidOneAmount + 0 - totalFeeAmount, + "auction owner: incorrect balance of quote token" + ); + + // Fees stored on auction house + assertEq( + quoteToken.balanceOf(address(auctionHouse)), + totalFeeAmount, + "auction house: incorrect balance of quote token" + ); + + // Fee records updated + assertEq( + auctionHouse.rewards(protocol, quoteToken), protocolFeeAmount, "incorrect protocol fees" + ); + assertEq( + auctionHouse.rewards(referrer, quoteToken), referrerFeeAmount, "incorrect referrer fees" + ); + } + + function test_lessThanCapacity_quoteTokenDecimalsLarger() + external + givenLotHasDecimals(17, 13) + givenLotHasStarted + givenLotHasBidsLessThanCapacity givenLotHasConcluded givenLotHasDecrypted { @@ -829,11 +1174,11 @@ contract SettleTest is Test, Permit2User { ); } - function test_lessThanCapacity_differentDecimals() + function test_lessThanCapacity_quoteTokenDecimalsSmaller() external - givenTokensHaveDifferentDecimals(17, 13) + givenLotHasDecimals(13, 17) givenLotHasStarted - givenLotHasBidsLessThanCapacity(17, 13) + givenLotHasBidsLessThanCapacity givenLotHasConcluded givenLotHasDecrypted { diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index b2986bd5..2693283a 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -38,6 +38,8 @@ contract LSBBASettleTest is Test, Permit2User { bytes32(0x1AFCC05BD15602738CBE9BD75B76403AB2C9409F2CC0C189B4551DEE8B576AD3) ); + uint256 internal constant SCALE = 1e18; // Constants are kept in this scale + uint256 internal bidSeed = 1e9; uint96 internal bidOne; uint256 internal bidOneAmount = 2e18; @@ -61,6 +63,11 @@ contract LSBBASettleTest is Test, Permit2User { LocalSealedBidBatchAuction.Decrypt internal decryptedBidFive; LocalSealedBidBatchAuction.Decrypt[] internal decrypts; + uint8 internal quoteTokenDecimals = 18; + uint8 internal baseTokenDecimals = 18; + + Auction.AuctionParams auctionParams; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -84,13 +91,13 @@ contract LSBBASettleTest is Test, Permit2User { lotDuration = uint48(1 days); lotConclusion = lotStart + lotDuration; - Auction.AuctionParams memory auctionParams = Auction.AuctionParams({ + auctionParams = Auction.AuctionParams({ start: lotStart, duration: lotDuration, capacityInQuote: false, capacity: LOT_CAPACITY, implParams: abi.encode(auctionDataParams) - }); + }); // TODO add decimals to AuctionParams // Create the auction vm.prank(address(auctionHouse)); @@ -138,6 +145,22 @@ contract LSBBASettleTest is Test, Permit2User { } } + /// @notice Calculates the marginal price, given the amount in and out + /// + /// @param bidAmount_ The amount of the bid + /// @param bidAmountOut_ The amount of the bid out + /// @return uint256 The marginal price (18 dp) + function _getMarginalPrice( + uint256 bidAmount_, + uint256 bidAmountOut_ + ) internal view returns (uint256) { + // Adjust all amounts to the scale + uint256 bidAmountScaled = bidAmount_ * SCALE / 10 ** quoteTokenDecimals; + uint256 bidAmountOutScaled = bidAmountOut_ * SCALE / 10 ** baseTokenDecimals; + + return bidAmountScaled * SCALE / bidAmountOutScaled; + } + // ===== Modifiers ===== // modifier whenLotIdIsInvalid() { @@ -155,6 +178,35 @@ contract LSBBASettleTest is Test, Permit2User { _; } + modifier givenLotHasDecimals(uint8 quoteTokenDecimals_, uint8 baseTokenDecimals_) { + quoteTokenDecimals = quoteTokenDecimals_; + baseTokenDecimals = baseTokenDecimals_; + + // Adjust bid amounts + bidOneAmount = bidOneAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidOneAmountOut = bidOneAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidTwoAmount = bidTwoAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidTwoAmountOut = bidTwoAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidThreeAmount = bidThreeAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidThreeAmountOut = bidThreeAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidFourAmount = bidFourAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFourAmountOut = bidFourAmountOut * 10 ** baseTokenDecimals_ / SCALE; + bidFiveAmount = bidFiveAmount * 10 ** quoteTokenDecimals_ / SCALE; + bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; + + // Update auction params + + lotId = 2; + + // Create a new lot with the decimals set + vm.prank(address(auctionHouse)); + auctionModule.auction(lotId, auctionParams); + + // Warp to the start of the auction + vm.warp(lotStart); + _; + } + modifier whenLotIsBelowMinimumFilled() { // 2 < 2.5 (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); @@ -166,6 +218,8 @@ contract LSBBASettleTest is Test, Permit2User { modifier whenLotIsOverSubscribed() { // 2 + 3 + 7 > 10 + // Marginal price 1 + // Smallest bid (2) will not be filled at all (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); (bidThree, decryptedBidThree) = _createBid(bidThreeAmount, bidThreeAmountOut); @@ -179,6 +233,8 @@ contract LSBBASettleTest is Test, Permit2User { modifier whenLotIsOverSubscribedPartialFill() { // 2 + 3 + 6 > 10 + // Marginal price 1 + // Smallest bid (2) will be partially filled (bidOne, decryptedBidOne) = _createBid(bidOneAmount, bidOneAmountOut); (bidTwo, decryptedBidTwo) = _createBid(bidTwoAmount, bidTwoAmountOut); (bidFive, decryptedBidFive) = _createBid(bidFiveAmount, bidFiveAmountOut); @@ -326,7 +382,7 @@ contract LSBBASettleTest is Test, Permit2User { auctionModule.settle(lotId); } - function test_whenLotIsBelowMinimumFilled_returnsNoWinningBids() + function test_whenLotIsBelowMinimumFilled() public whenLotIsBelowMinimumFilled whenLotHasConcluded @@ -340,7 +396,37 @@ contract LSBBASettleTest is Test, Permit2User { assertEq(winningBids.length, 0); } - function test_whenMarginalPriceBelowMinimum_returnsNoWinningBids() + function test_whenLotIsBelowMinimumFilled_quoteTokenDecimalsLarger() + public + givenLotHasDecimals(17, 13) + whenLotIsBelowMinimumFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + + function test_whenLotIsBelowMinimumFilled_quoteTokenDecimalsSmaller() + public + givenLotHasDecimals(13, 17) + whenLotIsBelowMinimumFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + + function test_whenMarginalPriceBelowMinimum() public whenMarginalPriceBelowMinimum whenLotHasConcluded @@ -354,6 +440,36 @@ contract LSBBASettleTest is Test, Permit2User { assertEq(winningBids.length, 0); } + function test_whenMarginalPriceBelowMinimum_quoteTokenDecimalsLarger() + public + givenLotHasDecimals(17, 13) + whenMarginalPriceBelowMinimum + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + + function test_whenMarginalPriceBelowMinimum_quoteTokenDecimalsSmaller() + public + givenLotHasDecimals(13, 17) + whenMarginalPriceBelowMinimum + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Expect no winning bids + assertEq(winningBids.length, 0); + } + function test_whenLotIsOverSubscribed() public whenLotIsOverSubscribed @@ -365,9 +481,65 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidThreeAmount); + assertEq(winningBids[0].minAmountOut, bidThreeAmountOut); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut); + + // Expect winning bids + assertEq(winningBids.length, 2); + } + + function test_whenLotIsOverSubscribed_quoteTokenDecimalsLarger() + public + givenLotHasDecimals(17, 13) + whenLotIsOverSubscribed + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidThreeAmount); + assertEq(winningBids[0].minAmountOut, bidThreeAmountOut); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut); + + // Expect winning bids + assertEq(winningBids.length, 2); + } + + function test_whenLotIsOverSubscribed_quoteTokenDecimalsSmaller() + public + givenLotHasDecimals(13, 17) + whenLotIsOverSubscribed + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * 1e18 / marginalPrice; + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; // First bid - largest amount out assertEq(winningBids[0].amount, bidThreeAmount); @@ -392,10 +564,76 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = bidTwoAmount * 1e18 / bidTwoAmountOut; + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * 1e18 / marginalPrice; - bidFiveAmountOut = bidFiveAmount * 1e18 / marginalPrice; + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); + assertEq(winningBids[0].minAmountOut, bidFiveAmountOut, "bid 1: minAmountOut mismatch"); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount, "bid 2: amount mismatch"); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut, "bid 2: minAmountOut mismatch"); + + // Third bid - will be a partial fill and recognised by the AuctionHouse + assertEq(winningBids[2].amount, bidOneAmount, "bid 3: amount mismatch"); + assertEq(winningBids[2].minAmountOut, bidOneAmountOut, "bid 3: minAmountOut mismatch"); + + // Expect winning bids + assertEq(winningBids.length, 3); + } + + function test_whenLotIsOverSubscribed_partialFill_quoteTokenDecimalsLarger() + public + givenLotHasDecimals(17, 13) + whenLotIsOverSubscribedPartialFill + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); + assertEq(winningBids[0].minAmountOut, bidFiveAmountOut, "bid 1: minAmountOut mismatch"); + + // Second bid + assertEq(winningBids[1].amount, bidTwoAmount, "bid 2: amount mismatch"); + assertEq(winningBids[1].minAmountOut, bidTwoAmountOut, "bid 2: minAmountOut mismatch"); + + // Third bid - will be a partial fill and recognised by the AuctionHouse + assertEq(winningBids[2].amount, bidOneAmount, "bid 3: amount mismatch"); + assertEq(winningBids[2].minAmountOut, bidOneAmountOut, "bid 3: minAmountOut mismatch"); + + // Expect winning bids + assertEq(winningBids.length, 3); + } + + function test_whenLotIsOverSubscribed_partialFill_quoteTokenDecimalsSmaller() + public + givenLotHasDecimals(13, 17) + whenLotIsOverSubscribedPartialFill + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + + bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; // First bid - largest amount out assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); @@ -424,9 +662,65 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = bidOneAmount * 1e18 / bidOneAmountOut; + uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + + bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidTwoAmount); + assertEq(winningBids[0].minAmountOut, bidTwoAmountOut); + + // Second bid + assertEq(winningBids[1].amount, bidOneAmount); + assertEq(winningBids[1].minAmountOut, bidOneAmountOut); + + // Expect winning bids + assertEq(winningBids.length, 2); + } + + function test_whenLotIsFilled_quoteTokenDecimalsLarger() + public + givenLotHasDecimals(17, 13) + whenLotIsFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + + bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; + + // First bid - largest amount out + assertEq(winningBids[0].amount, bidTwoAmount); + assertEq(winningBids[0].minAmountOut, bidTwoAmountOut); + + // Second bid + assertEq(winningBids[1].amount, bidOneAmount); + assertEq(winningBids[1].minAmountOut, bidOneAmountOut); + + // Expect winning bids + assertEq(winningBids.length, 2); + } + + function test_whenLotIsFilled_quoteTokenDecimalsSmaller() + public + givenLotHasDecimals(13, 17) + whenLotIsFilled + whenLotHasConcluded + whenLotDecryptionIsComplete + { + // Call for settlement + vm.prank(address(auctionHouse)); + (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); + + // Calculate the marginal price + uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); - bidTwoAmountOut = bidTwoAmount * 1e18 / marginalPrice; + bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; // First bid - largest amount out assertEq(winningBids[0].amount, bidTwoAmount); From c678992e351cbee17ac92dcae57571ea17cdfcf4 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 13:37:30 +0400 Subject: [PATCH 105/117] Pass quote and base token decimals to the auction module upon creation --- src/bases/Auctioneer.sol | 29 ++++---- src/modules/Auction.sol | 69 ++++++++++++++----- test/AuctionHouse/auction.t.sol | 2 +- test/AuctionHouse/cancelAuction.t.sol | 2 +- test/modules/Auction/auction.t.sol | 21 ++++-- test/modules/Auction/cancel.t.sol | 2 +- test/modules/auctions/LSBBA/auction.t.sol | 35 ++++++---- test/modules/auctions/LSBBA/bid.t.sol | 5 +- .../auctions/LSBBA/cancelAuction.t.sol | 5 +- test/modules/auctions/LSBBA/cancelBid.t.sol | 5 +- .../auctions/LSBBA/decryptAndSortBids.t.sol | 5 +- test/modules/auctions/LSBBA/isLive.t.sol | 5 +- test/modules/auctions/LSBBA/settle.t.sol | 8 +-- 13 files changed, 132 insertions(+), 61 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index b77a8148..ef090f12 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -155,18 +155,9 @@ abstract contract Auctioneer is WithModules { revert InvalidModuleType(auctionRef); } - // Increment lot count and get ID - lotId = lotCounter++; - - // Auction Module - bool requiresPrefunding; - uint256 lotCapacity; - { - // Call module auction function to store implementation-specific data - (requiresPrefunding, lotCapacity) = auctionModule.auction(lotId, params_); - } - // Validate routing parameters + uint8 quoteTokenDecimals; + uint8 baseTokenDecimals; { // Validate routing information if (address(routing_.baseToken) == address(0)) { @@ -177,8 +168,8 @@ abstract contract Auctioneer is WithModules { } // Confirm tokens are within the required decimal range - uint8 baseTokenDecimals = routing_.baseToken.decimals(); - uint8 quoteTokenDecimals = routing_.quoteToken.decimals(); + baseTokenDecimals = routing_.baseToken.decimals(); + quoteTokenDecimals = routing_.quoteToken.decimals(); if (baseTokenDecimals < 6 || baseTokenDecimals > 18) { revert InvalidParams(); @@ -188,6 +179,18 @@ abstract contract Auctioneer is WithModules { } } + // Increment lot count and get ID + lotId = lotCounter++; + + // Auction Module + bool requiresPrefunding; + uint256 lotCapacity; + { + // Call module auction function to store implementation-specific data + (requiresPrefunding, lotCapacity) = + auctionModule.auction(lotId, params_, quoteTokenDecimals, baseTokenDecimals); + } + // Store routing information Routing storage routing = lotRouting[lotId]; routing.auctionReference = auctionRef; diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 6784edff..bceec759 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -35,16 +35,36 @@ abstract contract Auction { event AuctionClosed(uint256 indexed id); // ========== DATA STRUCTURES ========== // - /// @notice Core data for an auction lot + + /// @notice Core data for an auction lot + /// + /// @param start The timestamp when the auction starts + /// @param conclusion The timestamp when the auction ends + /// @param capacityInQuote Whether or not the capacity is in quote tokens + /// @param capacity The capacity of the lot + /// @param sold The amount of base tokens sold + /// @param purchased The amount of quote tokens purchased + /// @param quoteTokenDecimals The quote token decimals + /// @param baseTokenDecimals The base token decimals struct Lot { - uint48 start; // timestamp when market starts - uint48 conclusion; // timestamp when market no longer offered - bool capacityInQuote; // capacity limit is in payment token (true) or in payout (false, default) - uint256 capacity; // capacity remaining - uint256 sold; // payout tokens out - uint256 purchased; // quote tokens in + uint48 start; + uint48 conclusion; + bool capacityInQuote; + uint256 capacity; + uint256 sold; + uint256 purchased; + uint8 quoteTokenDecimals; + uint8 baseTokenDecimals; } + /// @notice Core data for a bid + /// + /// @param bidder The address of the bidder + /// @param recipient The address of the recipient + /// @param referrer The address of the referrer + /// @param amount The amount of quote tokens bid + /// @param minAmountOut The minimum amount of base tokens to receive + /// @param auctionParam The auction-specific parameter for the bid // TODO pack if we anticipate on-chain auction variants struct Bid { address bidder; @@ -52,24 +72,31 @@ abstract contract Auction { address referrer; uint256 amount; uint256 minAmountOut; - bytes auctionParam; // optional implementation-specific parameter for the bid + bytes auctionParam; } + /// @notice Parameters when creating an auction lot + /// + /// @param start The timestamp when the auction starts + /// @param duration The duration of the auction (in seconds) + /// @param capacityInQuote Whether or not the capacity is in quote tokens + /// @param capacity The capacity of the lot + /// @param implParams Abi-encoded implementation-specific parameters struct AuctionParams { uint48 start; uint48 duration; bool capacityInQuote; uint256 capacity; - bytes implParams; // abi-encoded params for specific auction implementations + bytes implParams; } // ========= STATE ========== // /// @notice Minimum auction duration in seconds - // TODO should this be set at deployment and/or through a function? uint48 public minAuctionDuration; - // 1% = 1_000 or 1e3. 100% = 100_000 or 1e5. + /// @notice Constant for percentages + /// @dev 1% = 1_000 or 1e3. 100% = 100_000 or 1e5. uint48 internal constant _ONE_HUNDRED_PERCENT = 100_000; /// @notice General information pertaining to auction lots @@ -149,13 +176,17 @@ abstract contract Auction { /// @notice Create an auction lot /// - /// @param lotId_ The lot id - /// @param params_ The auction parameters - /// @return prefundingRequired Whether or not prefunding is required - /// @return capacity The capacity of the lot + /// @param lotId_ The lot id + /// @param params_ The auction parameters + /// @param quoteTokenDecimals_ The quote token decimals + /// @param baseTokenDecimals_ The base token decimals + /// @return prefundingRequired Whether or not prefunding is required + /// @return capacity The capacity of the lot function auction( uint96 lotId_, - AuctionParams memory params_ + AuctionParams memory params_, + uint8 quoteTokenDecimals_, + uint8 baseTokenDecimals_ ) external virtual returns (bool prefundingRequired, uint256 capacity); /// @notice Cancel an auction lot @@ -211,7 +242,9 @@ abstract contract AuctionModule is Auction, Module { /// - the duration is less than the minimum function auction( uint96 lotId_, - AuctionParams memory params_ + AuctionParams memory params_, + uint8 quoteTokenDecimals_, + uint8 baseTokenDecimals_ ) external override onlyInternal returns (bool prefundingRequired, uint256 capacity) { // Start time must be zero or in the future if (params_.start > 0 && params_.start < uint48(block.timestamp)) { @@ -229,6 +262,8 @@ abstract contract AuctionModule is Auction, Module { lot.conclusion = lot.start + params_.duration; lot.capacityInQuote = params_.capacityInQuote; lot.capacity = params_.capacity; + lot.quoteTokenDecimals = quoteTokenDecimals_; + lot.baseTokenDecimals = baseTokenDecimals_; // Call internal createAuction function to store implementation-specific data (prefundingRequired) = _auction(lotId_, lot, params_.implParams); diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index 6fbfef35..1837f299 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -248,7 +248,7 @@ contract AuctionTest is Test, Permit2User { assertEq(lotPrefunded, false, "prefunded mismatch"); // Auction module also updated - (uint48 lotStart,,,,,) = mockAuctionModule.lotData(lotId); + (uint48 lotStart,,,,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotStart, block.timestamp, "start mismatch"); } diff --git a/test/AuctionHouse/cancelAuction.t.sol b/test/AuctionHouse/cancelAuction.t.sol index 172018f3..279a6bce 100644 --- a/test/AuctionHouse/cancelAuction.t.sol +++ b/test/AuctionHouse/cancelAuction.t.sol @@ -148,7 +148,7 @@ contract CancelAuctionTest is Test, Permit2User { auctionHouse.cancel(lotId); // Get lot data from the module - (, uint48 lotConclusion,, uint256 lotCapacity,,) = mockAuctionModule.lotData(lotId); + (, uint48 lotConclusion,, uint256 lotCapacity,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotConclusion, uint48(block.timestamp)); assertEq(lotCapacity, 0); diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index d1d7b9e0..86683220 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -37,12 +37,15 @@ contract AuctionTest is Test, Permit2User { address internal protocol = address(0x2); + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() external { // Ensure the block timestamp is a sane value vm.warp(1_000_000); - baseToken = new MockERC20("Base Token", "BASE", 18); - quoteToken = new MockERC20("Quote Token", "QUOTE", 18); + baseToken = new MockERC20("Base Token", "BASE", _baseTokenDecimals); + quoteToken = new MockERC20("Quote Token", "QUOTE", _quoteTokenDecimals); auctionHouse = new AuctionHouse(protocol, _PERMIT2_ADDRESS); mockAuctionModule = new MockAuctionModule(address(auctionHouse)); @@ -116,7 +119,7 @@ contract AuctionTest is Test, Permit2User { bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); vm.expectRevert(err); - mockAuctionModule.auction(0, auctionParams); + mockAuctionModule.auction(0, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_success() external { @@ -129,7 +132,9 @@ contract AuctionTest is Test, Permit2User { bool lotCapacityInQuote, uint256 lotCapacity, uint256 sold, - uint256 purchased + uint256 purchased, + uint8 quoteTokenDecimals, + uint8 baseTokenDecimals ) = mockAuctionModule.lotData(lotId); assertEq(lotStart, uint48(block.timestamp)); @@ -138,6 +143,8 @@ contract AuctionTest is Test, Permit2User { assertEq(lotCapacity, auctionParams.capacity); assertEq(sold, 0); assertEq(purchased, 0); + assertEq(quoteTokenDecimals, quoteToken.decimals()); + assertEq(baseTokenDecimals, baseToken.decimals()); } function test_whenStartTimeIsZero() external { @@ -147,7 +154,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module - (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); + (uint48 lotStart, uint48 lotConclusion,,,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotStart, uint48(block.timestamp)); // Sets to current timestamp assertEq(lotConclusion, lotStart + auctionParams.duration); @@ -162,7 +169,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module - (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); + (uint48 lotStart, uint48 lotConclusion,,,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotConclusion, lotStart + auctionParams.duration); } @@ -175,7 +182,7 @@ contract AuctionTest is Test, Permit2User { uint96 lotId = auctionHouse.auction(routingParams, auctionParams); // Get lot data from the module - (uint48 lotStart, uint48 lotConclusion,,,,) = mockAuctionModule.lotData(lotId); + (uint48 lotStart, uint48 lotConclusion,,,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotStart, auctionParams.start); assertEq(lotConclusion, lotStart + auctionParams.duration); } diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index 6b778045..1ecef776 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -118,7 +118,7 @@ contract CancelTest is Test, Permit2User { mockAuctionModule.cancelAuction(lotId); // Get lot data from the module - (, uint48 lotConclusion,, uint256 lotCapacity,,) = mockAuctionModule.lotData(lotId); + (, uint48 lotConclusion,, uint256 lotCapacity,,,,) = mockAuctionModule.lotData(lotId); assertEq(lotConclusion, uint48(block.timestamp)); assertEq(lotCapacity, 0); diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol index 23ba1bdf..656e129f 100644 --- a/test/modules/auctions/LSBBA/auction.t.sol +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -23,6 +23,9 @@ contract LSBBACreateAuctionTest is Test, Permit2User { Auction.AuctionParams internal auctionParams; LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -137,7 +140,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { vm.expectRevert(err); // Call - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_startsInPast_reverts() external whenStartTimeIsInPast { @@ -149,13 +152,13 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_noStartTime() external whenStartTimeIsZero { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); // Check values assertEq(auctionModule.getLot(lotId).start, uint48(block.timestamp)); @@ -172,7 +175,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_auctionDataParamsAreInvalid_reverts() external whenAuctionDataParamsAreInvalid { @@ -181,7 +184,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_capacityInQuoteIsEnabled_reverts() external whenCapacityInQuoteIsEnabled { @@ -191,7 +194,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_minimumFillPercentageIsMoreThanMax_reverts() @@ -204,7 +207,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_minimumBidPercentageIsLessThanMin_reverts() @@ -217,7 +220,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_minimumBidPercentageIsMoreThanMax_reverts() @@ -230,7 +233,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_publicKeyModulusIsOfIncorrectLength_reverts() @@ -243,7 +246,7 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } function test_execOnModule() external { @@ -258,14 +261,22 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // Call auctionHouse.execOnModule( - moduleVeecode, abi.encodeWithSelector(Auction.auction.selector, lotId, auctionParams) + moduleVeecode, + abi.encodeWithSelector( + Auction.auction.selector, + lotId, + auctionParams, + _quoteTokenDecimals, + _baseTokenDecimals + ) ); } function test_success() external { // Call vm.prank(address(auctionHouse)); - (bool prefundingRequired_, uint256 capacity_) = auctionModule.auction(lotId, auctionParams); + (bool prefundingRequired_, uint256 capacity_) = + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); // Check return values assertEq(prefundingRequired_, true); // Always true for LSBBA diff --git a/test/modules/auctions/LSBBA/bid.t.sol b/test/modules/auctions/LSBBA/bid.t.sol index faa80323..a4b62f4e 100644 --- a/test/modules/auctions/LSBBA/bid.t.sol +++ b/test/modules/auctions/LSBBA/bid.t.sol @@ -33,6 +33,9 @@ contract LSBBABidTest is Test, Permit2User { uint256 internal MIN_BID_SIZE; uint256 internal bidAmount = 1e18; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -67,7 +70,7 @@ contract LSBBABidTest is Test, Permit2User { // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); auctionData = abi.encode(1e9); // Encrypted amount out } diff --git a/test/modules/auctions/LSBBA/cancelAuction.t.sol b/test/modules/auctions/LSBBA/cancelAuction.t.sol index 4a3341ab..3c0100b3 100644 --- a/test/modules/auctions/LSBBA/cancelAuction.t.sol +++ b/test/modules/auctions/LSBBA/cancelAuction.t.sol @@ -27,6 +27,9 @@ contract LSBBACancelAuctionTest is Test, Permit2User { Auction.AuctionParams internal auctionParams; LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -59,7 +62,7 @@ contract LSBBACancelAuctionTest is Test, Permit2User { // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } // ===== Modifiers ===== // diff --git a/test/modules/auctions/LSBBA/cancelBid.t.sol b/test/modules/auctions/LSBBA/cancelBid.t.sol index d883a677..66134f76 100644 --- a/test/modules/auctions/LSBBA/cancelBid.t.sol +++ b/test/modules/auctions/LSBBA/cancelBid.t.sol @@ -42,6 +42,9 @@ contract LSBBACancelBidTest is Test, Permit2User { uint256 internal bidSeed = 1e9; LocalSealedBidBatchAuction.Decrypt internal decryptedBid; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -75,7 +78,7 @@ contract LSBBACancelBidTest is Test, Permit2User { // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); // Warp to the start of the auction vm.warp(lotStart); diff --git a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol index d6e96f40..6cbc8564 100644 --- a/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol +++ b/test/modules/auctions/LSBBA/decryptAndSortBids.t.sol @@ -54,6 +54,9 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { LocalSealedBidBatchAuction.Decrypt internal decryptedBidThree; LocalSealedBidBatchAuction.Decrypt[] internal decrypts; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -87,7 +90,7 @@ contract LSBBADecryptAndSortBidsTest is Test, Permit2User { // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); // Warp to the start of the auction vm.warp(lotStart); diff --git a/test/modules/auctions/LSBBA/isLive.t.sol b/test/modules/auctions/LSBBA/isLive.t.sol index 7bac62d1..f95a3dd4 100644 --- a/test/modules/auctions/LSBBA/isLive.t.sol +++ b/test/modules/auctions/LSBBA/isLive.t.sol @@ -32,6 +32,9 @@ contract LSBBAIsLiveTest is Test, Permit2User { Auction.AuctionParams internal auctionParams; LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; + uint8 internal constant _quoteTokenDecimals = 18; + uint8 internal constant _baseTokenDecimals = 18; + function setUp() public { // Ensure the block timestamp is a sane value vm.warp(1_000_000); @@ -64,7 +67,7 @@ contract LSBBAIsLiveTest is Test, Permit2User { // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } // ===== Modifiers ===== // diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index 2693283a..76f2789f 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -97,11 +97,11 @@ contract LSBBASettleTest is Test, Permit2User { capacityInQuote: false, capacity: LOT_CAPACITY, implParams: abi.encode(auctionDataParams) - }); // TODO add decimals to AuctionParams + }); // Create the auction vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, quoteTokenDecimals, baseTokenDecimals); // Warp to the start of the auction vm.warp(lotStart); @@ -195,12 +195,12 @@ contract LSBBASettleTest is Test, Permit2User { bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; // Update auction params - + auctionParams.capacity = LOT_CAPACITY * 10 ** quoteTokenDecimals_ / SCALE; lotId = 2; // Create a new lot with the decimals set vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams); + auctionModule.auction(lotId, auctionParams, quoteTokenDecimals, baseTokenDecimals); // Warp to the start of the auction vm.warp(lotStart); From c2779f75513e0e5acc7ef921b5a0fd31261cbf39 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 21:14:33 +0400 Subject: [PATCH 106/117] Working decimals --- src/modules/auctions/LSBBA/LSBBA.sol | 144 ++++++++++++++++------- test/AuctionHouse/settle.t.sol | 36 ++++-- test/modules/auctions/LSBBA/settle.t.sol | 81 +++++++------ 3 files changed, 172 insertions(+), 89 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index e3d55e83..d59dda4b 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -74,7 +74,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @param status The status of the auction /// @param nextDecryptIndex The index of the next bid to decrypt /// @param nextBidId The ID of the next bid to be submitted - /// @param minimumPrice The minimum price that the auction can settle at + /// @param minimumPrice The minimum price that the auction can settle at (in terms of quote token) /// @param minFilled The minimum amount of capacity that must be filled to settle the auction /// @param minBidSize The minimum amount that can be bid for the lot, determined by the percentage of capacity that must be filled per bid times the min bid price /// @param publicKeyModulus The public key modulus used to encrypt bids @@ -94,7 +94,7 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (_SCALE: `_ONE_HUNDRED_PERCENT`) /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (_SCALE: `_ONE_HUNDRED_PERCENT`) - /// @param minimumPrice_ The minimum price that the auction can settle at + /// @param minimumPrice_ The minimum price that the auction can settle at (in terms of quote token) /// @param publicKeyModulus_ The public key modulus used to encrypt bids struct AuctionDataParams { uint24 minFillPercent; @@ -422,76 +422,129 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== SETTLEMENT =========== // - /// @inheritdoc AuctionModule - /// @dev This function performs the following: - /// - Validates inputs - /// - Iterates over the bid queue to calculate the marginal clearing price of the auction - /// - Creates an array of winning bids - /// - Sets the auction status to settled - /// - Returns the array of winning bids + function _baseTokenToScale(uint256 amount_, Lot storage lot_) internal view returns (uint256) { + return amount_ * _SCALE / 10 ** lot_.baseTokenDecimals; + } + + function _quoteTokenToScale( + uint256 amount_, + Lot storage lot_ + ) internal view returns (uint256) { + return amount_ * _SCALE / 10 ** lot_.quoteTokenDecimals; + } + + /// @notice Calculates the marginal clearing price of the auction /// - /// This function reverts if: - /// - The auction is not in the Decrypted state - /// - The auction has already been settled - function _settle(uint96 lotId_) + /// @param lotId_ The lot ID of the auction to calculate the marginal price for + /// @return marginalPriceScaled The marginal clearing price of the auction (in terms of _SCALE) + /// @return numWinningBids The number of winning bids + function _calculateMarginalPrice(uint96 lotId_) internal - override - returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + view + returns (uint256 marginalPriceScaled, uint256 numWinningBids) { - // Check that auction is in the right state for settlement - if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); - - // Cache capacity - uint256 capacity = lotData[lotId_].capacity; + uint256 capacityScaled; // In terms of _SCALE + Lot storage lot = lotData[lotId_]; + { + capacityScaled = _baseTokenToScale(lot.capacity, lot); // Capacity is always in terms of base token for this auction type + } // Iterate over bid queue to calculate the marginal clearing price of the auction Queue storage queue = lotSortedBids[lotId_]; uint256 numBids = queue.getNumBids(); - uint256 marginalPrice; - uint256 totalAmountIn; - uint256 numWinningBids; + uint256 totalAmountInScaled; for (uint256 i = 0; i < numBids; i++) { - // Load bid - QueueBid storage qBid = queue.getBid(uint96(i)); - - // Calculate bid price - uint256 price = (qBid.amountIn * _SCALE) / qBid.minAmountOut; + // Calculate bid price in terms of _SCALE + uint256 priceScaled; + uint256 expended; + { + // Load bid + QueueBid storage qBid = queue.getBid(uint96(i)); + + uint256 amountInScaled; + { + amountInScaled = _quoteTokenToScale(qBid.amountIn, lot); + totalAmountInScaled += amountInScaled; + } - // Increment total amount in - totalAmountIn += qBid.amountIn; + // Calculate price + { + uint256 minAmountOutScaled = _baseTokenToScale(qBid.minAmountOut, lot); + priceScaled = amountInScaled * _SCALE / minAmountOutScaled; + } - // Determine total capacity expended at this price - uint256 expended = (totalAmountIn * _SCALE) / price; + // Determine total capacity expended at this price + expended = (totalAmountInScaled * _SCALE) / priceScaled; // In terms of _SCALE - // If total capacity expended is greater than or equal to the capacity, we have found the marginal price - if (expended >= capacity) { - marginalPrice = price; - numWinningBids = i + 1; - break; + // If total capacity expended is greater than or equal to the capacity, we have found the marginal price + if (expended >= capacityScaled) { + marginalPriceScaled = priceScaled; + numWinningBids = i + 1; + break; + } } // If we have reached the end of the queue, we have found the marginal price and the maximum capacity that can be filled if (i == numBids - 1) { + uint256 minFilledScaled; + { + minFilledScaled = _baseTokenToScale(auctionData[lotId_].minFilled, lot); // Capacity is always in terms of base token for this auction type + } + // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) - if (expended < auctionData[lotId_].minFilled) { - auctionData[lotId_].status = AuctionStatus.Settled; - return (winningBids_, bytes("")); + if (expended < minFilledScaled) { + marginalPriceScaled = 0; + numWinningBids = 0; } else { - marginalPrice = price; + marginalPriceScaled = priceScaled; numWinningBids = numBids; } } } + // Scale minimum price to _SCALE + uint256 minimumPriceScaled = _quoteTokenToScale(auctionData[lotId_].minimumPrice, lot); + // Check if the minimum price for the auction was reached - // If not, mark as settled and return no winning bids (so users can claim refunds) - if (marginalPrice < auctionData[lotId_].minimumPrice) { + if (marginalPriceScaled < minimumPriceScaled) { + return (0, 0); + } + + return (marginalPriceScaled, numWinningBids); + } + + /// @inheritdoc AuctionModule + /// @dev This function performs the following: + /// - Validates inputs + /// - Iterates over the bid queue to calculate the marginal clearing price of the auction + /// - Creates an array of winning bids + /// - Sets the auction status to settled + /// - Returns the array of winning bids + /// + /// As the decimals of the quote token and base token may be different, the capacity is converted to a common unit (_SCALE) before calculating the marginal clearing price. + /// + /// This function reverts if: + /// - The auction is not in the Decrypted state + /// - The auction has already been settled + function _settle(uint96 lotId_) + internal + override + returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + { + // Check that auction is in the right state for settlement + if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); + + (uint256 marginalPriceScaled, uint256 numWinningBids) = _calculateMarginalPrice(lotId_); + + // If the marginal price was not resolved, mark as settled and return no winning bids (so users can claim refunds) + if (marginalPriceScaled == 0) { auctionData[lotId_].status = AuctionStatus.Settled; return (winningBids_, bytes("")); } // Auction can be settled at the marginal price if we reach this point // Create winning bid array using marginal price to set amounts out + Queue storage queue = lotSortedBids[lotId_]; winningBids_ = new Bid[](numWinningBids); for (uint256 i; i < numWinningBids; i++) { // Load bid @@ -500,7 +553,10 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Calculate amount out // For partial bids, this will be the amount they would get at full value // The auction house handles reduction of payouts for partial bids - uint256 amountOut = (qBid.amountIn * _SCALE) / marginalPrice; + // Scaled to quoteTokenDecimals + uint256 amountInScaled = _quoteTokenToScale(qBid.amountIn, lotData[lotId_]); + uint256 amountOutScaled = (amountInScaled * _SCALE) / marginalPriceScaled; + uint256 amountOut = (amountOutScaled * 10 ** lotData[lotId_].baseTokenDecimals) / _SCALE; // Create winning bid from encrypted bid and calculated amount out EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.bidId]; diff --git a/test/AuctionHouse/settle.t.sol b/test/AuctionHouse/settle.t.sol index 117165a7..17dc9b9d 100644 --- a/test/AuctionHouse/settle.t.sol +++ b/test/AuctionHouse/settle.t.sol @@ -59,6 +59,7 @@ contract SettleTest is Test, Permit2User { uint48 internal constant referrerFee = 500; // 0.5% uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal constant MINIMUM_PRICE = 5e17; // 0.5e18 uint256 internal _lotCapacity = LOT_CAPACITY; uint256 internal constant SCALE = 1e18; uint256 internal constant BID_SEED = 1e9; @@ -79,6 +80,7 @@ contract SettleTest is Test, Permit2User { Auction.AuctionParams internal auctionParams; Auctioneer.RoutingParams internal routingParams; + LocalSealedBidBatchAuction.AuctionDataParams internal auctionDataParams; function setUp() external { // Set block timestamp @@ -96,11 +98,10 @@ contract SettleTest is Test, Permit2User { auctionHouse.setReferrerFee(referrer, referrerFee); // Auction parameters - LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = - LocalSealedBidBatchAuction.AuctionDataParams({ + auctionDataParams = LocalSealedBidBatchAuction.AuctionDataParams({ minFillPercent: 1000, // 1% minBidPercent: 1000, // 1% - minimumPrice: 5e17, // 0.5e18 + minimumPrice: MINIMUM_PRICE, publicKeyModulus: PUBLIC_KEY_MODULUS }); @@ -183,7 +184,7 @@ contract SettleTest is Test, Permit2User { /// @param bidAmount_ The amount of the bid /// @param bidAmountOut_ The amount of the bid out /// @return uint256 The marginal price (18 dp) - function _getMarginalPrice( + function _getMarginalPriceScaled( uint256 bidAmount_, uint256 bidAmountOut_ ) internal view returns (uint256) { @@ -194,6 +195,14 @@ contract SettleTest is Test, Permit2User { return bidAmountScaled * SCALE / bidAmountOutScaled; } + function _getAmountOut( + uint256 amountIn_, + uint256 marginalPrice_ + ) internal view returns (uint256) { + uint256 amountOutScaled = amountIn_ * SCALE / marginalPrice_; + return amountOutScaled * 10 ** baseTokenDecimals / 10 ** quoteTokenDecimals; + } + // ======== Modifiers ======== // LocalSealedBidBatchAuction.Decrypt[] internal decryptedBids; @@ -210,7 +219,10 @@ contract SettleTest is Test, Permit2User { // Update parameters _lotCapacity = _lotCapacity * 10 ** baseTokenDecimals_ / SCALE; + auctionDataParams.minimumPrice = MINIMUM_PRICE * 10 ** quoteTokenDecimals_ / SCALE; + auctionParams.capacity = _lotCapacity; + auctionParams.implParams = abi.encode(auctionDataParams); routingParams.baseToken = baseToken; routingParams.quoteToken = quoteToken; @@ -253,7 +265,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidOne); // bidOne first (price = 1) - marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + marginalPrice = _getMarginalPriceScaled(bidOneAmount, bidOneAmountOut); _; } @@ -278,7 +290,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidTwo); // bidOne first (price = 1), then bidTwo (price = 1) - marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); _; } @@ -303,7 +315,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidThree); // bidOne first (price = 1), then bidThree (price = 1) - marginalPrice = _getMarginalPrice(bidThreeAmount, bidThreeAmountOut); + marginalPrice = _getMarginalPriceScaled(bidThreeAmount, bidThreeAmountOut); _; } @@ -328,7 +340,7 @@ contract SettleTest is Test, Permit2User { decryptedBids.push(decryptedBidTwo); // bidFour first (price = 4), then bidFive (price = 3) - marginalPrice = _getMarginalPrice(bidFiveAmount, bidFiveAmountOut); + marginalPrice = _getMarginalPriceScaled(bidFiveAmount, bidFiveAmountOut); _; } @@ -400,8 +412,8 @@ contract SettleTest is Test, Permit2User { // [X] it succeeds - last bidder receives the partial fill and is returned excess quote tokens // [X] given the auction bids have different prices // [X] it succeeds - // [ ] given that the quote token decimals differ from the base token decimals - // [ ] it succeeds + // [X] given that the quote token decimals differ from the base token decimals + // [X] it succeeds // [X] it succeeds - auction owner receives quote tokens (minus fees), bidders receive base tokens and fees accrued function test_invalidLotId() external givenLotIdIsInvalid givenLotHasStarted { @@ -933,7 +945,7 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); // Check base token balances - uint256 bidFourAmountOutActual = bidFourAmount * SCALE / marginalPrice; + uint256 bidFourAmountOutActual = _getAmountOut(bidFourAmount, marginalPrice); assertEq( baseToken.balanceOf(bidderOne), bidFourAmountOutActual, @@ -999,7 +1011,7 @@ contract SettleTest is Test, Permit2User { auctionHouse.settle(lotId); // Check base token balances - uint256 bidFourAmountOutActual = bidFourAmount * SCALE / marginalPrice; + uint256 bidFourAmountOutActual = _getAmountOut(bidFourAmount, marginalPrice); assertEq( baseToken.balanceOf(bidderOne), bidFourAmountOutActual, diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index 76f2789f..af182b13 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -14,6 +14,8 @@ import {Auction} from "src/modules/Auction.sol"; import {RSAOAEP} from "src/lib/RSA.sol"; import {Bid as QueueBid} from "src/modules/auctions/LSBBA/MaxPriorityQueue.sol"; +import {console2} from "forge-std/console2.sol"; + contract LSBBASettleTest is Test, Permit2User { address internal constant _PROTOCOL = address(0x1); address internal alice = address(0x2); @@ -24,6 +26,7 @@ contract LSBBASettleTest is Test, Permit2User { LocalSealedBidBatchAuction internal auctionModule; uint256 internal constant LOT_CAPACITY = 10e18; + uint256 internal constant MINIMUM_PRICE = 1e18; uint48 internal lotStart; uint48 internal lotDuration; @@ -67,6 +70,7 @@ contract LSBBASettleTest is Test, Permit2User { uint8 internal baseTokenDecimals = 18; Auction.AuctionParams auctionParams; + LocalSealedBidBatchAuction.AuctionDataParams auctionDataParams; function setUp() public { // Ensure the block timestamp is a sane value @@ -78,11 +82,10 @@ contract LSBBASettleTest is Test, Permit2User { auctionHouse.installModule(auctionModule); // Set auction data parameters - LocalSealedBidBatchAuction.AuctionDataParams memory auctionDataParams = - LocalSealedBidBatchAuction.AuctionDataParams({ + auctionDataParams = LocalSealedBidBatchAuction.AuctionDataParams({ minFillPercent: 25_000, // 25% = 2.5e18 minBidPercent: 1000, - minimumPrice: 1e18, + minimumPrice: MINIMUM_PRICE, publicKeyModulus: PUBLIC_KEY_MODULUS }); @@ -150,7 +153,7 @@ contract LSBBASettleTest is Test, Permit2User { /// @param bidAmount_ The amount of the bid /// @param bidAmountOut_ The amount of the bid out /// @return uint256 The marginal price (18 dp) - function _getMarginalPrice( + function _getMarginalPriceScaled( uint256 bidAmount_, uint256 bidAmountOut_ ) internal view returns (uint256) { @@ -161,6 +164,14 @@ contract LSBBASettleTest is Test, Permit2User { return bidAmountScaled * SCALE / bidAmountOutScaled; } + function _getAmountOut( + uint256 amountIn_, + uint256 marginalPrice_ + ) internal view returns (uint256) { + uint256 amountOutScaled = amountIn_ * SCALE / marginalPrice_; + return amountOutScaled * 10 ** baseTokenDecimals / 10 ** quoteTokenDecimals; + } + // ===== Modifiers ===== // modifier whenLotIdIsInvalid() { @@ -194,8 +205,12 @@ contract LSBBASettleTest is Test, Permit2User { bidFiveAmount = bidFiveAmount * 10 ** quoteTokenDecimals_ / SCALE; bidFiveAmountOut = bidFiveAmountOut * 10 ** baseTokenDecimals_ / SCALE; + // Update auction implementation params + auctionDataParams.minimumPrice = MINIMUM_PRICE * 10 ** quoteTokenDecimals_ / SCALE; + // Update auction params - auctionParams.capacity = LOT_CAPACITY * 10 ** quoteTokenDecimals_ / SCALE; + auctionParams.capacity = LOT_CAPACITY * 10 ** baseTokenDecimals_ / SCALE; // Always base token + auctionParams.implParams = abi.encode(auctionDataParams); lotId = 2; // Create a new lot with the decimals set @@ -304,8 +319,8 @@ contract LSBBASettleTest is Test, Permit2User { // [X] it returns no winning bids // [X] given the lot is over-subscribed // [X] it returns winning bids, with the marginal price is the price at which the lot capacity is exhausted - // [ ] given the lot is over-subscribed with a partial fill - // [ ] it returns winning bids, with the marginal price is the price at which the lot capacity is exhausted, and a partial fill for the lowest winning bid + // [X] given the lot is over-subscribed with a partial fill + // [X] it returns winning bids, with the marginal price is the price at which the lot capacity is exhausted, and a partial fill for the lowest winning bid // [X] when the filled amount is greater than the lot minimum // [X] it returns winning bids, with the marginal price is the minimum price @@ -481,9 +496,9 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidThreeAmount); @@ -509,9 +524,9 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidThreeAmount); @@ -537,9 +552,9 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidThreeAmount); @@ -564,10 +579,10 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; - bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); + bidFiveAmountOut = _getAmountOut(bidFiveAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); @@ -597,10 +612,10 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; - bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); + bidFiveAmountOut = _getAmountOut(bidFiveAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); @@ -630,10 +645,10 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidTwoAmount, bidTwoAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidTwoAmount, bidTwoAmountOut); - bidThreeAmountOut = bidThreeAmount * SCALE / marginalPrice; - bidFiveAmountOut = bidFiveAmount * SCALE / marginalPrice; + bidThreeAmountOut = _getAmountOut(bidThreeAmount, marginalPrice); + bidFiveAmountOut = _getAmountOut(bidFiveAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidFiveAmount, "bid 1: amount mismatch"); @@ -662,9 +677,9 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidOneAmount, bidOneAmountOut); - bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; + bidTwoAmountOut = _getAmountOut(bidTwoAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidTwoAmount); @@ -690,20 +705,20 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidOneAmount, bidOneAmountOut); - bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; + bidTwoAmountOut = _getAmountOut(bidTwoAmount, marginalPrice); // First bid - largest amount out - assertEq(winningBids[0].amount, bidTwoAmount); - assertEq(winningBids[0].minAmountOut, bidTwoAmountOut); + assertEq(winningBids[0].amount, bidTwoAmount, "bid 1: amount mismatch"); + assertEq(winningBids[0].minAmountOut, bidTwoAmountOut, "bid 1: minAmountOut mismatch"); // Second bid - assertEq(winningBids[1].amount, bidOneAmount); - assertEq(winningBids[1].minAmountOut, bidOneAmountOut); + assertEq(winningBids[1].amount, bidOneAmount, "bid 2: amount mismatch"); + assertEq(winningBids[1].minAmountOut, bidOneAmountOut, "bid 2: minAmountOut mismatch"); // Expect winning bids - assertEq(winningBids.length, 2); + assertEq(winningBids.length, 2, "winning bids length mismatch"); } function test_whenLotIsFilled_quoteTokenDecimalsSmaller() @@ -718,9 +733,9 @@ contract LSBBASettleTest is Test, Permit2User { (LocalSealedBidBatchAuction.Bid[] memory winningBids,) = auctionModule.settle(lotId); // Calculate the marginal price - uint256 marginalPrice = _getMarginalPrice(bidOneAmount, bidOneAmountOut); + uint256 marginalPrice = _getMarginalPriceScaled(bidOneAmount, bidOneAmountOut); - bidTwoAmountOut = bidTwoAmount * SCALE / marginalPrice; + bidTwoAmountOut = _getAmountOut(bidTwoAmount, marginalPrice); // First bid - largest amount out assertEq(winningBids[0].amount, bidTwoAmount); From 7d4771eac8377ec8d25c15c54ac57a315f506553 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 21:20:41 +0400 Subject: [PATCH 107/117] De-uglify decimal code --- src/modules/auctions/LSBBA/LSBBA.sol | 41 ++++++++++++------------ test/modules/auctions/LSBBA/settle.t.sol | 9 ++++-- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index d59dda4b..87381622 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -422,10 +422,20 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== SETTLEMENT =========== // + /// @notice Scales a base token amount to _SCALE + /// + /// @param amount_ The amount to scale + /// @param lot_ The lot data for the auction + /// @return scaled The scaled amount function _baseTokenToScale(uint256 amount_, Lot storage lot_) internal view returns (uint256) { return amount_ * _SCALE / 10 ** lot_.baseTokenDecimals; } + /// @notice Scales a quote token amount to _SCALE + /// + /// @param amount_ The amount to scale + /// @param lot_ The lot data for the auction + /// @return scaled The scaled amount function _quoteTokenToScale( uint256 amount_, Lot storage lot_ @@ -443,11 +453,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { view returns (uint256 marginalPriceScaled, uint256 numWinningBids) { - uint256 capacityScaled; // In terms of _SCALE Lot storage lot = lotData[lotId_]; - { - capacityScaled = _baseTokenToScale(lot.capacity, lot); // Capacity is always in terms of base token for this auction type - } + // Capacity is always in terms of base token for this auction type + uint256 capacityScaled = _baseTokenToScale(lot.capacity, lot); // Iterate over bid queue to calculate the marginal clearing price of the auction Queue storage queue = lotSortedBids[lotId_]; @@ -456,28 +464,22 @@ contract LocalSealedBidBatchAuction is AuctionModule { for (uint256 i = 0; i < numBids; i++) { // Calculate bid price in terms of _SCALE uint256 priceScaled; - uint256 expended; + uint256 expendedScaled; { // Load bid QueueBid storage qBid = queue.getBid(uint96(i)); - uint256 amountInScaled; - { - amountInScaled = _quoteTokenToScale(qBid.amountIn, lot); - totalAmountInScaled += amountInScaled; - } + uint256 amountInScaled = _quoteTokenToScale(qBid.amountIn, lot); + totalAmountInScaled += amountInScaled; // Calculate price - { - uint256 minAmountOutScaled = _baseTokenToScale(qBid.minAmountOut, lot); - priceScaled = amountInScaled * _SCALE / minAmountOutScaled; - } + priceScaled = amountInScaled * _SCALE / _baseTokenToScale(qBid.minAmountOut, lot); // Determine total capacity expended at this price - expended = (totalAmountInScaled * _SCALE) / priceScaled; // In terms of _SCALE + expendedScaled = (totalAmountInScaled * _SCALE) / priceScaled; // In terms of _SCALE // If total capacity expended is greater than or equal to the capacity, we have found the marginal price - if (expended >= capacityScaled) { + if (expendedScaled >= capacityScaled) { marginalPriceScaled = priceScaled; numWinningBids = i + 1; break; @@ -486,13 +488,10 @@ contract LocalSealedBidBatchAuction is AuctionModule { // If we have reached the end of the queue, we have found the marginal price and the maximum capacity that can be filled if (i == numBids - 1) { - uint256 minFilledScaled; - { - minFilledScaled = _baseTokenToScale(auctionData[lotId_].minFilled, lot); // Capacity is always in terms of base token for this auction type - } + uint256 minFilledScaled = _baseTokenToScale(auctionData[lotId_].minFilled, lot); // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) - if (expended < minFilledScaled) { + if (expendedScaled < minFilledScaled) { marginalPriceScaled = 0; numWinningBids = 0; } else { diff --git a/test/modules/auctions/LSBBA/settle.t.sol b/test/modules/auctions/LSBBA/settle.t.sol index af182b13..468ae478 100644 --- a/test/modules/auctions/LSBBA/settle.t.sol +++ b/test/modules/auctions/LSBBA/settle.t.sol @@ -164,11 +164,16 @@ contract LSBBASettleTest is Test, Permit2User { return bidAmountScaled * SCALE / bidAmountOutScaled; } + /// @notice Calculates the amount out, given the amount in and the marginal price + /// + /// @param amountIn_ The amount in + /// @param marginalPriceScaled_ The marginal price (in terms of SCALE) + /// @return uint256 The amount out (in native decimals) function _getAmountOut( uint256 amountIn_, - uint256 marginalPrice_ + uint256 marginalPriceScaled_ ) internal view returns (uint256) { - uint256 amountOutScaled = amountIn_ * SCALE / marginalPrice_; + uint256 amountOutScaled = amountIn_ * SCALE / marginalPriceScaled_; return amountOutScaled * 10 ** baseTokenDecimals / 10 ** quoteTokenDecimals; } From 57af8f27ad4de2e8fc9f5fa32797dfa0a55bc98a Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 21:24:10 +0400 Subject: [PATCH 108/117] Implement suggestion --- src/AuctionHouse.sol | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 21b493bd..71cc2d8a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -450,7 +450,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { Routing memory routing = lotRouting[lotId_]; // Calculate the payout amount, handling partial fills - uint256[] memory paymentRefunds = new uint256[](winningBids.length); + uint256 lastBidRefund; + address lastBidder; { uint256 bidCount = winningBids.length; uint256 payoutRemaining = remainingCapacity; @@ -465,7 +466,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Calculate the refund amount in terms of the quote token uint256 payoutUnfulfilled = 1e18 - payoutRemaining * 1e18 / payoutAmount; uint256 refundAmount = winningBids[i].amount * payoutUnfulfilled / 1e18; - paymentRefunds[i] = refundAmount; + lastBidRefund = refundAmount; + lastBidder = winningBids[i].bidder; // Check that the refund amount is not greater than the bid amount if (refundAmount > winningBids[i].amount) { @@ -551,14 +553,8 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Handle the refund to the bidder is the last bid was a partial fill - { - uint256 bidCount = winningBids.length; - for (uint256 i; i < bidCount; i++) { - // Send refund to each bidder - if (paymentRefunds[i] > 0) { - routing.quoteToken.safeTransfer(winningBids[i].bidder, paymentRefunds[i]); - } - } + if (lastBidRefund > 0 && lastBidder != address(0)) { + routing.quoteToken.safeTransfer(lastBidder, lastBidRefund); } } From d0034d59734f008f1a4d49c820e2b201a5537438 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 30 Jan 2024 21:24:27 +0400 Subject: [PATCH 109/117] Typo --- src/AuctionHouse.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 71cc2d8a..1e7419b7 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -552,7 +552,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _sendPayment(routing.owner, totalAmountInLessFees, routing.quoteToken, routing.hooks); } - // Handle the refund to the bidder is the last bid was a partial fill + // Handle the refund to the bidder if the last bid was a partial fill if (lastBidRefund > 0 && lastBidder != address(0)) { routing.quoteToken.safeTransfer(lastBidder, lastBidRefund); } From f12e9b77946c53287099eb293e630b4440fe16ca Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 30 Jan 2024 13:48:30 -0600 Subject: [PATCH 110/117] feat: add single getter for decrypt data --- src/modules/auctions/LSBBA/LSBBA.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index e3d55e83..290a027c 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -608,6 +608,12 @@ contract LocalSealedBidBatchAuction is AuctionModule { return lotSortedBids[lotId_].getNumBids(); } + /// @notice Single view function to return the data needed to lookup private key for an auction and determine the number of bids left to decrypt + function getDecryptData(uint96 lotId_) public view returns (AuctionStatus status_, uint96 activeBids_, uint96 nextDecryptIndex_, bytes memory publicKeyModulus_) { + AuctionData storage data = auctionData[lotId_]; + return (data.status, uint96(data.bidIds.length), data.nextDecryptIndex, data.publicKeyModulus); + } + // =========== ATOMIC AUCTION STUBS ========== // /// @inheritdoc AuctionModule From c28eb650484424651e03dde3165aab3ec9048777 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 30 Jan 2024 14:25:49 -0600 Subject: [PATCH 111/117] refactor: pack variables in Lot struct --- src/modules/Auction.sol | 13 ++++++------- src/modules/auctions/LSBBA/LSBBA.sol | 14 ++++++++++++-- test/AuctionHouse/cancelAuction.t.sol | 2 +- test/modules/Auction/auction.t.sol | 6 +++--- test/modules/Auction/cancel.t.sol | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index bceec759..697aaf6e 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -40,21 +40,21 @@ abstract contract Auction { /// /// @param start The timestamp when the auction starts /// @param conclusion The timestamp when the auction ends + /// @param quoteTokenDecimals The quote token decimals + /// @param baseTokenDecimals The base token decimals /// @param capacityInQuote Whether or not the capacity is in quote tokens /// @param capacity The capacity of the lot /// @param sold The amount of base tokens sold /// @param purchased The amount of quote tokens purchased - /// @param quoteTokenDecimals The quote token decimals - /// @param baseTokenDecimals The base token decimals struct Lot { uint48 start; uint48 conclusion; + uint8 quoteTokenDecimals; + uint8 baseTokenDecimals; bool capacityInQuote; uint256 capacity; uint256 sold; uint256 purchased; - uint8 quoteTokenDecimals; - uint8 baseTokenDecimals; } /// @notice Core data for a bid @@ -65,7 +65,6 @@ abstract contract Auction { /// @param amount The amount of quote tokens bid /// @param minAmountOut The minimum amount of base tokens to receive /// @param auctionParam The auction-specific parameter for the bid - // TODO pack if we anticipate on-chain auction variants struct Bid { address bidder; address recipient; @@ -260,10 +259,10 @@ abstract contract AuctionModule is Auction, Module { Lot memory lot; lot.start = params_.start == 0 ? uint48(block.timestamp) : params_.start; lot.conclusion = lot.start + params_.duration; - lot.capacityInQuote = params_.capacityInQuote; - lot.capacity = params_.capacity; lot.quoteTokenDecimals = quoteTokenDecimals_; lot.baseTokenDecimals = baseTokenDecimals_; + lot.capacityInQuote = params_.capacityInQuote; + lot.capacity = params_.capacity; // Call internal createAuction function to store implementation-specific data (prefundingRequired) = _auction(lotId_, lot, params_.implParams); diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index c6feb7e9..8a8d6051 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -664,9 +664,19 @@ contract LocalSealedBidBatchAuction is AuctionModule { } /// @notice Single view function to return the data needed to lookup private key for an auction and determine the number of bids left to decrypt - function getDecryptData(uint96 lotId_) public view returns (AuctionStatus status_, uint96 activeBids_, uint96 nextDecryptIndex_, bytes memory publicKeyModulus_) { + function getDecryptData(uint96 lotId_) + public + view + returns ( + AuctionStatus status_, + uint96 activeBids_, + uint96 nextDecryptIndex_, + bytes memory publicKeyModulus_ + ) + { AuctionData storage data = auctionData[lotId_]; - return (data.status, uint96(data.bidIds.length), data.nextDecryptIndex, data.publicKeyModulus); + return + (data.status, uint96(data.bidIds.length), data.nextDecryptIndex, data.publicKeyModulus); } // =========== ATOMIC AUCTION STUBS ========== // diff --git a/test/AuctionHouse/cancelAuction.t.sol b/test/AuctionHouse/cancelAuction.t.sol index 279a6bce..90aac6ca 100644 --- a/test/AuctionHouse/cancelAuction.t.sol +++ b/test/AuctionHouse/cancelAuction.t.sol @@ -148,7 +148,7 @@ contract CancelAuctionTest is Test, Permit2User { auctionHouse.cancel(lotId); // Get lot data from the module - (, uint48 lotConclusion,, uint256 lotCapacity,,,,) = mockAuctionModule.lotData(lotId); + (, uint48 lotConclusion,,,, uint256 lotCapacity,,) = mockAuctionModule.lotData(lotId); assertEq(lotConclusion, uint48(block.timestamp)); assertEq(lotCapacity, 0); diff --git a/test/modules/Auction/auction.t.sol b/test/modules/Auction/auction.t.sol index 86683220..d2758965 100644 --- a/test/modules/Auction/auction.t.sol +++ b/test/modules/Auction/auction.t.sol @@ -129,12 +129,12 @@ contract AuctionTest is Test, Permit2User { ( uint48 lotStart, uint48 lotConclusion, + uint8 quoteTokenDecimals, + uint8 baseTokenDecimals, bool lotCapacityInQuote, uint256 lotCapacity, uint256 sold, - uint256 purchased, - uint8 quoteTokenDecimals, - uint8 baseTokenDecimals + uint256 purchased ) = mockAuctionModule.lotData(lotId); assertEq(lotStart, uint48(block.timestamp)); diff --git a/test/modules/Auction/cancel.t.sol b/test/modules/Auction/cancel.t.sol index 1ecef776..c560619f 100644 --- a/test/modules/Auction/cancel.t.sol +++ b/test/modules/Auction/cancel.t.sol @@ -118,7 +118,7 @@ contract CancelTest is Test, Permit2User { mockAuctionModule.cancelAuction(lotId); // Get lot data from the module - (, uint48 lotConclusion,, uint256 lotCapacity,,,,) = mockAuctionModule.lotData(lotId); + (, uint48 lotConclusion,,,, uint256 lotCapacity,,) = mockAuctionModule.lotData(lotId); assertEq(lotConclusion, uint48(block.timestamp)); assertEq(lotCapacity, 0); From 42c5c4f0800b8ef12d1cad70dadbf04e9b4b6101 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 30 Jan 2024 15:11:27 -0600 Subject: [PATCH 112/117] refactor: simplify decimal scaling --- src/modules/auctions/LSBBA/LSBBA.sol | 129 +++++++++------------------ 1 file changed, 44 insertions(+), 85 deletions(-) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 8a8d6051..9b7aefec 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -92,8 +92,8 @@ contract LocalSealedBidBatchAuction is AuctionModule { /// @notice Struct containing parameters for creating a new LSBBA auction /// - /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (_SCALE: `_ONE_HUNDRED_PERCENT`) - /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (_SCALE: `_ONE_HUNDRED_PERCENT`) + /// @param minFillPercent_ The minimum percentage of the lot capacity that must be filled for the auction to settle (scale: `_ONE_HUNDRED_PERCENT`) + /// @param minBidPercent_ The minimum percentage of the lot capacity that must be bid for each bid (scale: `_ONE_HUNDRED_PERCENT`) /// @param minimumPrice_ The minimum price that the auction can settle at (in terms of quote token) /// @param publicKeyModulus_ The public key modulus used to encrypt bids struct AuctionDataParams { @@ -107,7 +107,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { uint24 internal constant _MIN_BID_PERCENT = 1000; // 1% uint24 internal constant _PUB_KEY_EXPONENT = 65_537; - uint256 internal constant _SCALE = 1e18; mapping(uint96 lotId => AuctionData) public auctionData; mapping(uint96 lotId => mapping(uint96 bidId => EncryptedBid bid)) public lotEncryptedBids; @@ -422,121 +421,82 @@ contract LocalSealedBidBatchAuction is AuctionModule { // =========== SETTLEMENT =========== // - /// @notice Scales a base token amount to _SCALE - /// - /// @param amount_ The amount to scale - /// @param lot_ The lot data for the auction - /// @return scaled The scaled amount - function _baseTokenToScale(uint256 amount_, Lot storage lot_) internal view returns (uint256) { - return amount_ * _SCALE / 10 ** lot_.baseTokenDecimals; - } - - /// @notice Scales a quote token amount to _SCALE - /// - /// @param amount_ The amount to scale - /// @param lot_ The lot data for the auction - /// @return scaled The scaled amount - function _quoteTokenToScale( - uint256 amount_, - Lot storage lot_ - ) internal view returns (uint256) { - return amount_ * _SCALE / 10 ** lot_.quoteTokenDecimals; - } - /// @notice Calculates the marginal clearing price of the auction /// /// @param lotId_ The lot ID of the auction to calculate the marginal price for - /// @return marginalPriceScaled The marginal clearing price of the auction (in terms of _SCALE) + /// @return marginalPrice The marginal clearing price of the auction (in quote token units) /// @return numWinningBids The number of winning bids function _calculateMarginalPrice(uint96 lotId_) internal view - returns (uint256 marginalPriceScaled, uint256 numWinningBids) + returns (uint256 marginalPrice, uint256 numWinningBids) { - Lot storage lot = lotData[lotId_]; - // Capacity is always in terms of base token for this auction type - uint256 capacityScaled = _baseTokenToScale(lot.capacity, lot); + // Cache capacity and scaling values + // Capacity is always in base token units for this auction type + uint256 capacity = lotData[lotId_].capacity; + uint256 baseScale = 10 ** lotData[lotId_].baseTokenDecimals; // Iterate over bid queue to calculate the marginal clearing price of the auction Queue storage queue = lotSortedBids[lotId_]; uint256 numBids = queue.getNumBids(); - uint256 totalAmountInScaled; + uint256 totalAmountIn; for (uint256 i = 0; i < numBids; i++) { - // Calculate bid price in terms of _SCALE - uint256 priceScaled; - uint256 expendedScaled; - { - // Load bid - QueueBid storage qBid = queue.getBid(uint96(i)); - - uint256 amountInScaled = _quoteTokenToScale(qBid.amountIn, lot); - totalAmountInScaled += amountInScaled; - - // Calculate price - priceScaled = amountInScaled * _SCALE / _baseTokenToScale(qBid.minAmountOut, lot); - - // Determine total capacity expended at this price - expendedScaled = (totalAmountInScaled * _SCALE) / priceScaled; // In terms of _SCALE - - // If total capacity expended is greater than or equal to the capacity, we have found the marginal price - if (expendedScaled >= capacityScaled) { - marginalPriceScaled = priceScaled; - numWinningBids = i + 1; - break; - } + // Load bid + QueueBid storage qBid = queue.getBid(uint96(i)); + + // Calculate bid price (in quote token units) + // quote scale * base scale / base scale = quote scale + uint256 price = (qBid.amountIn * baseScale) / qBid.minAmountOut; + + // Increment total amount in + totalAmountIn += qBid.amountIn; + + // Determine total capacity expended at this price (in base token units) + // quote scale * base scale / quote scale = base scale + uint256 expended = (totalAmountIn * baseScale) / price; + + // If total capacity expended is greater than or equal to the capacity, we have found the marginal price + if (expended >= capacity) { + marginalPrice = price; + numWinningBids = i + 1; + break; } // If we have reached the end of the queue, we have found the marginal price and the maximum capacity that can be filled if (i == numBids - 1) { - uint256 minFilledScaled = _baseTokenToScale(auctionData[lotId_].minFilled, lot); - // If the total filled is less than the minimum filled, mark as settled and return no winning bids (so users can claim refunds) - if (expendedScaled < minFilledScaled) { - marginalPriceScaled = 0; - numWinningBids = 0; + if (expended < auctionData[lotId_].minFilled) { + return (0, 0); } else { - marginalPriceScaled = priceScaled; + marginalPrice = price; numWinningBids = numBids; } } } - // Scale minimum price to _SCALE - uint256 minimumPriceScaled = _quoteTokenToScale(auctionData[lotId_].minimumPrice, lot); - // Check if the minimum price for the auction was reached - if (marginalPriceScaled < minimumPriceScaled) { + // If not, mark as settled and return no winning bids (so users can claim refunds) + if (marginalPrice < auctionData[lotId_].minimumPrice) { return (0, 0); } - return (marginalPriceScaled, numWinningBids); + return (marginalPrice, numWinningBids); } - /// @inheritdoc AuctionModule - /// @dev This function performs the following: - /// - Validates inputs - /// - Iterates over the bid queue to calculate the marginal clearing price of the auction - /// - Creates an array of winning bids - /// - Sets the auction status to settled - /// - Returns the array of winning bids - /// - /// As the decimals of the quote token and base token may be different, the capacity is converted to a common unit (_SCALE) before calculating the marginal clearing price. - /// - /// This function reverts if: - /// - The auction is not in the Decrypted state - /// - The auction has already been settled function _settle(uint96 lotId_) internal override - returns (Bid[] memory winningBids_, bytes memory auctionOutput_) + returns (Bid[] memory winningBids_, bytes memory) { // Check that auction is in the right state for settlement if (auctionData[lotId_].status != AuctionStatus.Decrypted) revert Auction_WrongState(); - (uint256 marginalPriceScaled, uint256 numWinningBids) = _calculateMarginalPrice(lotId_); + // Calculate marginal price and number of winning bids + (uint256 marginalPrice, uint256 numWinningBids) = _calculateMarginalPrice(lotId_); - // If the marginal price was not resolved, mark as settled and return no winning bids (so users can claim refunds) - if (marginalPriceScaled == 0) { + // Check if a valid price was reached + // If not, mark as settled and return no winning bids (so users can claim refunds) + if (marginalPrice == 0) { auctionData[lotId_].status = AuctionStatus.Settled; return (winningBids_, bytes("")); } @@ -544,18 +504,17 @@ contract LocalSealedBidBatchAuction is AuctionModule { // Auction can be settled at the marginal price if we reach this point // Create winning bid array using marginal price to set amounts out Queue storage queue = lotSortedBids[lotId_]; + uint256 baseScale = 10 ** lotData[lotId_].baseTokenDecimals; winningBids_ = new Bid[](numWinningBids); for (uint256 i; i < numWinningBids; i++) { // Load bid QueueBid memory qBid = queue.popMax(); - // Calculate amount out + // Calculate amount out (in base token units) + // quote scale * base scale / quote scale = base scale // For partial bids, this will be the amount they would get at full value // The auction house handles reduction of payouts for partial bids - // Scaled to quoteTokenDecimals - uint256 amountInScaled = _quoteTokenToScale(qBid.amountIn, lotData[lotId_]); - uint256 amountOutScaled = (amountInScaled * _SCALE) / marginalPriceScaled; - uint256 amountOut = (amountOutScaled * 10 ** lotData[lotId_].baseTokenDecimals) / _SCALE; + uint256 amountOut = (qBid.amountIn * baseScale) / marginalPrice; // Create winning bid from encrypted bid and calculated amount out EncryptedBid storage encBid = lotEncryptedBids[lotId_][qBid.bidId]; From 63d5e7ce3a14c16c8ecaf6de87fef25bcaca2704 Mon Sep 17 00:00:00 2001 From: Oighty Date: Tue, 30 Jan 2024 16:55:12 -0600 Subject: [PATCH 113/117] chore: remove unused file --- src/modules/auctions/OBB.sol | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/modules/auctions/OBB.sol diff --git a/src/modules/auctions/OBB.sol b/src/modules/auctions/OBB.sol deleted file mode 100644 index b6d1c4f1..00000000 --- a/src/modules/auctions/OBB.sol +++ /dev/null @@ -1,4 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// open bid batch auction From e767184b5dcac85008d86b116afe9c9641660d96 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 31 Jan 2024 10:39:52 +0400 Subject: [PATCH 114/117] Restore function documentation --- src/modules/auctions/LSBBA/LSBBA.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 9b7aefec..6866adc6 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -483,6 +483,17 @@ contract LocalSealedBidBatchAuction is AuctionModule { return (marginalPrice, numWinningBids); } + /// @inheritdoc AuctionModule + /// @dev This function performs the following: + /// - Validates inputs + /// - Iterates over the bid queue to calculate the marginal clearing price of the auction + /// - Creates an array of winning bids + /// - Sets the auction status to settled + /// - Returns the array of winning bids + /// + /// This function reverts if: + /// - The auction is not in the Decrypted state + /// - The auction has already been settled function _settle(uint96 lotId_) internal override From af979c4842a943008ced2d425b8f3e5b1a3b241c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 31 Jan 2024 12:36:55 +0400 Subject: [PATCH 115/117] Add additional checks when pre-funding for capacityInQuote --- src/bases/Auctioneer.sol | 10 +++++++--- src/modules/Auction.sol | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index ef090f12..d0e8dd0a 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -265,7 +265,8 @@ abstract contract Auctioneer is WithModules { } // Perform pre-funding, if needed - if (requiresPrefunding) { + // It does not make sense to pre-fund the auction if the capacity is in quote tokens + if (requiresPrefunding == true && params_.capacityInQuote == false) { // Store pre-funding information routing.prefunded = true; @@ -324,8 +325,11 @@ abstract contract Auctioneer is WithModules { // Cancel the auction on the module module.cancelAuction(lotId_); - // If the auction is prefunded, transfer the remaining capacity to the owner - if (lotRouting[lotId_].prefunded && lotRemainingCapacity > 0) { + // If the auction is prefunded and supported, transfer the remaining capacity to the owner + if ( + lotRouting[lotId_].prefunded == true && module.capacityInQuote(lotId_) == false + && lotRemainingCapacity > 0 + ) { // Transfer payout tokens to the owner Routing memory routing = lotRouting[lotId_]; routing.baseToken.safeTransfer(routing.owner, lotRemainingCapacity); diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 697aaf6e..0edcae49 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -223,6 +223,15 @@ abstract contract Auction { /// @param lotId_ The lot id /// @return uint256 The remaining capacity of the lot function remainingCapacity(uint96 lotId_) external view virtual returns (uint256); + + /// @notice Get whether or not the capacity is in quote tokens + /// @dev The implementing function should handle the following: + /// - Return true if the capacity is in quote tokens + /// - Return false if the capacity is in base tokens + /// + /// @param lotId_ The lot id + /// @return bool Whether or not the capacity is in quote tokens + function capacityInQuote(uint96 lotId_) external view virtual returns (bool); } abstract contract AuctionModule is Auction, Module { @@ -532,6 +541,11 @@ abstract contract AuctionModule is Auction, Module { return lotData[lotId_].capacity; } + /// @inheritdoc Auction + function capacityInQuote(uint96 lotId_) external view override returns (bool) { + return lotData[lotId_].capacityInQuote; + } + /// @notice Get the lot data for a given lot ID /// /// @param lotId_ The lot ID From be60e9d768536bfd9afdcdaff495d369e32a8d28 Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 31 Jan 2024 09:13:17 -0600 Subject: [PATCH 116/117] chore: remove unused auction files --- src/modules/auctions/FPA.sol | 65 ---- src/modules/auctions/GDA.sol | 308 --------------- .../auctions/LSBBA/OldMaxPriorityQueue.sol | 130 ------- src/modules/auctions/OFDA.sol | 126 ------ src/modules/auctions/OSDA.sol | 194 --------- src/modules/auctions/SDA.sol | 368 ------------------ src/modules/auctions/TVGDA.sol | 120 ------ src/modules/auctions/bases/AtomicAuction.sol | 82 ---- src/modules/auctions/bases/BatchAuction.sol | 156 -------- .../auctions/bases/DiscreteAuction.sol | 186 --------- 10 files changed, 1735 deletions(-) delete mode 100644 src/modules/auctions/FPA.sol delete mode 100644 src/modules/auctions/GDA.sol delete mode 100644 src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol delete mode 100644 src/modules/auctions/OFDA.sol delete mode 100644 src/modules/auctions/OSDA.sol delete mode 100644 src/modules/auctions/SDA.sol delete mode 100644 src/modules/auctions/TVGDA.sol delete mode 100644 src/modules/auctions/bases/AtomicAuction.sol delete mode 100644 src/modules/auctions/bases/BatchAuction.sol delete mode 100644 src/modules/auctions/bases/DiscreteAuction.sol diff --git a/src/modules/auctions/FPA.sol b/src/modules/auctions/FPA.sol deleted file mode 100644 index e93d6f1a..00000000 --- a/src/modules/auctions/FPA.sol +++ /dev/null @@ -1,65 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; - -// interface IFixedPriceAuctioneer is IMaxPayoutAuctioneer { -// /// @notice Calculate current market price of payout token in quote tokens -// /// @param id_ ID of market -// /// @return Price for market in configured decimals (see MarketParams) -// /// @dev price is derived from the equation: -// // -// // p = f_p -// // -// // where -// // p = price -// // f_p = fixed price provided on creation -// // -// function marketPrice(uint256 id_) external view override returns (uint256); -// } - -// contract FixedPriceAuctioneer is MaxPayoutAuctioneer, IFixedPriceAuctioneer { -// /* ========== STATE ========== */ - -// mapping(uint256 id => uint256 price) internal fixedPrices; - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// IAggregator aggregator_, -// address guardian_, -// Authority authority_ -// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - -// /* ========== MARKET FUNCTIONS ========== */ - -// function __createMarket( -// uint256 id_, -// CoreData memory core_, -// StyleData memory style_, -// bytes memory params_ -// ) internal override { -// // Decode provided params -// uint256 fixedPrice = abi.decode(params_, (uint256)); - -// // Validate that fixed price is not zero -// if (fixedPrice == 0) revert Auctioneer_InvalidParams(); - -// // Set fixed price -// fixedPrices[id_] = fixedPrice; -// } - -// /* ========== TELLER FUNCTIONS ========== */ - -// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { -// // Calculate the payout from the fixed price and return -// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); -// } - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @inheritdoc IFixedPriceAuctioneer -// function marketPrice(uint256 id_) public view override returns (uint256) { -// return fixedPrices[id_]; -// } -// } diff --git a/src/modules/auctions/GDA.sol b/src/modules/auctions/GDA.sol deleted file mode 100644 index a17d910d..00000000 --- a/src/modules/auctions/GDA.sol +++ /dev/null @@ -1,308 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import "src/modules/auctions/bases/AtomicAuction.sol"; -// import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; - -// abstract contract GDA { -// /* ========== DATA STRUCTURES ========== */ -// enum Decay { -// Linear, -// Exponential -// } - -// /// @notice Auction pricing data -// struct AuctionData { -// uint256 equilibriumPrice; // price at which the auction is balanced -// uint256 minimumPrice; // minimum price the auction can reach -// uint256 baseScale; -// uint256 quoteScale; -// uint48 lastAuctionStart; -// Decay decayType; // type of decay to use for the market -// SD59x18 decayConstant; // speed at which the price decays, as SD59x18. -// SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. -// } - -// /* ========== STATE ========== */ - -// SD59x18 public constant ONE = SD59x18.wrap(1e18); -// mapping(uint96 lotId => AuctionData) public auctionData; -// } - -// contract GradualDutchAuctioneer is AtomicAuctionModule, GDA { -// /* ========== ERRORS ========== */ -// error InsufficientCapacity(); -// error InvalidParams(); - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// address auctionHouse_ -// ) Module(auctionHouse_) {} - -// /* ========== MODULE FUNCTIONS ========== */ -// function ID() public pure override returns (Keycode, uint8) { -// return (toKeycode("GDA"), 1); -// } - -// /* ========== MARKET FUNCTIONS ========== */ - -// function _auction( -// uint256 id_, -// Lot memory lot_, -// bytes memory params_ -// ) internal override { -// // Decode params -// ( -// uint256 equilibriumPrice_, // quote tokens per base token, so would be in quote token decimals? -// Decay decayType_, -// SD59x18 decayConstant_ -// ) = abi.decode(params_, (uint256, Decay, SD59x18)); - -// // Validate params -// // TODO - -// // Calculate scale from base token decimals -// uint256 baseScale = 10 ** uint256(lot_.baseToken.decimals()); -// uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); - -// // Calculate emissions rate -// uint256 baseCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(baseScale, equilibriumPrice_) : lot_.capacity; -// SD59x18 emissionsRate = sd(int256(baseCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * baseScale))); - -// // Set auction data -// AuctionData storage auction = auctionData[id_]; -// auction.equilibriumPrice = equilibriumPrice_; -// auction.baseScale = baseScale; -// auction.quoteScale = quoteScale; -// auction.lastAuctionStart = uint48(block.timestamp); -// auction.decayType = decayType_; -// auction.decayConstant = decayConstant_; -// auction.emissionsRate = emissionsRate; -// } - -// /* ========== TELLER FUNCTIONS ========== */ - -// function _purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { -// // Calculate payout amount for quote amount and seconds of emissions using GDA formula -// (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_); - -// // Update last auction start with seconds of emissions -// // Do not have to check that too many seconds have passed here -// // since payout/amount is checked against capacity in the top-level function -// auctionData[id_].lastAuctionStart += secondsOfEmissions; - -// return payout; -// } - -// /* ========== PRICE FUNCTIONS ========== */ - -// function priceFor(uint256 id_, uint256 payout_) public view override returns (uint256) { -// Decay decayType = auctionData[id_].decayType; - -// uint256 amount; -// if (decayType == Decay.EXPONENTIAL) { -// amount = _exponentialPriceFor(id_, payout_); -// } else if (decayType == Decay.LINEAR) { -// amount = _linearPriceFor(id_, payout_); -// } - -// // Check that amount in or payout do not exceed remaining capacity -// Lot memory lot = lotData[id_]; -// if (lot.capacityInQuote ? amount > lot.capacity : payout_ > lot.capacity) -// revert InsufficientCapacity(); - -// return amount; -// } - -// // For Continuos GDAs with exponential decay, the price of a given token t seconds after being emitted is: p(t) = p0 * e^(-k*t) -// // Integrating this function from the last auction start time for a particular number of tokens, gives the multiplier for the token price to determine amount of quote tokens required to purchase -// // P(T) = (p0 / k) * (e^(k*q/r) - 1) / e^(k*T) where T is the time since the last auction start, q is the number of tokens to purchase, p0 is the initial price, and r is the emissions rate -// function _exponentialPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { -// Lot memory lot = lotData[id_]; -// AuctionData memory auction = auctionData[id_]; - -// // Convert payout to SD59x18. We scale first to 18 decimals from the base token decimals -// SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.baseScale))); - -// // Calculate time since last auction start -// SD59x18 timeSinceLastAuctionStart = convert( -// int256(block.timestamp - uint256(auction.lastAuctionStart)) -// ); - -// // Calculate the first numerator factor -// SD59x18 num1 = sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))).div( -// auction.decayConstant -// ); - -// // Calculate the second numerator factor -// SD59x18 num2 = auction.decayConstant.mul(payout).div(auction.emissionsRate).exp().sub(ONE); - -// // Calculate the denominator -// SD59x18 denominator = auction.decayConstant.mul(timeSinceLastAuctionStart).exp(); - -// // Calculate return value -// // This value should always be positive, therefore, we can safely cast to uint256 -// // We scale the return value back to base token decimals -// return num1.mul(num2).div(denominator).intoUint256().mulDiv(auction.quoteScale, uUNIT); -// } - -// // p(t) = p0 * (1 - k*t) where p0 is the initial price, k is the decay constant, and t is the time since the last auction start -// // P(T) = (p0 * q / r) * (1 - k*T + k*q/2r) where T is the time since the last auction start, q is the number of tokens to purchase, -// // r is the emissions rate, and p0 is the initial price -// function _linearPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { -// Lot memory lot = lotData[id_]; -// AuctionData memory auction = auctionData[id_]; - -// // Convert payout to SD59x18. We scale first to 18 decimals from the base token decimals -// SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.baseScale))); - -// // Calculate time since last auction start -// SD59x18 timeSinceLastAuctionStart = convert( -// int256(block.timestamp - uint256(auction.lastAuctionStart)) -// ); - -// // Calculate decay factor -// // TODO can we confirm this will be positive? -// SD59x18 decayFactor = ONE.sub(auction.decayConstant.mul(timeSinceLastAuctionStart)).add( -// auction.decayConstant.mul(payout).div(convert(int256(2)).mul(auction.emissionsRate)) -// ); - -// // Calculate payout factor -// SD59x18 payoutFactor = payout.mul( -// sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))) -// ).div(auction.emissionsRate); // TODO do we lose precision here by dividing early? - -// // Calculate final return value and convert back to market scale -// return -// payoutFactor.mul(decayFactor).intoUint256().mulDiv( -// auction.quoteScale, -// uUNIT -// ); -// } - -// /* ========== PAYOUT CALCULATIONS ========== */ - -// function _payoutFor(uint256 id_, uint256 amount_) internal view returns (uint256) { -// (uint256 payout, ) = _payoutAndEmissionsFor(id_, amount_); - -// // Check that amount in or payout do not exceed remaining capacity -// Lot memory lot = lotData[id_]; -// if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) -// revert InsufficientCapacity(); - -// return payout; -// } - -// function _payoutAndEmissionsFor( -// uint256 id_, -// uint256 amount_ -// ) internal view returns (uint256, uint48) { -// Decay decayType = auctionData[id_].decayType; - -// if (decayType == Decay.EXPONENTIAL) { -// return _payoutForExpDecay(id_, amount_); -// } else if (decayType == Decay.LINEAR) { -// return _payoutForLinearDecay(id_, amount_); -// } else { -// revert InvalidParams(); -// } -// } - -// // TODO check this math again -// // P = (r / k) * ln(Q * k / p0 * e^(k*T) + 1) where P is the number of base tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, -// // p0 is the price target of the market, and T is the time since the last auction start -// function _payoutForExpDecay( -// uint256 id_, -// uint256 amount_ -// ) internal view returns (uint256, uint48) { -// AuctionData memory auction = auctionData[id_]; - -// // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor -// SD59x18 scaledQ = sd( -// int256( -// amount_.mulDiv(auction.uUNIT, auction.equilibriumPrice) -// ) -// ); - -// // Calculate time since last auction start -// SD59x18 timeSinceLastAuctionStart = convert( -// int256(block.timestamp - uint256(auction.lastAuctionStart)) -// ); - -// // Calculate the logarithm -// SD59x18 logFactor = auction -// .decayConstant -// .mul(timeSinceLastAuctionStart) -// .exp() -// .mul(scaledQ) -// .mul(auction.decayConstant) -// .add(ONE) -// .ln(); - -// // Calculate the payout -// SD59x18 payout = logFactor.mul(auction.emissionsRate).div(auction.decayConstant); - -// // Scale back to base token decimals -// // TODO verify we can safely cast to uint256 -// return payout.intoUint256().mulDiv(auction.baseScale, uUNIT); - -// // Calculate seconds of emissions from payout or amount (depending on capacity type) -// // TODO emissionsRate is being set only in base tokens over, need to think about this -// // Might just use equilibrium price to convert the quote amount here to payout amount and then divide by emissions rate -// uint48 secondsOfEmissions; -// if (lotData[id_].capacityInQuote) { -// // Convert amount to SD59x18 -// SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.quoteScale))); -// secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); -// } else { -// secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); -// } - -// // Scale payout to base token decimals and return -// // Payout should always be positive since it is atleast 1, therefore, we can safely cast to uint256 -// return (payout.intoUint256().mulDiv(auction.baseScale, uUNIT), secondsOfEmissions); -// } - -// // TODO check this math again -// // P = (r / k) * (sqrt(2 * k * Q / p0) + k * T - 1) where P is the number of base tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, -// // p0 is the price target of the market, and T is the time since the last auction start -// function _payoutForLinearDecay(uint256 id_, uint256 amount_) internal view returns (uint256) { -// AuctionData memory auction = auctionData[id_]; - -// // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor -// SD59x18 scaledQ = sd( -// int256(amount_.mulDiv(uUNIT, auction.equilibriumPrice)) -// ); - -// // Calculate time since last auction start -// SD59x18 timeSinceLastAuctionStart = convert( -// int256(block.timestamp - uint256(auction.lastAuctionStart)) -// ); - -// // Calculate factors -// SD59x18 sqrtFactor = convert(int256(2)).mul(auction.decayConstant).mul(scaledQ).sqrt(); -// SD59x18 factor = sqrtFactor.add(auction.decayConstant.mul(timeSinceLastAuctionStart)).sub( -// ONE -// ); - -// // Calculate payout -// SD59x18 payout = auction.emissionsRate.div(auction.decayConstant).mul(factor); - -// // Calculate seconds of emissions from payout or amount (depending on capacity type) -// // TODO same as in the above function -// uint48 secondsOfEmissions; -// if (lotData[id_].capacityInQuote) { -// // Convert amount to SD59x18 -// SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.scale))); -// secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); -// } else { -// secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); -// } - -// // Scale payout to base token decimals and return -// return (payout.intoUint256().mulDiv(auction.baseScale, uUNIT), secondsOfEmissions); -// } -// /* ========== ADMIN FUNCTIONS ========== */ -// /* ========== VIEW FUNCTIONS ========== */ -// } diff --git a/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol b/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol deleted file mode 100644 index 6ef67572..00000000 --- a/src/modules/auctions/LSBBA/OldMaxPriorityQueue.sol +++ /dev/null @@ -1,130 +0,0 @@ -//SPDX-License-Identifier: Unlicense -pragma solidity ^0.8.0; - -struct Bid { - uint96 queueId; // ID representing order of insertion - uint96 bidId; // ID of encrypted bid to reference on settlement - uint256 amountIn; - uint256 minAmountOut; -} - -/// @notice a max priority queue implementation, based off https://algs4.cs.princeton.edu/24pq/MaxPQ.java.html -/// @notice adapted from FrankieIsLost's min priority queue implementation at https://github.com/FrankieIsLost/smart-batched-auction/blob/master/contracts/libraries/MinPriorityQueue.sol -/// @author FrankieIsLost -/// @author Oighty (edits) -/// Bids in descending order -library MaxPriorityQueue { - struct Queue { - ///@notice incrementing bid id - uint96 nextBidId; - ///@notice array backing priority queue - uint96[] queueIdList; - ///@notice total number of bids in queue - uint96 numBids; - //@notice map bid ids to bids - mapping(uint96 => Bid) queueIdToBidMap; - } - - ///@notice initialize must be called before using queue. - function initialize(Queue storage self) public { - self.queueIdList.push(0); - self.nextBidId = 1; - } - - function isEmpty(Queue storage self) public view returns (bool) { - return self.numBids == 0; - } - - function getNumBids(Queue storage self) public view returns (uint256) { - return self.numBids; - } - - ///@notice view max bid - function getMax(Queue storage self) public view returns (Bid storage) { - require(!isEmpty(self), "nothing to return"); - uint96 maxId = self.queueIdList[1]; - return self.queueIdToBidMap[maxId]; - } - - ///@notice view bid by index in ascending order - function getBid(Queue storage self, uint256 index) public view returns (Bid storage) { - require(!isEmpty(self), "nothing to return"); - require(index <= self.numBids, "bid does not exist"); - require(index > 0, "cannot use 0 index"); - return self.queueIdToBidMap[self.queueIdList[index]]; - } - - ///@notice move bid up heap - function _swim(Queue storage self, uint96 k) private { - while (k > 1 && _isLess(self, k / 2, k)) { - _exchange(self, k, k / 2); - k = k / 2; - } - } - - ///@notice move bid down heap - function _sink(Queue storage self, uint96 k) private { - while (2 * k <= self.numBids) { - uint96 j = 2 * k; - if (j < self.numBids && _isLess(self, j, j + 1)) { - j++; - } - if (!_isLess(self, k, j)) { - break; - } - _exchange(self, k, j); - k = j; - } - } - - ///@notice insert bid in heap - function insert( - Queue storage self, - uint96 bidId, - uint256 amountIn, - uint256 minAmountOut - ) public { - _insert(self, Bid(self.nextBidId++, bidId, amountIn, minAmountOut)); - } - - ///@notice insert bid in heap - function _insert(Queue storage self, Bid memory bid) private { - self.queueIdList.push(bid.queueId); - self.queueIdToBidMap[bid.queueId] = bid; - self.numBids += 1; - _swim(self, self.numBids); - } - - ///@notice delete max bid from heap and return - function delMax(Queue storage self) public returns (Bid memory) { - require(!isEmpty(self), "nothing to delete"); - Bid memory max = self.queueIdToBidMap[self.queueIdList[1]]; - _exchange(self, 1, self.numBids--); - self.queueIdList.pop(); - delete self.queueIdToBidMap[max.queueId]; - _sink(self, 1); - return max; - } - - ///@notice helper function to determine ordering. When two bids have the same price, give priority - ///to the lower bid ID (inserted earlier) - function _isLess(Queue storage self, uint256 i, uint256 j) private view returns (bool) { - uint96 iId = self.queueIdList[i]; - uint96 jId = self.queueIdList[j]; - Bid memory bidI = self.queueIdToBidMap[iId]; - Bid memory bidJ = self.queueIdToBidMap[jId]; - uint256 relI = bidI.amountIn * bidJ.minAmountOut; - uint256 relJ = bidJ.amountIn * bidI.minAmountOut; - if (relI == relJ) { - return bidI.bidId < bidJ.bidId; - } - return relI < relJ; - } - - ///@notice helper function to exchange to bids in the heap - function _exchange(Queue storage self, uint256 i, uint256 j) private { - uint96 tempId = self.queueIdList[i]; - self.queueIdList[i] = self.queueIdList[j]; - self.queueIdList[j] = tempId; - } -} diff --git a/src/modules/auctions/OFDA.sol b/src/modules/auctions/OFDA.sol deleted file mode 100644 index 085d507e..00000000 --- a/src/modules/auctions/OFDA.sol +++ /dev/null @@ -1,126 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -// interface IOracleFixedDiscountAuctioneer is IMaxPayoutAuctioneer { -// /// @notice Auction pricing data -// struct AuctionData { -// IBondOracle oracle; -// uint48 fixedDiscount; -// bool conversionMul; -// uint256 conversionFactor; -// uint256 minPrice; -// } - -// /// @notice Calculate current market price of payout token in quote tokens -// /// @param id_ ID of market -// /// @return Price for market in configured decimals (see MarketParams) -// /// @dev price is derived from the equation: -// // -// // p = max(min_p, o_p * (1 - d)) -// // -// // where -// // p = price -// // min_p = minimum price -// // o_p = oracle price -// // d = fixed discount -// // -// // if price is below minimum price, minimum price is returned -// function marketPrice(uint256 id_) external view returns (uint256); -// } - -// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -// import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; -// import {OracleHelper} from "src/lib/OracleHelper.sol"; - -// contract OracleFixedDiscountAuctioneer is MaxPayoutAuctioneer, IOracleFixedDiscountAuctioneer { -// /* ========== ERRORS ========== */ -// error Auctioneer_OraclePriceZero(); - -// /* ========== STATE ========== */ - -// mapping(uint256 id => AuctionData) internal auctionData; - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// IAggregator aggregator_, -// address guardian_, -// Authority authority_ -// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - -// /* ========== MARKET FUNCTIONS ========== */ - -// function __createMarket( -// uint256 id_, -// CoreData memory core_, -// StyleData memory style_, -// bytes memory params_ -// ) internal override { -// // Decode params -// (IBondOracle oracle, uint48 fixedDiscount, uint48 maxDiscountFromCurrent) = abi.decode( -// params_, -// (IBondOracle, uint48, uint48) -// ); - -// // Validate oracle -// (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper -// .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); - -// // Validate discounts -// if ( -// fixedDiscount >= _ONE_HUNDRED_PERCENT || -// maxDiscountFromCurrent > _ONE_HUNDRED_PERCENT || -// fixedDiscount > maxDiscountFromCurrent -// ) revert Auctioneer_InvalidParams(); - -// // Set auction data -// AuctionData storage auction = auctionData[id_]; -// auction.oracle = oracle; -// auction.fixedDiscount = fixedDiscount; -// auction.conversionMul = conversionMul; -// auction.conversionFactor = conversionFactor; -// auction.minPrice = oraclePrice.mulDivUp( -// _ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, -// _ONE_HUNDRED_PERCENT -// ); -// } - -// /* ========== TELLER FUNCTIONS ========== */ - -// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { -// // Calculate the payout from the market price and return -// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); -// } - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @inheritdoc IOracleFixedDiscountAuctioneer -// function marketPrice(uint256 id_) public view override returns (uint256) { -// // Get auction data -// AuctionData memory auction = auctionData[id_]; - -// // Get oracle price -// uint256 oraclePrice = auction.oracle.currentPrice(id_); - -// // Revert if oracle price is 0 -// if (oraclePrice == 0) revert Auctioneer_OraclePriceZero(); - -// // Apply conversion factor -// if (auction.conversionMul) { -// oraclePrice *= auction.conversionFactor; -// } else { -// oraclePrice /= auction.conversionFactor; -// } - -// // Apply fixed discount -// uint256 price = oraclePrice.mulDivUp( -// uint256(_ONE_HUNDRED_PERCENT - auction.fixedDiscount), -// uint256(_ONE_HUNDRED_PERCENT) -// ); - -// // Compare the current price to the minimum price and return the maximum -// return price > auction.minPrice ? price : auction.minPrice; -// } -// } diff --git a/src/modules/auctions/OSDA.sol b/src/modules/auctions/OSDA.sol deleted file mode 100644 index 72a36cc5..00000000 --- a/src/modules/auctions/OSDA.sol +++ /dev/null @@ -1,194 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -// interface IOracleSequentialDutchAuctioneer is IMaxPayoutAuctioneer { -// /// @notice Auction pricing data -// struct AuctionData { -// IBondOracle oracle; // oracle to use for equilibrium price -// uint48 baseDiscount; // base discount from the oracle price to be used to determine equilibrium price -// bool conversionMul; // whether to multiply (true) or divide (false) oracle price by conversion factor -// uint256 conversionFactor; // conversion factor for oracle price to market price scale -// uint256 minPrice; // minimum price for the auction -// uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) -// bool tuning; // whether or not the auction tunes the equilibrium price -// } - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @notice Calculate current market price of payout token in quote tokens -// /// @param id_ ID of market -// /// @return Price for market in configured decimals (see MarketParams) -// /// @dev price is derived from the equation -// // -// // p(t) = max(min_p, p_o * (1 - d) * (1 + k * r(t))) -// // -// // where -// // p: price -// // min_p: minimum price -// // p_o: oracle price -// // d: base discount -// // -// // k: decay speed -// // k = l / i_d * t_d -// // where -// // l: market length -// // i_d: deposit interval -// // t_d: target interval discount -// // -// // r(t): percent difference of expected capacity and actual capacity at time t -// // r(t) = (ec(t) - c(t)) / ic -// // where -// // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) -// // ec(t) = ic * (l - t) / l -// // c(t): capacity remaining at time t -// // ic = initial capacity -// // -// // if price is below minimum price, minimum price is returned -// function marketPrice(uint256 id_) external view override returns (uint256); -// } - -// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -// import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; -// import {OracleHelper} from "src/lib/OracleHelper.sol"; - -// contract OracleSequentialDutchAuctioneer is MaxPayoutAuctioneer, IOracleSequentialDutchAuctioneer { -// /* ========== ERRORS ========== */ -// error Auctioneer_OraclePriceZero(); - -// /* ========== STATE ========== */ - -// mapping(uint256 id => AuctionData) internal auctionData; - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// IAggregator aggregator_, -// address guardian_, -// Authority authority_ -// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - -// /* ========== MARKET FUNCTIONS ========== */ - -// function __createMarket( -// uint256 id_, -// CoreData memory core_, -// StyleData memory style_, -// bytes memory params_ -// ) internal override { -// // Decode params -// ( -// IBondOracle oracle, -// uint48 baseDiscount, -// uint48 maxDiscountFromCurrent, -// uint48 targetIntervalDiscount -// ) = abi.decode(params_, (IBondOracle, uint48, uint48)); - -// // Validate oracle -// (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper -// .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); - -// // Validate discounts -// if ( -// baseDiscount >= _ONE_HUNDRED_PERCENT || -// maxDiscountFromCurrent > _ONE_HUNDRED_PERCENT || -// baseDiscount > maxDiscountFromCurrent -// ) revert Auctioneer_InvalidParams(); - -// // Set auction data -// AuctionData storage auction = auctionData[id_]; -// auction.oracle = oracle; -// auction.baseDiscount = baseDiscount; -// auction.conversionMul = conversionMul; -// auction.conversionFactor = conversionFactor; -// auction.minPrice = oraclePrice.mulDivUp( -// _ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, -// _ONE_HUNDRED_PERCENT -// ); -// } - -// /* ========== TELLER FUNCTIONS ========== */ - -// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { -// // Calculate the payout from the market price and return -// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); -// } - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @inheritdoc IOracleSequentialDutchAuctioneer -// function marketPrice(uint256 id_) public view override returns (uint256) { -// // Get auction data -// AuctionData memory auction = auctionData[id_]; - -// // Get oracle price -// uint256 price = auction.oracle.currentPrice(id_); - -// // Revert if oracle price is 0 -// if (price == 0) revert Auctioneer_OraclePriceZero(); - -// // Apply conversion factor -// if (auction.conversionMul) { -// price *= auction.conversionFactor; -// } else { -// price /= auction.conversionFactor; -// } - -// // Apply base discount -// price = price.mulDivUp( -// uint256(_ONE_HUNDRED_PERCENT - auction.baseDiscount), -// uint256(_ONE_HUNDRED_PERCENT) -// ); - -// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point -// uint256 initialCapacity = market.capacity + -// (market.capacityInQuote ? market.purchased : market.sold); - -// // Compute seconds remaining until market will conclude -// uint256 conclusion = uint256(term.conclusion); -// uint256 timeRemaining = conclusion - block.timestamp; - -// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point -// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold -// uint256 expectedCapacity = initialCapacity.mulDiv( -// timeRemaining, -// conclusion - uint256(term.start) -// ); - -// // Price is increased or decreased based on how far the market is ahead or behind -// // Intuition: -// // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted -// // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased -// // -// // This implementation uses a linear price decay -// // P(t) = P(0) * (1 + k * (X(t) - C(t) / C(0))) -// // P(t): price at time t -// // P(0): target price of the market provided by oracle + base discount (see IOSDA.MarketParams) -// // k: decay speed of the market -// // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. -// // X(t): expected capacity of the market at time t. -// // X(t) = C(0) * t / L. -// // C(t): actual capacity of the market at time t. -// // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). -// uint256 decay; -// if (expectedCapacity > core.capacity) { -// decay = -// _ONE_HUNDRED_PERCENT + -// (auction.decaySpeed * (expectedCapacity - core.capacity)) / -// initialCapacity; -// } else { -// // If actual capacity is greater than expected capacity, we need to check for underflows -// // The decay has a minimum value of 0 since that will reduce the price to 0 as well. -// uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / -// initialCapacity; -// decay = _ONE_HUNDRED_PERCENT > factor ? _ONE_HUNDRED_PERCENT - factor : 0; -// } - -// // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) -// price = price.mulDivUp(decay, _ONE_HUNDRED_PERCENT); - -// // Compare the current price to the minimum price and return the maximum -// return price > auction.minPrice ? price : auction.minPrice; -// } -// } diff --git a/src/modules/auctions/SDA.sol b/src/modules/auctions/SDA.sol deleted file mode 100644 index 1ce2538c..00000000 --- a/src/modules/auctions/SDA.sol +++ /dev/null @@ -1,368 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -// interface ISequentialDutchAuctioneer is IMaxPayoutAuctioneer { -// /// @notice Auction pricing data -// struct AuctionData { -// uint256 equilibriumPrice; // price at which the auction is balanced -// uint256 minPrice; // minimum price for the auction -// uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) -// bool tuning; // whether or not the auction tunes the equilibrium price -// } - -// /// @notice Data needed for tuning equilibrium price -// struct TuneData { -// uint48 lastTune; // last timestamp when control variable was tuned -// uint48 tuneInterval; // frequency of tuning -// uint48 tuneAdjustmentDelay; // time to implement downward tuning adjustments -// uint48 tuneGain; // parameter that controls how aggressive the tuning mechanism is, provided as a percentage with 3 decimals, i.e. 1% = 1_000 -// uint256 tuneIntervalCapacity; // capacity expected to be used during a tuning interval -// uint256 tuneBelowCapacity; // capacity that the next tuning will occur at -// } - -// /// @notice Equilibrium price adjustment data -// struct Adjustment { -// uint256 change; -// uint48 lastAdjustment; -// uint48 timeToAdjusted; // how long until adjustment is complete -// bool active; -// } - -// /* ========== ADMIN FUNCTIONS ========== */ - -// // TODO determine if we should have minimum values for tune parameters - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @notice Calculate current market price of payout token in quote tokens -// /// @param id_ ID of market -// /// @return Price for market in configured decimals (see MarketParams) -// /// @dev price is derived from the equation -// // -// // p(t) = max(min_p, p_eq * (1 + k * r(t))) -// // -// // where -// // p: price -// // min_p: minimum price -// // p_eq: equilibrium price -// // -// // k: decay speed -// // k = l / i_d * t_d -// // where -// // l: market length -// // i_d: deposit interval -// // t_d: target interval discount -// // -// // r(t): percent difference of expected capacity and actual capacity at time t -// // r(t) = (ec(t) - c(t)) / ic -// // where -// // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) -// // ec(t) = ic * (l - t) / l -// // c(t): capacity remaining at time t -// // ic = initial capacity -// // -// // if price is below minimum price, minimum price is returned -// function marketPrice(uint256 id_) external view override returns (uint256); -// } - -// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -// import {ISequentialDutchAuctioneer} from "src/interfaces/ISequentialDutchAuctioneer.sol"; - -// contract SequentialDutchAuctioneer is MaxPayoutAuctioneer, ISequentialDutchAuctioneer { -// /* ========== STATE ========== */ - -// mapping(uint256 id => AuctionData) public auctionData; -// mapping(uint256 id => TuneData) public tuneData; -// mapping(uint256 id => Adjustment) public adjustments; - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// IAggregator aggregator_, -// address guardian_, -// Authority authority_ -// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - -// /* ========== MARKET FUNCTIONS ========== */ - -// function __createMarket( -// uint256 id_, -// CoreData memory core_, -// StyleData memory style_, -// bytes memory params_ -// ) internal override { -// // Decode provided params -// // TODO - should we use a struct for this? that way it can be specified in the interface -// ( -// uint256 initialPrice, -// uint256 minPrice, -// uint48 targetIntervalDiscount, -// bool tuning, -// uint48 tuneInterval, -// uint48 tuneAdjustmentDelay, -// uint48 tuneGain -// ) = abi.decode(params_, (uint256, uint256, uint48, bool, uint48, uint48, uint48)); - -// // Validate auction data -// if (initialPrice == 0) revert Auctioneer_InvalidParams(); -// if (initialPrice < minPrice) revert Auctioneer_InitialPriceLessThanMin(); -// if (targetIntervalDiscount >= _ONE_HUNDRED_PERCENT) revert Auctioneer_InvalidParams(); - -// // Set auction data -// uint48 duration = core_.conclusion - core_.start; - -// AuctionData storage auction = auctionData[id_]; -// auction.equilibriumPrice = initialPrice; -// auction.minPrice = minPrice; -// auction.decaySpeed = (duration * targetIntervalDiscount) / style_.depositInterval; -// auction.tuning = tuning; - -// // Check if tuning is enabled -// if (tuning) { -// // Tune interval must be at least the deposit interval -// // and atleast the minimum global tune interval -// if (tuneInterval < style_.depositInterval || tuneInterval < minTuneInterval) -// revert Auctioneer_InvalidParams(); - -// // Tune adjustment delay must be less than or equal to the tune interval -// if (tuneAdjustmentDelay > tuneInterval) revert Auctioneer_InvalidParams(); - -// // Set tune data -// TuneData storage tune = tuneData[id_]; -// tune.lastTune = uint48(block.timestamp); -// tune.tuneInterval = tuneInterval; -// tune.tuneAdjustmentDelay = tuneAdjustmentDelay; -// tune.tuneIntervalCapacity = core_.capacity.mulDiv(tuneInterval, duration); -// tune.tuneBelowCapacity = core_.capacity - tune.tuneIntervalCapacity; -// // TODO should we enforce a maximum tune gain? there is likely a level above which it will greatly misbehave -// tune.tuneGain = tuneGain; -// } -// } - -// /* ========== TELLER FUNCTIONS ========== */ - -// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { -// // If tuning, apply any active adjustments to the equilibrium price -// if (auctionData[id_].tuning) { -// // The market equilibrium price can optionally be tuned to keep the market on schedule. -// // When it is lowered, the change is carried out smoothly over the tuneAdjustmentDelay. -// Adjustment storage adjustment = adjustments[id_]; -// if (adjustment.active) { -// // Update equilibrium price with adjusted price -// auctionData[id_].equilibriumPrice = _adjustedEquilibriumPrice( -// auctionData[id_].equilibriumPrice, -// adjustment -// ); - -// // Update adjustment data -// if (stillActive) { -// adjustment.change -= adjustBy; -// adjustment.timeToAdjusted -= secondsSince; -// adjustment.lastAdjustment = time_; -// } else { -// adjustment.active = false; -// } -// } -// } - -// // Calculate payout -// uint256 price = marketPrice(id_); -// uint256 payout = amount_.mulDiv(styleData[id_].scale, price); - -// // If tuning, attempt to tune the market -// // The payout value is required and capacity isn't updated until we provide this data back to the top level function. -// // Therefore, the function manually handles updates to capacity when tuning. -// if (auction.tuning) _tune(id_, price); - -// return payout; -// } - -// function _tune(uint256 id_, uint256 price_, uint256 amount_, uint256 payout_) internal { -// CoreData memory core = coreData[id_]; -// StyleData memory style = styleData[id_]; -// AuctionData memory auction = auctionData[id_]; -// TuneData storage tune = tuneData[id_]; - -// // Market tunes in 2 situations: -// // 1. If capacity has exceeded target since last tune adjustment and the market is oversold -// // 2. If a tune interval has passed since last tune adjustment and the market is undersold -// // -// // Markets are created with a target capacity with the expectation that capacity will -// // be utilized evenly over the duration of the market. -// // The intuition with tuning is: -// // - When the market is ahead of target capacity, we should tune based on capacity. -// // - When the market is behind target capacity, we should tune based on time. -// // -// // Tuning is equivalent to using a P controller to adjust the price to stay on schedule with selling capacity. -// // We don't want to make adjustments when the market is close to on schedule to avoid overcorrections. -// // Adjustments should undershoot rather than overshoot the target. - -// // Compute seconds remaining until market will conclude and total duration of market -// uint256 currentTime = block.timestamp; -// uint256 timeRemaining = uint256(core.conclusion - currentTime); -// uint256 duration = uint256(core.conclusion - core.start); - -// // Subtract amount / payout for this purchase from capacity since it hasn't been updated in the state yet. -// // If it is greater than capacity, revert. -// if (core.capacityInQuote ? amount_ > capacity : payout_ > capacity) -// revert Auctioneer_InsufficientCapacity(); -// uint256 capacity = capacityInQuote ? core.capacity - amount_ : core.capacity - payout_; - -// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point -// uint256 initialCapacity = capacity + -// (core.capacityInQuote ? core.purchased + amount_ : core.sold + payout_); - -// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point -// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold -// uint256 expectedCapacity = initialCapacity.mulDiv(timeRemaining, duration); - -// if ( -// (capacity < tune.tuneBelowCapacity && capacity < expectedCapacity) || -// (currentTime >= tune.lastTune + tune.tuneInterval && capacity > expectedCapacity) -// ) { -// // Calculate and apply tune adjustment - -// // Calculate the percent delta expected and current capacity -// uint256 delta = capacity > expectedCapacity -// ? ((capacity - expectedCapacity) * _ONE_HUNDRED_PERCENT) / initialCapacity -// : ((expectedCapacity - capacity) * _ONE_HUNDRED_PERCENT) / initialCapacity; - -// // Do not tune if the delta is within a reasonable range based on the deposit interval -// // Market capacity does not decrease continuously, but follows a step function -// // based on purchases. If the capacity deviation is less than the amount of capacity in a -// // deposit interval, then we should not tune. -// if (delta < (style.depositInterval * _ONE_HUNDRED_PERCENT) / duration) return; - -// // Apply the controller gain to the delta to determine the amount of change -// delta = (delta * tune.gain) / _ONE_HUNDRED_PERCENT; -// if (capacity > expectedCapacity) { -// // Apply a tune adjustment since the market is undersold - -// // Create an adjustment to lower the equilibrium price by delta percent over the tune adjustment delay -// Adjustment storage adjustment = adjustments[id_]; -// adjustment.active = true; -// adjustment.change = auction.equilibriumPrice.mulDiv(delta, _ONE_HUNDRED_PERCENT); -// adjustment.lastAdjustment = currentTime; -// adjustment.timeToAdjusted = tune.tuneAdjustmentDelay; -// } else { -// // Immediately tune up since the market is oversold - -// // Increase equilibrium price by delta percent -// auctionData[id_].equilibriumPrice = auction.equilibriumPrice.mulDiv( -// _ONE_HUNDRED_PERCENT + delta, -// _ONE_HUNDRED_PERCENT -// ); - -// // Set current adjustment to inactive (e.g. if we are re-tuning early) -// adjustment.active = false; -// } - -// // Update tune data -// tune.lastTune = currentTime; -// tune.tuneBelowCapacity = capacity > tune.tuneIntervalCapacity -// ? capacity - tune.tuneIntervalCapacity -// : 0; - -// // Calculate the correct payout to complete on time assuming each bond -// // will be max size in the desired deposit interval for the remaining time -// // -// // i.e. market has 10 days remaining. deposit interval is 1 day. capacity -// // is 10,000 TOKEN. max payout would be 1,000 TOKEN (10,000 * 1 / 10). -// uint256 payoutCapacity = core.capacityInQuote -// ? capacity.mulDiv(style.scale, price_) -// : capacity; -// styleData[id_].maxPayout = payoutCapacity.mulDiv( -// uint256(style.depositInterval), -// timeRemaining -// ); - -// emit Tuned(id_); -// } -// } - -// /* ========== ADMIN FUNCTIONS ========== */ -// /* ========== VIEW FUNCTIONS ========== */ - -// function _adjustedEquilibriumPrice( -// uint256 equilibriumPrice_, -// Adjustment memory adjustment_ -// ) internal view returns (uint256) { -// // Adjustment should be active if passed to this function. -// // Calculate the change to apply based on the time elapsed since the last adjustment. -// uint256 timeElapsed = block.timestamp - adjustment_.lastAdjustment; - -// // If timeElapsed is zero, return early since the adjustment has already been applied up to the present. -// if (timeElapsed == 0) return equilibriumPrice_; - -// uint256 timeToAdjusted = adjustment_.timeToAdjusted; -// bool stillActive = timeElapsed < timeToAdjusted; -// uint256 change = stillActive -// ? adjustment_.change.mulDiv(timeElapsed, timeToAdjusted) -// : adjustment_.change; -// return equilibriumPrice_ - change; -// } - -// /// @inheritdoc ISequentialDutchAuctioneer -// function marketPrice(uint256 id_) public view override returns (uint256) { -// CoreData memory core = coreData[id_]; -// AuctionData memory auction = auctionData[id_]; - -// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point -// uint256 initialCapacity = core.capacity + -// (core.capacityInQuote ? core.purchased : core.sold); - -// // Compute seconds remaining until market will conclude -// uint256 timeRemaining = core.conclusion - block.timestamp; - -// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point -// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold -// uint256 expectedCapacity = initialCapacity.mulDiv( -// timeRemaining, -// uint256(core.conclusion) - uint256(core.start) -// ); - -// // If tuning, apply any active adjustments to the equilibrium price before decaying -// uint256 price = auction.equilibriumPrice; -// if (auction.tuning) { -// Adjustment memory adjustment = adjustments[id_]; -// if (adjustment.active) price = _adjustedEquilibriumPrice(price, adjustment); -// } - -// // Price is increased or decreased based on how far the market is ahead or behind -// // Intuition: -// // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted -// // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased -// // -// // This implementation uses a linear price decay -// // P(t) = P_eq * (1 + k * (X(t) - C(t) / C(0))) -// // P(t): price at time t -// // P_eq: equilibrium price of the market, initialized by issuer on market creation and potential updated via tuning -// // k: decay speed of the market -// // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. -// // X(t): expected capacity of the market at time t. -// // X(t) = C(0) * t / L. -// // C(t): actual capacity of the market at time t. -// // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). -// uint256 decay; -// if (expectedCapacity > core.capacity) { -// decay = -// _ONE_HUNDRED_PERCENT + -// (auction.decaySpeed * (expectedCapacity - core.capacity)) / -// initialCapacity; -// } else { -// // If actual capacity is greater than expected capacity, we need to check for underflows -// // The decay has a minimum value of 0 since that will reduce the price to 0 as well. -// uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / -// initialCapacity; -// decay = _ONE_HUNDRED_PERCENT > factor ? _ONE_HUNDRED_PERCENT - factor : 0; -// } - -// // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) -// price = price.mulDivUp(decay, _ONE_HUNDRED_PERCENT); - -// // Compare the current price to the minimum price and return the maximum -// return price > auction.minPrice ? price : auction.minPrice; -// } -// } diff --git a/src/modules/auctions/TVGDA.sol b/src/modules/auctions/TVGDA.sol deleted file mode 100644 index db82491d..00000000 --- a/src/modules/auctions/TVGDA.sol +++ /dev/null @@ -1,120 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import "src/modules/auctions/bases/AtomicAuction.sol"; -// import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; - -// /// @notice Two variable GDA. Price is dependent on time. The other variable is independent of time. -// abstract contract TVGDA { -// /* ========== DATA STRUCTURES ========== */ -// enum Decay { -// Linear, -// Exponential -// } - -// /// @notice Auction pricing data -// struct AuctionData { -// uint256 equilibriumPrice; // price at which the auction is balanced when emissions rate is on schedule and the independent variable is zero -// uint256 minimumPrice; // minimum price the auction can reach -// uint256 payoutScale; -// uint256 quoteScale; -// uint48 lastAuctionStart; -// Decay priceDecayType; // type of decay to use for the market price -// Decay variableDecayType; // type of decay to use for the independent variable -// SD59x18 priceDecayConstant; // speed at which the price decays, as SD59x18. -// SD59x18 variableDecayConstant; // speed at which the independent variable decays, as SD59x18. -// SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. -// } -// } - -// contract TwoVariableGradualDutchAuctioneer is AtomicAuctionModule, TVGDA { - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// address auctionHouse_ -// ) Module(auctionHouse_) {} - -// /* ========== AUCTION FUNCTIONS ========== */ - -// function _auction( -// uint96 lotId_, -// Lot memory lot_, -// bytes memory params_ -// ) internal override { -// // Decode params -// ( -// uint256 equilibriumPrice_, // quote tokens per payout token, in quote token decimals -// uint256 minimumPrice_, // fewest quote tokens per payout token acceptable for the auction, in quote token decimals -// Decay priceDecayType_, -// Decay variableDecayType_, -// SD59x18 priceDecayConstant_, -// SD59x18 variableDecayConstant_, -// ) = abi.decode(params_, (uint256, uint256, Decay, Decay, SD59x18, SD59x18)); - -// // Validate params -// // TODO - -// // Calculate scale from payout token decimals -// uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); -// uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); - -// // Calculate emissions rate -// uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; -// SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); - -// // Set auction data -// AuctionData storage auction = auctionData[lotId_]; -// auction.equilibriumPrice = equilibriumPrice_; -// auction.minimumPrice = minimumPrice_; -// auction.payoutScale = payoutScale; -// auction.quoteScale = quoteScale; -// auction.lastAuctionStart = uint48(block.timestamp); -// auction.priceDecayType = priceDecayType_; -// auction.variableDecayType = variableDecayType_; -// auction.priceDecayConstant = priceDecayConstant_; -// auction.variableDecayConstant = variableDecayConstant_; -// auction.emissionsRate = emissionsRate; -// } - -// function _purchase(uint96 lotId_, uint256 amount_, bytes memory variableInput_) internal override returns (uint256) { -// // variableInput should be a single uint256 -// uint256 variableInput = abi.decode(variableInput_, (uint256)); - -// // Calculate payout amount for quote amount and seconds of emissions using GDA formula -// (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_, variableInput); - -// // Update last auction start with seconds of emissions -// // Do not have to check that too many seconds have passed here -// // since payout/amount is checked against capacity in the top-level function -// auctionData[id_].lastAuctionStart += secondsOfEmissions; - -// return payout; -// } - -// function _payoutAndEmissionsFor(uint96 lotId_, uint256 amount_, uint256 variableInput_) internal view override returns (uint256) { -// // Load decay types for lot -// priceDecayType = auctionData[lotId_].priceDecayType; -// variableDecayType = auctionData[lotId_].variableDecayType; - -// // Get payout information based on the various combinations of decay types -// if (priceDecayType == Decay.Linear && variableDecayType == Decay.Linear) { -// return _payoutForLinLin(auction, amount_, variableInput_); -// } else if (priceDecayType == Decay.Linear && variableDecayType == Decay.Exponential) { -// return _payoutForLinExp(auction, amount_, variableInput_); -// } else if (priceDecayType == Decay.Exponential && variableDecayType == Decay.Linear) { -// return _payoutForExpLin(auction, amount_, variableInput_); -// } else { -// return _payoutForExpExp(auction, amount_, variableInput_); -// } -// } - -// // TODO problem with having a minimum price -> messes up the math and the inverse solution is not closed form -// function _payoutForExpExp( -// uint96 lotId_, -// uint256 amount_, -// uint256 variableInput_ -// ) internal view returns (uint256, uint48) { - -// } -// } diff --git a/src/modules/auctions/bases/AtomicAuction.sol b/src/modules/auctions/bases/AtomicAuction.sol deleted file mode 100644 index 1b301535..00000000 --- a/src/modules/auctions/bases/AtomicAuction.sol +++ /dev/null @@ -1,82 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import "src/modules/Auction.sol"; - -// abstract contract AtomicAuction { - -// // ========== AUCTION INFORMATION ========== // - -// function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); - -// function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); - -// function maxPayout(uint256 id_) public view virtual returns (uint256); - -// function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); -// } - -// abstract contract AtomicAuctionModule is AuctionModule, AtomicAuction { - -// // ========== AUCTION EXECUTION ========== // - -// function purchase(uint256 id_, uint256 amount_, bytes calldata auctionData_) external override onlyParent returns (uint256 payout, bytes memory auctionOutput) { -// Lot storage lot = lotData[id_]; - -// // Check if market is live, if not revert -// if (!isLive(id_)) revert Auction_MarketNotActive(); - -// // Get payout from implementation-specific auction logic -// payout = _purchase(id_, amount_); - -// // Update Capacity - -// // Capacity is either the number of payout tokens that the market can sell -// // (if capacity in quote is false), -// // -// // or the number of quote tokens that the market can buy -// // (if capacity in quote is true) - -// // If amount/payout is greater than capacity remaining, revert -// if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) -// revert Auction_NotEnoughCapacity(); -// // Capacity is decreased by the deposited or paid amount -// lot.capacity -= lot.capacityInQuote ? amount_ : payout; - -// // Markets keep track of how many quote tokens have been -// // purchased, and how many payout tokens have been sold -// lot.purchased += amount_; -// lot.sold += payout; -// } - -// /// @dev implementation-specific purchase logic can be inserted by overriding this function -// function _purchase( -// uint256 id_, -// uint256 amount_, -// uint256 minAmountOut_ -// ) internal virtual returns (uint256); - -// function bid(uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_) external override onlyParent { -// revert Auction_NotImplemented(); -// } - -// function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { -// revert Auction_NotImplemented(); -// } - -// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { -// revert Auction_NotImplemented(); -// } - -// // ========== AUCTION INFORMATION ========== // - -// // These functions do not include fees. Policies can call these functions with the after-fee amount to get a payout value. -// // TODO -// // function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); - -// // function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); - -// // function maxPayout(uint256 id_) public view virtual returns (uint256); - -// // function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); -// } diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol deleted file mode 100644 index 395f849c..00000000 --- a/src/modules/auctions/bases/BatchAuction.sol +++ /dev/null @@ -1,156 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// TODO may not need this file. Lot of implementation specifics. - -import {AuctionModule} from "src/modules/Auction.sol"; - -// Spec -// - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token -// - Purchasers will submit orders off-chain that will be batched and submitted at the end of the auction by a Teller. All Tellers should be able to execute batches of orders? -// - The issuer will provide all relevant information for the running of the batch auction to this contract. Some parameters for derivatives of the payout will be passed onto and processed by the Teller. -// - The issuer should be able to auction different variables in the purchase. -// I need to determine if this should be handled by different batch auctioneers. -// - There are some overlap with the variables used in Live Auctions, so those should be abstracted and inherited so we don't repeat ourselves. -// - Data needed for a batch auction: -// - capacity - amount of tokens being sold (or bought?) -// - quote token -// - payout token -// - teller -// - teller params -// - duration (start & conclusion) -// - allowlist -// - amount sold & amount purchased - do we need to track this since it is just for historical purposes? can we emit the data in an event? -// - minimum value to settle auction - minimum value for whatever parameter is being auctioned. -// need to think if we need to have a maximum value option, but it can probably just use an inverse. -// - info to tell the teller what the auctioned value is and how to settle the auction. need to think on this more - -abstract contract BatchAuction { - error BatchAuction_NotConcluded(); - - // ========== AUCTION INFORMATION ========== // - - // TODO add batch auction specific getters -} - -abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { -// // ========== STATE VARIABLES ========== // - -// mapping(uint96 lotId => Auction.Bid[] bids) public lotBids; - -// /// @inheritdoc AuctionModule -// function _bid( -// uint96 lotId_, -// address bidder_, -// address recipient_, -// address referrer_, -// uint256 amount_, -// bytes calldata auctionData_ -// ) internal override returns (uint96 bidId) { -// // TODO -// // Validate inputs - -// // Execute user approval if provided? - -// // Call implementation specific bid logic - -// // Store bid data -// } - -// /// @inheritdoc Auction -// function settle( -// uint96 lotId, -// Auction.Bid[] memory winningBids_, -// bytes calldata settlementProof_, -// bytes calldata settlementData_ -// ) -// external -// override -// onlyParent -// returns (uint256[] memory amountsOut, bytes memory auctionOutput) -// { -// // TODO -// // Validate inputs - -// // Call implementation specific settle logic - -// // Store settle data -// } -} - -// abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { -// // ========== AUCTION EXECUTION ========== // - -// function bid( -// uint96 lotId_, -// address recipient_, -// address referrer_, -// uint256 amount_, -// bytes calldata auctionData_, -// bytes calldata approval_ -// ) external override onlyParent { -// revert Auction_NotImplemented(); -// } - -// /// @notice Settle a batch auction with the provided bids -// function settle( -// uint256 id_, -// Bid[] memory bids_ -// ) external onlyParent returns (uint256[] memory amountsOut) { -// Lot storage lot = lotData[id_]; - -// // Must be past the conclusion time to settle -// if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); - -// // Bids must not be greater than the capacity -// uint256 len = bids_.length; -// uint256 sum; -// if (lot.capacityInQuote) { -// for (uint256 i; i < len; i++) { -// sum += bids_[i].amount; -// } -// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); -// } else { -// for (uint256 i; i < len; i++) { -// sum += bids_[i].minAmountOut; -// } -// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); -// } - -// // Get amounts out from implementation-specific auction logic -// amountsOut = _settle(id_, bids_); -// } - -// function _settle( -// uint256 id_, -// Bid[] memory bids_ -// ) internal virtual returns (uint256[] memory amountsOut); -// } - -// abstract contract ExternalBatchAuction is AuctionModule, BatchAuction { -// function bid( -// uint96 lotId_, -// address recipient_, -// address referrer_, -// uint256 amount_, -// bytes calldata auctionData_, -// bytes calldata approval_ -// ) external override onlyParent { -// revert Auction_NotImplemented(); -// } - -// /// @inheritdoc Auction -// function settle( -// uint96 lotId, -// Auction.Bid[] memory winningBids_, -// bytes calldata settlementProof_, -// bytes calldata settlementData_ -// ) -// external -// override -// onlyParent -// returns (uint256[] memory amountsOut) -// { -// revert Auction_NotImplemented(); -// } -// } diff --git a/src/modules/auctions/bases/DiscreteAuction.sol b/src/modules/auctions/bases/DiscreteAuction.sol deleted file mode 100644 index afdb2001..00000000 --- a/src/modules/auctions/bases/DiscreteAuction.sol +++ /dev/null @@ -1,186 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -// import "src/modules/auctions/bases/AtomicAuction.sol"; - -// abstract contract DiscreteAuction { -// /* ========== ERRORS ========== */ -// error Auction_MaxPayoutExceeded(); - -// /* ========== DATA STRUCTURES ========== */ -// struct StyleData { -// uint48 depositInterval; // target interval between deposits -// uint256 maxPayout; // maximum payout for a single purchase -// uint256 scale; // stored scale for auction price -// } - -// /* ========== STATE ========== */ - -// /// @notice Minimum deposit interval for a discrete auction -// uint48 public minDepositInterval; - -// mapping(uint96 lotId => StyleData style) public styleData; - -// /* ========== ADMIN FUNCTIONS ========== */ - -// /// @notice Set the minimum deposit interval -// /// @notice Access controlled -// /// @param depositInterval_ Minimum deposit interval in seconds -// function setMinDepositInterval(uint48 depositInterval_) external; - -// /* ========== VIEW FUNCTIONS ========== */ - -// /// @notice Calculate current auction price of base token in quote tokens -// /// @param id_ ID of auction -// /// @return Price for auction in configured decimals -// function auctionPrice(uint256 id_) external view returns (uint256); - -// /// @notice Scale value to use when converting between quote token and base token amounts with auctionPrice() -// /// @param id_ ID of auction -// /// @return Scaling factor for auction in configured decimals -// function auctionScale(uint256 id_) external view returns (uint256); - -// function maxPayout(uint256 id_) external view returns(uint256); -// } - -// abstract contract DiscreteAuctionModule is AtomicAuctionModule, DiscreteAuction { - -// /* ========== ERRORS ========== */ -// error InvalidParams(); - -// /* ========== CONSTRUCTOR ========== */ - -// constructor( -// address auctionHouse_ -// ) AtomicAuctionModule(auctionHouse_) { -// minDepositInterval = 1 hours; -// } - -// /* ========== MARKET FUNCTIONS ========== */ - -// function _auction( -// uint256 id_, -// Lot memory lot_, -// bytes calldata params_ -// ) internal override { -// // Decode provided params -// (uint48 depositInterval, bytes calldata params) = abi.decode(params_, (uint48, bytes)); - -// // Validate that deposit interval is in-bounds -// uint48 duration = lot_.conclusion - lot_.start; -// if (depositInterval < minDepositInterval || depositInterval > duration) -// revert InvalidParams(); - -// // Set style data -// StyleData memory style = styleData[id_]; -// style.depositInterval = depositInterval; -// style.scale = 10 ** lot_.quoteToken.decimals(); - -// // Call internal __createMarket function to store implementation-specific data -// __auction(id_, lot_, style, params); - -// // Set max payout (depends on auctionPrice being available so must be done after __createMarket) -// style.maxPayout = _baseCapacity(id_, lot_).mulDiv(depositInterval, duration); -// } - -// /// @dev implementation-specific auction creation logic can be inserted by overriding this function -// function __auction( -// uint256 id_, -// Lot memory lot_, -// StyleData memory style_, -// bytes memory params_ -// ) internal virtual; - -// /* ========== TELLER FUNCTIONS ========== */ - -// function _purchase(uint256 id_, uint256 amount_, bytes memory auctionData_) internal returns (uint256, bytes memory) { -// // Get payout from implementation-specific purchase logic -// (uint256 payout, bytes memory auctionOutput) = __purchase(id_, amount_, auctionData_); - -// // Check that payout is less than or equal to max payout -// if (payout > styleData[id_].maxPayout) revert Auction_MaxPayoutExceeded(); - -// return (payout, auctionOutput); -// } - -// /// @dev implementation-specific purchase logic can be inserted by overriding this function -// function __purchase(uint256 id_, uint256 amount_, bytes memory auctionData_) internal virtual returns (uint256, bytes memory); - -// /* ========== ADMIN FUNCTIONS ========== */ - -// /// @inheritdoc DiscreteAuction -// function setMinDepositInterval(uint48 depositInterval_) external override onlyParent { -// // Restricted to authorized addresses - -// // Require min deposit interval to be less than minimum auction duration and at least 1 hour -// if (depositInterval_ > minAuctionDuration || depositInterval_ < 1 hours) -// revert Auction_InvalidParams(); - -// minDepositInterval = depositInterval_; -// } - -// /* ========== VIEW FUNCTIONS ========== */ - -// function _baseCapacity(uint256 id_, Lot memory lot_) internal view returns (uint256) { -// // Calculate capacity in terms of base tokens -// // If capacity is in quote tokens, convert to base tokens with auction price -// // Otherwise, return capacity as-is -// return -// lot_.capacityInQuote -// ? lot_.capacity.mulDiv(styleData[id_].scale, auctionPrice(id_)) -// : lot_.capacity; -// } - -// /// @inheritdoc DiscreteAuction -// function auctionPrice(uint256 id_) public view virtual returns (uint256); - -// /// @inheritdoc DiscreteAuction -// function auctionScale(uint256 id_) external view override returns (uint256) { -// return styleData[id_].scale; -// } - -// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract -// function payoutFor(uint256 id_, uint256 amount_) public view override onlyParent returns (uint256) { -// // TODO handle payout greater than max payout - revert? - -// // Calculate payout for amount of quote tokens -// return amount_.mulDiv(styleData[id_].scale, auctionPrice(id_)); -// } - -// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract -// function priceFor(uint256 id_, uint256 payout_) public view override onlyParent returns (uint256) { -// // TODO handle payout greater than max payout - revert? - -// // Calculate price for payout in quote tokens -// return payout_.mulDiv(auctionPrice(id_), styleData[id_].scale); -// } - -// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract -// function maxAmountAccepted(uint256 id_) external view override onlyParent returns (uint256) { -// // Calculate maximum amount of quote tokens that correspond to max bond size -// // Maximum of the maxPayout and the remaining capacity converted to quote tokens -// Lot memory lot = lotData[id_]; -// StyleData memory style = styleData[id_]; -// uint256 price = auctionPrice(id_); -// uint256 quoteCapacity = lot.capacityInQuote -// ? lot.capacity -// : lot.capacity.mulDiv(price, style.scale); -// uint256 maxQuote = style.maxPayout.mulDiv(price, style.scale); -// uint256 amountAccepted = quoteCapacity < maxQuote ? quoteCapacity : maxQuote; - -// return amountAccepted; -// } - -// /// @notice Calculate max payout of the auction in base tokens -// /// @dev Returns a dynamically calculated payout or the maximum set by the creator, whichever is less. -// /// @param id_ ID of auction -// /// @return Current max payout for the auction in base tokens -// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract -// function maxPayout(uint256 id_) public view override onlyParent returns (uint256) { -// // Convert capacity to base token units for comparison with max payout -// uint256 capacity = _baseCapacity(id_, lotData[id_]); - -// // Cap max payout at the remaining capacity -// return styleData[id_].maxPayout > capacity ? capacity : styleData[id_].maxPayout; -// } -// } From a97c0cad8f5802b66ee96ef13fd9fabdaa596316 Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 31 Jan 2024 10:27:48 -0600 Subject: [PATCH 117/117] refactor: capacityInQuote + prefunding to Auctioneer --- src/bases/Auctioneer.sol | 11 +++++------ src/modules/Auction.sol | 3 --- src/modules/auctions/LSBBA/LSBBA.sol | 6 +++--- test/AuctionHouse/auction.t.sol | 2 +- test/modules/auctions/LSBBA/auction.t.sol | 12 ------------ 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index d0e8dd0a..e8b9a2d1 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -266,11 +266,13 @@ abstract contract Auctioneer is WithModules { // Perform pre-funding, if needed // It does not make sense to pre-fund the auction if the capacity is in quote tokens - if (requiresPrefunding == true && params_.capacityInQuote == false) { + if (requiresPrefunding == true) { + // Capacity must be in base token for auctions that require pre-funding + if (params_.capacityInQuote) revert InvalidParams(); + // Store pre-funding information routing.prefunded = true; - // TODO copied from AuctionHouse. Consider consolidating. // Get the balance of the base token before the transfer uint256 balanceBefore = routing_.baseToken.balanceOf(address(this)); @@ -326,10 +328,7 @@ abstract contract Auctioneer is WithModules { module.cancelAuction(lotId_); // If the auction is prefunded and supported, transfer the remaining capacity to the owner - if ( - lotRouting[lotId_].prefunded == true && module.capacityInQuote(lotId_) == false - && lotRemainingCapacity > 0 - ) { + if (lotRouting[lotId_].prefunded == true && lotRemainingCapacity > 0) { // Transfer payout tokens to the owner Routing memory routing = lotRouting[lotId_]; routing.baseToken.safeTransfer(routing.owner, lotRemainingCapacity); diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 0edcae49..165403d4 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -276,9 +276,6 @@ abstract contract AuctionModule is Auction, Module { // Call internal createAuction function to store implementation-specific data (prefundingRequired) = _auction(lotId_, lot, params_.implParams); - // Cannot pre-fund if capacity is in quote token - if (prefundingRequired && lot.capacityInQuote) revert Auction_InvalidParams(); - // Store lot data lotData[lotId_] = lot; diff --git a/src/modules/auctions/LSBBA/LSBBA.sol b/src/modules/auctions/LSBBA/LSBBA.sol index 6866adc6..edb87a2f 100644 --- a/src/modules/auctions/LSBBA/LSBBA.sol +++ b/src/modules/auctions/LSBBA/LSBBA.sol @@ -563,8 +563,6 @@ contract LocalSealedBidBatchAuction is AuctionModule { AuctionDataParams memory implParams = abi.decode(params_, (AuctionDataParams)); // Validate params - // Capacity must be in base token for this auction type - if (lot_.capacityInQuote) revert Auction_InvalidParams(); // minFillPercent must be less than or equal to 100% if (implParams.minFillPercent > _ONE_HUNDRED_PERCENT) revert Auction_InvalidParams(); @@ -588,7 +586,9 @@ contract LocalSealedBidBatchAuction is AuctionModule { data.publicKeyModulus = implParams.publicKeyModulus; // This auction type requires pre-funding - return (true); + // This setting requires the capacity to be in the base token, + // so we know the capacity values above are in base token units. + prefundingRequired = true; } function _cancelAuction(uint96 lotId_) internal override { diff --git a/test/AuctionHouse/auction.t.sol b/test/AuctionHouse/auction.t.sol index 1837f299..d6fe30db 100644 --- a/test/AuctionHouse/auction.t.sol +++ b/test/AuctionHouse/auction.t.sol @@ -546,7 +546,7 @@ contract AuctionTest is Test, Permit2User { whenAuctionCapacityInQuote { // Expect revert - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); + bytes memory err = abi.encodeWithSelector(Auctioneer.InvalidParams.selector); vm.expectRevert(err); auctionHouse.auction(routingParams, auctionParams); diff --git a/test/modules/auctions/LSBBA/auction.t.sol b/test/modules/auctions/LSBBA/auction.t.sol index 656e129f..3482694d 100644 --- a/test/modules/auctions/LSBBA/auction.t.sol +++ b/test/modules/auctions/LSBBA/auction.t.sol @@ -120,8 +120,6 @@ contract LSBBACreateAuctionTest is Test, Permit2User { // [X] it reverts // [X] when the auction parameters are invalid // [X] it reverts - // [X] when capacity in quote is enabled - // [X] it reverts // [X] when minimum fill percentage is more than 100% // [X] it reverts // [X] when minimum bid percentage is less than the minimum @@ -187,16 +185,6 @@ contract LSBBACreateAuctionTest is Test, Permit2User { auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); } - function test_capacityInQuoteIsEnabled_reverts() external whenCapacityInQuoteIsEnabled { - // Expected error - bytes memory err = abi.encodeWithSelector(Auction.Auction_InvalidParams.selector); - vm.expectRevert(err); - - // Call - vm.prank(address(auctionHouse)); - auctionModule.auction(lotId, auctionParams, _quoteTokenDecimals, _baseTokenDecimals); - } - function test_minimumFillPercentageIsMoreThanMax_reverts() external whenMinimumFillPercentageIsMoreThanMax